277 lines
12 KiB
Python
277 lines
12 KiB
Python
import asyncio
|
|
import logging
|
|
import random
|
|
from copy import copy
|
|
from time import perf_counter
|
|
|
|
import discord
|
|
from redbot.core import commands
|
|
|
|
from ..abc import MixinMeta
|
|
from ..common.models import GuildSettings, VoiceTracking
|
|
|
|
log = logging.getLogger("red.vrt.levelup.listeners.voice")
|
|
|
|
|
|
class VoiceListener(MixinMeta):
|
|
async def initialize_voice_states(self) -> int:
|
|
self.voice_tracking.clear()
|
|
|
|
def _init() -> int:
|
|
initialized = 0
|
|
perf = perf_counter()
|
|
for guild in self.bot.guilds:
|
|
if guild.id not in self.db.configs:
|
|
continue
|
|
conf = self.db.get_conf(guild)
|
|
if not conf.enabled:
|
|
continue
|
|
for member in guild.members:
|
|
if member.voice and member.voice.channel:
|
|
self.get_init_state(conf, member, perf)
|
|
initialized += 1
|
|
return initialized
|
|
|
|
return await asyncio.to_thread(_init)
|
|
|
|
def get_init_state(
|
|
self,
|
|
conf: GuildSettings,
|
|
member: discord.Member,
|
|
perf: float = None,
|
|
state: discord.VoiceState = None,
|
|
) -> VoiceTracking:
|
|
earning_xp = self.can_gain_exp(conf, member, state or member.voice)
|
|
return self.voice_tracking[member.guild.id].setdefault(
|
|
member.id,
|
|
VoiceTracking(
|
|
joined=perf or perf_counter(),
|
|
not_gaining_xp=not earning_xp,
|
|
not_gaining_xp_time=0.0,
|
|
stopped_gaining_xp_at=perf if not earning_xp else None,
|
|
),
|
|
)
|
|
|
|
@commands.Cog.listener()
|
|
async def on_member_update(self, before: discord.Member, after: discord.Member) -> None:
|
|
# If roles changed and user is in VC, we need to check if they can gain exp
|
|
if before.roles != after.roles and (after.voice or before.voice):
|
|
await self._on_voice_state_update(after, before.voice, after.voice)
|
|
|
|
@commands.Cog.listener()
|
|
async def on_presence_update(self, before: discord.Member, after: discord.Member) -> None:
|
|
# If user goes offline/online and they're in VC, we need to check if they can gain exp
|
|
if before.status != after.status and (after.voice or before.voice):
|
|
await self._on_voice_state_update(after, before.voice, after.voice)
|
|
|
|
@commands.Cog.listener()
|
|
async def on_voice_state_update(
|
|
self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState
|
|
):
|
|
try:
|
|
await self._on_voice_state_update(member, before, after)
|
|
except Exception as e:
|
|
log.error(f"Error in voice state update event.\nBefore: {before}\nAfter: {after}", exc_info=e)
|
|
|
|
async def _on_voice_state_update(
|
|
self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState
|
|
):
|
|
if member.bot and self.db.ignore_bots:
|
|
return
|
|
conf = self.db.get_conf(member.guild)
|
|
if not conf.enabled:
|
|
return
|
|
voice = self.voice_tracking[member.guild.id]
|
|
if not before.channel and not after.channel:
|
|
log.error(f"False voice state update for {member.name} in {member.guild}")
|
|
voice.pop(member.id, None)
|
|
return
|
|
|
|
perf = perf_counter()
|
|
|
|
if before.channel == after.channel:
|
|
log.debug(f"Voice state changed for {member.name} in {member.guild}")
|
|
earning_xp = self.can_gain_exp(conf, member, after)
|
|
user_data = self.get_init_state(conf, member, perf, after)
|
|
if user_data.not_gaining_xp and earning_xp:
|
|
log.debug(f"{member.name} now earning xp in {after.channel.name} in {member.guild}")
|
|
# User's state change means they can now earn exp again
|
|
user_data.not_gaining_xp = False
|
|
user_data.not_gaining_xp_time += perf - user_data.stopped_gaining_xp_at
|
|
user_data.stopped_gaining_xp_at = None
|
|
elif not user_data.not_gaining_xp and not earning_xp:
|
|
log.debug(f"{member.name} no longer earning xp in {after.channel.name} in {member.guild}")
|
|
# User's state change means they shouldnt earn exp
|
|
user_data.not_gaining_xp = True
|
|
user_data.stopped_gaining_xp_at = perf
|
|
else:
|
|
# No meaningful state change, just return
|
|
pass
|
|
return
|
|
|
|
# Case 1: User joins VC
|
|
if not before.channel and after.channel:
|
|
log.debug(f"{member.name} joined VC {after.channel.name} in {member.guild}")
|
|
self.get_init_state(conf, member, perf, after)
|
|
# Go ahead and update other users in the channel
|
|
for m in after.channel.members:
|
|
if m.id == member.id:
|
|
# We just initialized them
|
|
continue
|
|
user_data = self.get_init_state(conf, m, perf)
|
|
earning_xp = self.can_gain_exp(conf, m, m.voice)
|
|
if user_data.not_gaining_xp and earning_xp:
|
|
log.debug(f"{m.name} now earning xp in {after.channel.name} in {member.guild}")
|
|
user_data.not_gaining_xp = False
|
|
user_data.not_gaining_xp_time += perf - user_data.stopped_gaining_xp_at
|
|
user_data.stopped_gaining_xp_at = None
|
|
elif not user_data.not_gaining_xp and not earning_xp:
|
|
log.debug(f"{m.name} no longer earning xp in {after.channel.name} in {member.guild}")
|
|
user_data.not_gaining_xp = True
|
|
user_data.stopped_gaining_xp_at = perf
|
|
# No exp needs to be added here so just return
|
|
return
|
|
|
|
# Case 2: User switches VC
|
|
if before.channel and after.channel:
|
|
# Treat this as a the user leaving the VC and then joining the new one
|
|
# We'll fire off two events, one for leaving and one for joining
|
|
# This will also reduce the amount of code that needs to be duplicated
|
|
|
|
# Simulate user leaving the VC
|
|
mock_after_voice_state = copy(after)
|
|
mock_after_voice_state.channel = None
|
|
await self._on_voice_state_update(member, before, mock_after_voice_state)
|
|
|
|
# Simulate user joining the new VC
|
|
mock_before_voice_state = before
|
|
mock_before_voice_state.channel = None
|
|
await self._on_voice_state_update(member, mock_before_voice_state, after)
|
|
|
|
# No exp needs to be added here so just return
|
|
return
|
|
|
|
# Case 3: If we're here, the user left the VC
|
|
log.debug(f"{member.name} left VC {before.channel.name} in {member.guild}")
|
|
# First lets add the time to the user, and exp if they were earning it
|
|
data = voice.pop(member.id, None)
|
|
if not data:
|
|
# User wasnt in the voice cache, maybe cog was reloaded while user was in VC?
|
|
log.error(
|
|
f"User {member.name} left VC but wasnt in voice cache in {member.guild}\nBefore: {before}\nAfter: {after}"
|
|
)
|
|
return
|
|
# Add whatever time is left that the user wasnt gaining exp to their total time not gaining exp
|
|
if data.not_gaining_xp and data.stopped_gaining_xp_at:
|
|
data.not_gaining_xp_time += perf - data.stopped_gaining_xp_at
|
|
# Calculate the total time the user spent in the VC
|
|
total_time_in_voice = perf - data.joined
|
|
# Effective time is the total time minus the time they weren't earning exp
|
|
effective_time = total_time_in_voice - data.not_gaining_xp_time
|
|
profile = conf.get_profile(member)
|
|
weekly = conf.get_weekly_profile(member) if conf.weeklysettings.on else None
|
|
|
|
log.debug(f"{member.name} spent {round(total_time_in_voice, 2)}s in VC {before.channel.name} in {member.guild}")
|
|
if effective_time > 0:
|
|
log.debug(f"{round(effective_time, 2)}s of that was effective time")
|
|
profile.voice += total_time_in_voice
|
|
if weekly:
|
|
weekly.voice += total_time_in_voice
|
|
|
|
# Calculate the exp to add
|
|
xp_to_add = conf.voicexp * (effective_time / 60)
|
|
cat_id = getattr(before.channel.category, "id", 0)
|
|
if before.channel.id in conf.channelbonus.voice:
|
|
xp_to_add += random.randint(*conf.channelbonus.voice[before.channel.id]) * (effective_time / 60)
|
|
elif cat_id in conf.channelbonus.voice:
|
|
xp_to_add += random.randint(*conf.channelbonus.voice[cat_id]) * (effective_time / 60)
|
|
|
|
# Stack all role bonuses
|
|
role_ids = [role.id for role in member.roles]
|
|
for role_id, (bonus_min, bonus_max) in conf.rolebonus.voice.items():
|
|
if role_id in role_ids:
|
|
xp_to_add += random.randint(bonus_min, bonus_max) * (effective_time / 60)
|
|
|
|
# Add the exp to the user
|
|
if xp_to_add:
|
|
log.debug(f"Adding {round(xp_to_add, 2)} Voice XP to {member.name} in {member.guild}")
|
|
profile.xp += xp_to_add
|
|
if weekly:
|
|
weekly.xp += xp_to_add
|
|
|
|
# Now we need to update everyone else in the channel in case the exp gaining states have changed
|
|
# Get the channel now that the user has left
|
|
channel: discord.VoiceChannel = member.guild.get_channel(before.channel.id)
|
|
if not channel:
|
|
# User left channel because it was deleted?
|
|
log.warning(f"User {member.name} left VC {before.channel.name} but channel wasnt found in {member.guild}")
|
|
else:
|
|
# Update everyone else in the channel
|
|
for m in channel.members:
|
|
if m.id == member.id:
|
|
continue
|
|
earning_xp = self.can_gain_exp(conf, m, m.voice)
|
|
user_data = self.get_init_state(conf, m, perf)
|
|
if user_data.not_gaining_xp and earning_xp:
|
|
log.debug(f"{m.name} now earning xp in {channel.name} in {member.guild}")
|
|
user_data.not_gaining_xp = False
|
|
user_data.not_gaining_xp_time += perf - user_data.stopped_gaining_xp_at
|
|
user_data.stopped_gaining_xp_at = None
|
|
elif not user_data.not_gaining_xp and not earning_xp:
|
|
log.debug(f"{m.name} no longer earning xp in {channel.name} in {member.guild}")
|
|
user_data.not_gaining_xp = True
|
|
user_data.stopped_gaining_xp_at = perf
|
|
|
|
# Save the changes
|
|
self.save()
|
|
# Check for levelups
|
|
await self.check_levelups(member.guild, member, profile, conf, channel=channel)
|
|
|
|
def can_gain_exp(
|
|
self,
|
|
conf: GuildSettings,
|
|
member: discord.Member,
|
|
voice_state: discord.VoiceState,
|
|
) -> bool:
|
|
"""Determine whether a user can gain exp in the current voice state
|
|
|
|
voice_state may be the before or after voice state, depending on the event
|
|
|
|
Args:
|
|
conf (GuildSettings): The guild settings
|
|
member (discord.Member): The member to check
|
|
voice_state (discord.VoiceState): The current state of the user in the VC
|
|
|
|
Returns:
|
|
bool: Whether the user can gain exp
|
|
"""
|
|
addxp = True
|
|
if conf.ignore_deafened and voice_state.self_deaf:
|
|
addxp = False
|
|
elif conf.ignore_muted and voice_state.self_mute:
|
|
addxp = False
|
|
elif conf.ignore_invisible and member.status.name == "offline":
|
|
addxp = False
|
|
elif any(role.id in conf.ignoredroles for role in member.roles):
|
|
addxp = False
|
|
elif member.id in conf.ignoredusers:
|
|
addxp = False
|
|
elif voice_state.channel.id in conf.ignoredchannels:
|
|
addxp = False
|
|
elif voice_state.channel.category_id and voice_state.channel.category_id in conf.ignoredchannels:
|
|
addxp = False
|
|
elif (
|
|
conf.ignore_solo and len([i for i in voice_state.channel.members if (not i.bot and i.id != member.id)]) < 1
|
|
):
|
|
addxp = False
|
|
elif self.db.ignore_bots and member.bot:
|
|
addxp = False
|
|
elif conf.allowedchannels:
|
|
if voice_state.channel.id not in conf.allowedchannels:
|
|
if voice_state.channel.category_id:
|
|
if voice_state.channel.category_id not in conf.allowedchannels:
|
|
addxp = False
|
|
else:
|
|
addxp = False
|
|
|
|
return addxp
|