2020-02-02 19:26:54 +13:00
|
|
|
# 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
|
2020-02-09 08:52:28 +13:00
|
|
|
import random
|
2020-02-02 19:26:54 +13:00
|
|
|
|
|
|
|
import discord
|
|
|
|
from redbot.core import commands, checks
|
|
|
|
from redbot.core.config import Config
|
|
|
|
from redbot.core import bank
|
2020-02-15 20:42:38 +13:00
|
|
|
from redbot.core.utils.chat_formatting import pagify, box
|
2020-02-02 19:26:54 +13:00
|
|
|
|
|
|
|
from .activity import RecordHandler
|
|
|
|
from .converters import configable_guild_defaults, settings_converter
|
|
|
|
|
|
|
|
|
|
|
|
class EconomyTrickle(commands.Cog):
|
|
|
|
"""
|
|
|
|
Automatic Economy gains for active users
|
|
|
|
"""
|
|
|
|
|
2020-02-09 08:52:28 +13:00
|
|
|
__version__ = "2.1.0"
|
2020-02-02 19:26:54 +13:00
|
|
|
|
|
|
|
def __init__(self, bot, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.bot = bot
|
2020-02-02 20:18:16 +13:00
|
|
|
self.config = Config.get_conf(self, identifier=78631113035100160, force_registration=True)
|
2020-02-02 19:26:54 +13:00
|
|
|
self.config.register_guild(
|
|
|
|
active=False,
|
|
|
|
mode="blacklist",
|
|
|
|
blacklist=[],
|
|
|
|
whitelist=[],
|
2020-02-09 08:52:28 +13:00
|
|
|
min_voice_members=2,
|
2020-02-02 19:26:54 +13:00
|
|
|
**configable_guild_defaults,
|
|
|
|
# custom_level_table={}, # TODO
|
|
|
|
)
|
|
|
|
self.config.register_member(xp=0, level=0)
|
|
|
|
self.recordhandler = RecordHandler()
|
|
|
|
|
|
|
|
self.main_loop_task = bot.loop.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 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
|
2020-02-09 08:52:28 +13:00
|
|
|
if minutes[g] % data[g.id]["interval"] == 0:
|
|
|
|
minutes[g] = 0
|
|
|
|
now = datetime.utcnow()
|
2020-02-02 20:18:16 +13:00
|
|
|
tsk = self.bot.loop.create_task(self.do_rewards_for(g, now, data[g.id]))
|
2020-02-02 19:26:54 +13:00
|
|
|
self.extra_tasks.append(tsk)
|
|
|
|
|
|
|
|
async def do_rewards_for(self, guild: discord.Guild, now: datetime, data: dict):
|
|
|
|
|
2020-02-09 08:52:28 +13:00
|
|
|
after = now - timedelta(minutes=data["interval"], seconds=10)
|
|
|
|
voice_mem = await self.config.guild(guild).min_voice_members()
|
2020-02-02 19:26:54 +13:00
|
|
|
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):
|
2020-02-09 08:53:01 +13:00
|
|
|
return (
|
|
|
|
len(mem.voice.channel.members) > voice_mem
|
|
|
|
and mem.voice.channel.id not in data["blacklist"]
|
|
|
|
and not mem.bot
|
|
|
|
)
|
2020-02-02 19:26:54 +13:00
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
def mpred(m: discord.Message):
|
|
|
|
return m.channel.id in data["whitelist"]
|
|
|
|
|
|
|
|
def vpred(mem: discord.Member):
|
|
|
|
with contextlib.suppress(AttributeError):
|
2020-02-09 08:53:01 +13:00
|
|
|
return (
|
|
|
|
len(mem.voice.channel.members) > voice_mem
|
|
|
|
and mem.voice.channel.id in data["whitelist"]
|
|
|
|
and not mem.bot
|
|
|
|
)
|
2020-02-02 19:26:54 +13:00
|
|
|
|
2020-02-02 20:18:16 +13:00
|
|
|
has_active_message = set(self.recordhandler.get_active_for_guild(guild=guild, after=after, message_check=mpred))
|
2020-02-02 19:26:54 +13:00
|
|
|
|
|
|
|
is_active_voice = {m for m in guild.members if vpred(m)}
|
|
|
|
is_active = has_active_message | is_active_voice
|
|
|
|
|
2020-02-09 08:52:28 +13:00
|
|
|
# take exp away from inactive users
|
|
|
|
for member in guild.members:
|
|
|
|
if member in is_active:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# loose exp per interval
|
2020-02-09 09:08:35 +13:00
|
|
|
xp = max(data["xp_per_interval"] * data["decay_rate"], 1)
|
2020-02-09 08:52:28 +13:00
|
|
|
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"]
|
2020-02-02 19:26:54 +13:00
|
|
|
|
2020-02-09 08:52:28 +13:00
|
|
|
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
|
2020-02-02 19:26:54 +13:00
|
|
|
# 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
|
2020-02-09 08:52:28 +13:00
|
|
|
async def setstuff(self, ctx, *, data: settings_converter = None):
|
2020-02-02 19:26:54 +13:00
|
|
|
"""
|
|
|
|
Set other variables
|
|
|
|
|
2020-02-09 08:52:28 +13:00
|
|
|
format *example* for this (and defaults):
|
2020-02-02 19:26:54 +13:00
|
|
|
|
|
|
|
```yaml
|
|
|
|
bonus_per_level: 5
|
|
|
|
econ_per_interval: 20
|
2020-02-09 08:52:28 +13:00
|
|
|
fail_rate: 0.2
|
|
|
|
decay_rate: 0.5
|
2020-02-02 19:26:54 +13:00
|
|
|
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
|
|
|
|
```
|
|
|
|
"""
|
2020-02-09 08:52:28 +13:00
|
|
|
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
|
|
|
|
|
2020-02-02 19:26:54 +13:00
|
|
|
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()
|
|
|
|
|
2020-02-09 08:52:28 +13:00
|
|
|
@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()
|
|
|
|
|
2020-02-02 19:26:54 +13:00
|
|
|
@ect.command(name="addchan")
|
2020-02-02 20:18:16 +13:00
|
|
|
async def rset_add_chan(self, ctx, *channels: Union[discord.TextChannel, discord.VoiceChannel]):
|
2020-02-02 19:26:54 +13:00
|
|
|
"""
|
|
|
|
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:
|
2020-02-15 20:42:38 +13:00
|
|
|
return await ctx.send(f"You need to set a mode using `{ctx.clean_prefix}trickleset mode` first")
|
2020-02-02 19:26:54 +13:00
|
|
|
|
|
|
|
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")
|
2020-02-02 20:18:16 +13:00
|
|
|
async def rset_rem_chan(self, ctx, *channels: Union[discord.TextChannel, discord.VoiceChannel]):
|
2020-02-02 19:26:54 +13:00
|
|
|
"""
|
|
|
|
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:
|
2020-02-02 20:18:16 +13:00
|
|
|
return await ctx.send(f"You need to set a mode using `{ctx.clean_prefix}trickleset mode` first")
|
2020-02-02 19:26:54 +13:00
|
|
|
|
|
|
|
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()
|
2020-02-15 20:42:38 +13:00
|
|
|
|
|
|
|
@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]):
|
2020-02-15 20:45:55 +13:00
|
|
|
msg += f"{i+1}. {ctx.guild.get_channel(channel)}\n"
|
2020-02-15 20:42:38 +13:00
|
|
|
|
|
|
|
pages = pagify(msg)
|
|
|
|
for page in pages:
|
|
|
|
await ctx.send(box(page))
|