Cleaning up migrated components.
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
from . import data
|
||||
from . import admin
|
||||
|
||||
from . import watchdog
|
||||
@@ -1,128 +0,0 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from settings import GuildSettings, GuildSetting
|
||||
from wards import guild_admin
|
||||
|
||||
import settings
|
||||
|
||||
from .data import video_channels
|
||||
|
||||
|
||||
@GuildSettings.attach_setting
|
||||
class video_channels(settings.ChannelList, settings.ListData, settings.Setting):
|
||||
category = "Video Channels"
|
||||
|
||||
attr_name = 'video_channels'
|
||||
|
||||
_table_interface = video_channels
|
||||
_id_column = 'guildid'
|
||||
_data_column = 'channelid'
|
||||
_setting = settings.VoiceChannel
|
||||
|
||||
write_ward = guild_admin
|
||||
display_name = "video_channels"
|
||||
desc = "Channels where members are required to enable their video."
|
||||
|
||||
_force_unique = True
|
||||
|
||||
long_desc = (
|
||||
"Members must keep their video enabled in these channels.\n"
|
||||
"If they do not keep their video enabled, they will be asked to enable it in their DMS after `15` seconds, "
|
||||
"and then kicked from the channel with another warning after the `video_grace_period` duration has passed.\n"
|
||||
"After the first offence, if the `video_studyban` is enabled and the `studyban_role` is set, "
|
||||
"they will also be automatically studybanned."
|
||||
)
|
||||
|
||||
# Flat cache, no need to expire objects
|
||||
_cache = {}
|
||||
|
||||
@property
|
||||
def success_response(self):
|
||||
if self.value:
|
||||
return "Members must enable their video in the following channels:\n{}".format(self.formatted)
|
||||
else:
|
||||
return "There are no video-required channels set up."
|
||||
|
||||
@classmethod
|
||||
async def launch_task(cls, client):
|
||||
"""
|
||||
Launch initialisation step for the `video_channels` setting.
|
||||
|
||||
Pre-fill cache for the guilds with currently active voice channels.
|
||||
"""
|
||||
active_guildids = [
|
||||
guild.id
|
||||
for guild in client.guilds
|
||||
if any(channel.members for channel in guild.voice_channels)
|
||||
]
|
||||
if active_guildids:
|
||||
cache = {guildid: [] for guildid in active_guildids}
|
||||
rows = cls._table_interface.select_where(
|
||||
guildid=active_guildids
|
||||
)
|
||||
for row in rows:
|
||||
cache[row['guildid']].append(row['channelid'])
|
||||
cls._cache.update(cache)
|
||||
|
||||
|
||||
@GuildSettings.attach_setting
|
||||
class video_studyban(settings.Boolean, GuildSetting):
|
||||
category = "Video Channels"
|
||||
|
||||
attr_name = 'video_studyban'
|
||||
_data_column = 'video_studyban'
|
||||
|
||||
display_name = "video_studyban"
|
||||
desc = "Whether to studyban members if they don't enable their video."
|
||||
|
||||
long_desc = (
|
||||
"If enabled, members who do not enable their video in the configured `video_channels` will be "
|
||||
"study-banned after a single warning.\n"
|
||||
"When disabled, members will only be warned and removed from the channel."
|
||||
)
|
||||
|
||||
_default = True
|
||||
_outputs = {True: "Enabled", False: "Disabled"}
|
||||
|
||||
@property
|
||||
def success_response(self):
|
||||
if self.value:
|
||||
return "Members will now be study-banned if they don't enable their video in the configured video channels."
|
||||
else:
|
||||
return "Members will not be study-banned if they don't enable their video in video channels."
|
||||
|
||||
|
||||
@GuildSettings.attach_setting
|
||||
class video_grace_period(settings.Duration, GuildSetting):
|
||||
category = "Video Channels"
|
||||
|
||||
attr_name = 'video_grace_period'
|
||||
_data_column = 'video_grace_period'
|
||||
|
||||
display_name = "video_grace_period"
|
||||
desc = "How long to wait before kicking/studybanning members who don't enable their video."
|
||||
|
||||
long_desc = (
|
||||
"The period after a member has been asked to enable their video in a video-only channel "
|
||||
"before they will be kicked from the channel, and warned or studybanned (if enabled)."
|
||||
)
|
||||
|
||||
_default = 90
|
||||
_default_multiplier = 1
|
||||
|
||||
@classmethod
|
||||
def _format_data(cls, id: int, data, **kwargs):
|
||||
"""
|
||||
Return the string version of the data.
|
||||
"""
|
||||
if data is None:
|
||||
return None
|
||||
else:
|
||||
return "`{} seconds`".format(data)
|
||||
|
||||
@property
|
||||
def success_response(self):
|
||||
return (
|
||||
"Members who do not enable their video will "
|
||||
"be disconnected after {}.".format(self.formatted)
|
||||
)
|
||||
@@ -1,4 +0,0 @@
|
||||
from data import Table, RowTable
|
||||
|
||||
|
||||
video_channels = Table('video_channels')
|
||||
@@ -1,381 +0,0 @@
|
||||
"""
|
||||
Implements a tracker to warn, kick, and studyban members in video channels without video enabled.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import datetime
|
||||
import discord
|
||||
|
||||
from meta import client
|
||||
from core import Lion
|
||||
from utils.lib import strfdelta
|
||||
from settings import GuildSettings
|
||||
|
||||
from ..tickets import StudyBanTicket, WarnTicket
|
||||
from ..module import module
|
||||
|
||||
|
||||
_tasks = {} # (guildid, userid) -> Task
|
||||
|
||||
|
||||
async def _send_alert(member, embed, alert_channel):
|
||||
"""
|
||||
Sends an embed to the member.
|
||||
If we can't reach the member, send it via alert_channel, if it exists.
|
||||
Returns the message, if it was sent, otherwise None.
|
||||
"""
|
||||
try:
|
||||
return await member.send(embed=embed)
|
||||
except discord.Forbidden:
|
||||
if alert_channel:
|
||||
try:
|
||||
return await alert_channel.send(
|
||||
content=(
|
||||
"{} (Please enable your DMs with me to get alerts privately!)"
|
||||
).format(member.mention),
|
||||
embed=embed
|
||||
)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
|
||||
|
||||
async def _join_video_channel(member, channel):
|
||||
# Sanity checks
|
||||
if not member.voice and member.voice.channel:
|
||||
# Not in a voice channel
|
||||
return
|
||||
if member.voice.self_video:
|
||||
# Already have video on
|
||||
return
|
||||
|
||||
# First wait for 15 seconds for them to turn their video on
|
||||
try:
|
||||
await asyncio.sleep(15)
|
||||
except asyncio.CancelledError:
|
||||
# They left the channel or turned their video on
|
||||
return
|
||||
|
||||
# Fetch the relevant settings and build embeds
|
||||
guild_settings = GuildSettings(member.guild.id)
|
||||
grace_period = guild_settings.video_grace_period.value
|
||||
studyban = guild_settings.video_studyban.value
|
||||
studyban_role = guild_settings.studyban_role.value
|
||||
alert_channel = guild_settings.alert_channel.value
|
||||
|
||||
lion = Lion.fetch(member.guild.id, member.id)
|
||||
previously_warned = lion.data.video_warned
|
||||
|
||||
request_embed = discord.Embed(
|
||||
title="Please enable your video!",
|
||||
description=(
|
||||
"**You have joined the video-only channel {}!**\n"
|
||||
"Please **enable your video** or **leave the channel** in the next `{}` seconds, "
|
||||
"otherwise you will be **disconnected** and "
|
||||
"potentially **banned** from using this server's study facilities."
|
||||
).format(
|
||||
channel.mention,
|
||||
grace_period
|
||||
),
|
||||
colour=discord.Colour.orange(),
|
||||
timestamp=datetime.datetime.utcnow()
|
||||
).set_footer(
|
||||
text=member.guild.name,
|
||||
icon_url=member.guild.icon_url
|
||||
)
|
||||
|
||||
thanks_embed = discord.Embed(
|
||||
title="Thanks for enabling your video! Best of luck with your study.",
|
||||
colour=discord.Colour.green(),
|
||||
timestamp=datetime.datetime.utcnow()
|
||||
).set_footer(
|
||||
text=member.guild.name,
|
||||
icon_url=member.guild.icon_url
|
||||
)
|
||||
|
||||
bye_embed = discord.Embed(
|
||||
title="Thanks for leaving the channel promptly!",
|
||||
colour=discord.Colour.green(),
|
||||
timestamp=datetime.datetime.utcnow()
|
||||
).set_footer(
|
||||
text=member.guild.name,
|
||||
icon_url=member.guild.icon_url
|
||||
)
|
||||
|
||||
# Send the notification message and wait for the grace period
|
||||
out_msg = None
|
||||
alert_task = asyncio.create_task(_send_alert(
|
||||
member,
|
||||
request_embed,
|
||||
alert_channel
|
||||
))
|
||||
try:
|
||||
out_msg = await asyncio.shield(alert_task)
|
||||
await asyncio.sleep(grace_period)
|
||||
except asyncio.CancelledError:
|
||||
# They left the channel or turned their video on
|
||||
|
||||
# Finish the message task if it wasn't complete
|
||||
if not alert_task.done():
|
||||
out_msg = await alert_task
|
||||
|
||||
# Update the notification message
|
||||
# The out_msg may be None here, if we have no way of reaching the member
|
||||
if out_msg is not None:
|
||||
try:
|
||||
if not member.voice or not (member.voice.channel == channel):
|
||||
await out_msg.edit(embed=bye_embed)
|
||||
elif member.voice.self_video:
|
||||
await out_msg.edit(embed=thanks_embed)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
return
|
||||
|
||||
# Disconnect, notify, warn, and potentially study ban
|
||||
# Don't allow this to be cancelled any more
|
||||
_tasks.pop((member.guild.id, member.id), None)
|
||||
|
||||
# First disconnect
|
||||
client.log(
|
||||
("Disconnecting member {} (uid: {}) in guild {} (gid: {}) from video channel {} (cid:{}) "
|
||||
"for not enabling their video.").format(
|
||||
member.name,
|
||||
member.id,
|
||||
member.guild.name,
|
||||
member.guild.id,
|
||||
channel.name,
|
||||
channel.id
|
||||
),
|
||||
context="VIDEO_WATCHDOG"
|
||||
)
|
||||
try:
|
||||
await member.edit(
|
||||
voice_channel=None,
|
||||
reason="Member in video-only channel did not enable video."
|
||||
)
|
||||
except discord.HTTPException:
|
||||
# TODO: Add it to the moderation ticket
|
||||
# Error log?
|
||||
...
|
||||
|
||||
# Then warn or study ban, with appropriate notification
|
||||
only_warn = not previously_warned or not studyban or not studyban_role
|
||||
|
||||
if only_warn:
|
||||
# Give them an official warning
|
||||
embed = discord.Embed(
|
||||
title="You have received a warning!",
|
||||
description=(
|
||||
"You must enable your camera in camera-only rooms."
|
||||
),
|
||||
colour=discord.Colour.red(),
|
||||
timestamp=datetime.datetime.utcnow()
|
||||
)
|
||||
embed.add_field(
|
||||
name="Info",
|
||||
value=(
|
||||
"*Warnings appear in your moderation history. "
|
||||
"Failure to comply, or repeated warnings, "
|
||||
"may result in muting, studybanning, or server banning.*"
|
||||
)
|
||||
)
|
||||
embed.set_footer(
|
||||
icon_url=member.guild.icon_url,
|
||||
text=member.guild.name
|
||||
)
|
||||
await _send_alert(member, embed, alert_channel)
|
||||
await WarnTicket.create(
|
||||
member.guild.id,
|
||||
member.id,
|
||||
client.user.id,
|
||||
"Failed to enable their video in time in the video channel {}.".format(channel.mention),
|
||||
auto=True
|
||||
)
|
||||
# TODO: Warning ticket and related embed.
|
||||
lion.data.video_warned = True
|
||||
else:
|
||||
# Apply an automatic studyban
|
||||
ticket = await StudyBanTicket.autoban(
|
||||
member.guild,
|
||||
member,
|
||||
"Failed to enable their video in time in the video channel {}.".format(channel.mention)
|
||||
)
|
||||
if ticket:
|
||||
tip = "TIP: When joining a video only study room, always be ready to enable your video immediately!"
|
||||
embed = discord.Embed(
|
||||
title="You have been studybanned!",
|
||||
description=(
|
||||
"You have been banned from studying in **{}**.\n"
|
||||
"Study features, including access to the server **study channels**, "
|
||||
"will ***not be available to you until this ban is lifted.***".format(
|
||||
member.guild.name,
|
||||
)
|
||||
),
|
||||
colour=discord.Colour.red(),
|
||||
timestamp=datetime.datetime.utcnow()
|
||||
)
|
||||
embed.add_field(
|
||||
name="Reason",
|
||||
value="Failure to enable your video in time in a video-only channel.\n\n*{}*".format(tip)
|
||||
)
|
||||
if ticket.data.duration:
|
||||
embed.add_field(
|
||||
name="Duration",
|
||||
value="`{}` (Expires <t:{:.0f}>)".format(
|
||||
strfdelta(datetime.timedelta(seconds=ticket.data.duration)),
|
||||
ticket.data.expiry.timestamp()
|
||||
),
|
||||
inline=False
|
||||
)
|
||||
embed.set_footer(
|
||||
text=member.guild.name,
|
||||
icon_url=member.guild.icon_url
|
||||
)
|
||||
await _send_alert(member, embed, alert_channel)
|
||||
else:
|
||||
# This should be impossible
|
||||
# TODO: Cautionary error logging
|
||||
pass
|
||||
|
||||
|
||||
@client.add_after_event("voice_state_update")
|
||||
async def video_watchdog(client, member, before, after):
|
||||
if member.bot:
|
||||
return
|
||||
|
||||
task_key = (member.guild.id, member.id)
|
||||
|
||||
if after.channel != before.channel:
|
||||
# Channel change, cancel any running tasks for the member
|
||||
task = _tasks.pop(task_key, None)
|
||||
if task and not task.done():
|
||||
task.cancel()
|
||||
|
||||
# Check whether they are joining a video channel, run join logic if so
|
||||
if after.channel and not after.self_video:
|
||||
video_channel_ids = GuildSettings(member.guild.id).video_channels.data
|
||||
if after.channel.id in video_channel_ids:
|
||||
client.log(
|
||||
("Launching join task for member {} (uid: {}) "
|
||||
"in guild {} (gid: {}) and video channel {} (cid:{}).").format(
|
||||
member.name,
|
||||
member.id,
|
||||
member.guild.name,
|
||||
member.guild.id,
|
||||
after.channel.name,
|
||||
after.channel.id
|
||||
),
|
||||
context="VIDEO_WATCHDOG",
|
||||
level=logging.DEBUG
|
||||
)
|
||||
_tasks[task_key] = asyncio.create_task(_join_video_channel(member, after.channel))
|
||||
else:
|
||||
video_channel_ids = GuildSettings(member.guild.id).video_channels.data
|
||||
if after.channel and after.channel.id in video_channel_ids:
|
||||
channel = after.channel
|
||||
if after.self_video:
|
||||
# If they have their video on, cancel any running tasks
|
||||
task = _tasks.pop(task_key, None)
|
||||
if task and not task.done():
|
||||
task.cancel()
|
||||
else:
|
||||
# They have their video off
|
||||
# Don't do anything if there are running tasks, the tasks will handle it
|
||||
task = _tasks.get(task_key, None)
|
||||
if task and not task.done():
|
||||
return
|
||||
|
||||
# Otherwise, give them 10 seconds
|
||||
_tasks[task_key] = task = asyncio.create_task(asyncio.sleep(10))
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
# Task was cancelled, they left the channel or turned their video on
|
||||
return
|
||||
|
||||
# Then kick them out, alert them, and event log it
|
||||
client.log(
|
||||
("Disconnecting member {} (uid: {}) in guild {} (gid: {}) from video channel {} (cid:{}) "
|
||||
"for disabling their video.").format(
|
||||
member.name,
|
||||
member.id,
|
||||
member.guild.name,
|
||||
member.guild.id,
|
||||
channel.name,
|
||||
channel.id
|
||||
),
|
||||
context="VIDEO_WATCHDOG"
|
||||
)
|
||||
try:
|
||||
await member.edit(
|
||||
voice_channel=None,
|
||||
reason="Removing non-video member from video-only channel."
|
||||
)
|
||||
await _send_alert(
|
||||
member,
|
||||
discord.Embed(
|
||||
title="You have been kicked from the video channel.",
|
||||
description=(
|
||||
"You were disconnected from the video-only channel {} for disabling your video.\n"
|
||||
"Please keep your video on at all times, and leave the channel if you need "
|
||||
"to make adjustments!"
|
||||
).format(
|
||||
channel.mention,
|
||||
),
|
||||
colour=discord.Colour.red(),
|
||||
timestamp=datetime.datetime.utcnow()
|
||||
).set_footer(
|
||||
text=member.guild.name,
|
||||
icon_url=member.guild.icon_url
|
||||
),
|
||||
GuildSettings(member.guild.id).alert_channel.value
|
||||
)
|
||||
except discord.Forbidden:
|
||||
GuildSettings(member.guild.id).event_log.log(
|
||||
"I attempted to disconnect {} from the video-only channel {} "
|
||||
"because they disabled their video, but I didn't have the required permissions!\n".format(
|
||||
member.mention,
|
||||
channel.mention
|
||||
)
|
||||
)
|
||||
else:
|
||||
GuildSettings(member.guild.id).event_log.log(
|
||||
"{} was disconnected from the video-only channel {} "
|
||||
"because they disabled their video.".format(
|
||||
member.mention,
|
||||
channel.mention
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@module.launch_task
|
||||
async def load_video_channels(client):
|
||||
"""
|
||||
Process existing video channel members.
|
||||
Pre-fills the video channel cache by running the setting launch task.
|
||||
|
||||
Treats members without video on as having just joined.
|
||||
"""
|
||||
# Run the video channel initialisation to populate the setting cache
|
||||
await GuildSettings.settings.video_channels.launch_task(client)
|
||||
|
||||
# Launch join tasks for all members in video channels without video enabled
|
||||
video_channels = (
|
||||
channel
|
||||
for guild in client.guilds
|
||||
for channel in guild.voice_channels
|
||||
if channel.members and channel.id in GuildSettings.settings.video_channels.get(guild.id).data
|
||||
)
|
||||
to_task = [
|
||||
(member, channel)
|
||||
for channel in video_channels
|
||||
for member in channel.members
|
||||
if not member.voice.self_video
|
||||
]
|
||||
for member, channel in to_task:
|
||||
_tasks[(member.guild.id, member.id)] = asyncio.create_task(_join_video_channel(member, channel))
|
||||
|
||||
if to_task:
|
||||
client.log(
|
||||
"Launched {} join tasks for members who need to enable their video.".format(len(to_task)),
|
||||
context="VIDEO_CHANNEL_LAUNCH"
|
||||
)
|
||||
Reference in New Issue
Block a user