# pylint: disable=not-async-context-manager import asyncio import contextlib from typing import no_type_check, Union from datetime import datetime, timedelta from collections import defaultdict import random import discord from redbot.core import commands, checks from redbot.core.config import Config from redbot.core import bank from redbot.core.utils.chat_formatting import pagify, box from typing import Literal from .activity import RecordHandler from .converters import configable_guild_defaults, settings_converter class EconomyTrickle(commands.Cog): """ Automatic Economy gains for active users """ __version__ = "2.1.0" def __init__(self, bot, *args, **kwargs): super().__init__(*args, **kwargs) self.bot = bot self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True) self.config.register_guild( active=False, mode="blacklist", blacklist=[], whitelist=[], min_voice_members=2, **configable_guild_defaults, # custom_level_table={}, # TODO ) self.config.register_member(xp=0, level=0) self.recordhandler = RecordHandler() self.main_loop_task = asyncio.create_task(self.main_loop()) self.extra_tasks = [] def cog_unload(self): self.main_loop_task.cancel() for t in self.extra_tasks: t.cancel() @commands.Cog.listener() async def on_message(self, message): if await self.bot.cog_disabled_in_guild(self, message.guild): return if message.guild and await self.config.guild(message.guild).active(): self.recordhandler.proccess_message(message) async def main_loop(self): minutes = defaultdict(int) while self is self.bot.get_cog(self.__class__.__name__): await asyncio.sleep(60) data = await self.config.all_guilds() for g in self.bot.guilds: if g.id in data and data[g.id]["active"]: minutes[g] += 1 if minutes[g] % data[g.id]["interval"] == 0: minutes[g] = 0 now = datetime.utcnow() tsk = asyncio.create_task(self.do_rewards_for(g, now, data[g.id])) self.extra_tasks.append(tsk) async def do_rewards_for(self, guild: discord.Guild, now: datetime, data: dict): after = now - timedelta(minutes=data["interval"], seconds=10) voice_mem = await self.config.guild(guild).min_voice_members() if data["mode"] == "blacklist": def mpred(m: discord.Message): return m.channel.id not in data["blacklist"] def vpred(mem: discord.Member): with contextlib.suppress(AttributeError): return ( len(mem.voice.channel.members) > voice_mem and mem.voice.channel.id not in data["blacklist"] and not mem.bot ) else: def mpred(m: discord.Message): return m.channel.id in data["whitelist"] def vpred(mem: discord.Member): with contextlib.suppress(AttributeError): return ( len(mem.voice.channel.members) > voice_mem and mem.voice.channel.id in data["whitelist"] and not mem.bot ) has_active_message = set(self.recordhandler.get_active_for_guild(guild=guild, after=after, message_check=mpred)) is_active_voice = {m for m in guild.members if vpred(m)} is_active = has_active_message | is_active_voice # take exp away from inactive users for member in guild.members: if member in is_active: continue # loose exp per interval xp = max(data["xp_per_interval"] * data["decay_rate"], 1) xp = await self.config.member(member).xp() - xp xp = max(xp, 0) await self.config.member(member).xp.set(xp) # update level on these users level, next_needed = 0, data["level_xp_base"] while xp >= next_needed: level += 1 xp -= next_needed next_needed += data["xp_lv_increase"] if data["maximum_level"] is not None: level = min(data["maximum_level"], level) await self.config.member(member).level.set(level) for member in is_active: # failed for this member, skip if data["fail_rate"] > random.random(): continue # xp processing first xp = data["xp_per_interval"] if member in has_active_message: xp += data["extra_message_xp"] if member in is_active_voice: xp += data["extra_voice_xp"] xp = xp + await self.config.member(member).xp() await self.config.member(member).xp.set(xp) # level up: new mode in future. level, next_needed = 0, data["level_xp_base"] while xp >= next_needed: level += 1 xp -= next_needed next_needed += data["xp_lv_increase"] if data["maximum_level"] is not None: level = min(data["maximum_level"], level) await self.config.member(member).level.set(level) # give economy to_give = data["econ_per_interval"] bonus = data["bonus_per_level"] * level if data["maximum_bonus"] is not None: bonus = min(data["maximum_bonus"], bonus) to_give += bonus try: await bank.deposit_credits(member, to_give) except bank.errors.BalanceTooHigh: pass # cleanup old message objects self.recordhandler.clear_before(guild=guild, before=after) # Commands go here @checks.admin_or_permissions(manage_guild=True) @commands.group(name="trickleset") async def ect(self, ctx): """ Settings for economy trickle """ pass @ect.command() async def active(self, ctx, active: bool): """ Sets this as active (or not) """ await self.config.guild(ctx.guild).active.set(active) await ctx.tick() @ect.command() @no_type_check async def setstuff(self, ctx, *, data: settings_converter = None): """ Set other variables format *example* for this (and defaults): ```yaml bonus_per_level: 5 econ_per_interval: 20 fail_rate: 0.2 decay_rate: 0.5 extra_message_xp: 0 extra_voice_xp: 0 interval: 5 level_xp_base: 100 maximum_bonus: null maximum_level: null xp_lv_increase: 50 xp_per_interval: 10 ``` """ if not data: data = await self.config.guild(ctx.guild).all() keys = list(configable_guild_defaults.keys()) msg = "Current data: (run `help trickleset setstuff to set`)\n```yaml\n" for key in keys: msg += f"{key}: {data[key]}\n" msg += "```" await ctx.send(msg) return for k, v in data.items(): await self.config.guild(ctx.guild).get_attr(k).set(v) await ctx.tick() @ect.command(name="mode") async def rset_set_mode(self, ctx, *, mode: str = ""): """ Whether to operate on a `whitelist`, or a `blacklist` """ mode = mode.lower() if mode not in ("whitelist", "blacklist"): return await ctx.send_help() await self.config.guild(ctx.guild).mode.set(mode) await ctx.tick() @ect.command(name="voice") async def rset_voicemem(self, ctx, min_voice_members: int = 0): """ Minimum number of voice members needed to count as active. If users are in a voice chat, only trickle if there are at least min_voice_members in there. """ if min_voice_members < 1: curr = await self.config.guild(ctx.guild).min_voice_members() await ctx.send(f"Current: {curr}") return await self.config.guild(ctx.guild).min_voice_members.set(min_voice_members) await ctx.tick() @ect.command(name="addchan") async def rset_add_chan(self, ctx, *channels: Union[discord.TextChannel, discord.VoiceChannel]): """ Adds one or more channels to the current mode's settings """ if not channels: return await ctx.send_help() gsets = await self.config.guild(ctx.guild).all() mode = gsets["mode"] if not mode: return await ctx.send(f"You need to set a mode using `{ctx.clean_prefix}trickleset mode` first") for channel in channels: if channel.id not in gsets[mode]: gsets[mode].append(channel.id) await self.config.guild(ctx.guild).set_raw(mode, value=gsets[mode]) await ctx.tick() @ect.command(name="remchan") async def rset_rem_chan(self, ctx, *channels: Union[discord.TextChannel, discord.VoiceChannel]): """ removes one or more channels from the current mode's settings """ if not channels: return await ctx.send_help() gsets = await self.config.guild(ctx.guild).all() mode = gsets["mode"] if not mode: return await ctx.send(f"You need to set a mode using `{ctx.clean_prefix}trickleset mode` first") for channel in channels: while channel.id in gsets[mode]: gsets[mode].remove(channel.id) await self.config.guild(ctx.guild).set_raw(mode, value=gsets[mode]) await ctx.tick() @ect.command(name="list") async def rset_list_chan(self, ctx): """ List current channels. """ gsets = await self.config.guild(ctx.guild).all() mode = gsets["mode"] if not mode: return await ctx.send(f"You need to set a mode using `{ctx.clean_prefix}trickleset mode` first") if not gsets[mode]: await ctx.send("No channels defined.") return msg = f"Mode: {mode}\n" for i, channel in enumerate(gsets[mode]): msg += f"{i+1}. {ctx.guild.get_channel(channel)}\n" pages = pagify(msg) for page in pages: await ctx.send(box(page)) async def red_delete_data_for_user( self, *, requester: Literal["discord_deleted_user", "owner", "user", "user_strict"], user_id: int, ): pass