Reputation:
I'm using the discord.py library and want to purge inactive members by kicking them from the server. I am aware of the function discord.Guild.prune_members, however it does not provide all the functionality I need as I'd like to send the user a warning a few days before they are removed.
How can I best find when a user was last active so that I can calculate the number of days they have been inactive for? I have been searching the internet for several days and cannot find a solution.
Upvotes: 2
Views: 3215
Reputation: 595
As stated in the discord.py server, you can use a simple cache alongside a database to keep track of this. Let's run through a simple example on how we could do this.
from typing import TYPE_CHECKING, Dict, List
import discord
from discord.ext import commands
if TYPE_CHECKING:
import datetime
import asyncpg
# We set the type checking here to avoid errors, your bot instance
# shouldnt run into this issue and this shouldnt be needed.
class Bot(commands.Bot):
pool: asyncpg.Pool[asyncpg.Record]
else:
Bot = commands.Bot
# We're going to layout our data as so:
# CREATE TABLE prune (member_id BIGINT, guild_id BIGINT, last_login TIMESTAMP WITH TIMEZONE)
class Tracker(commands.Cog):
def __init__(self, bot: Bot) -> None:
self.bot: Bot = bot
self.presence_cache: Dict[int, Dict[int, datetime.datetime]] = {} # mapping of guild : {member_id: last_login}
async def cog_load(self) -> None:
# Let's load the info from our database table into our cache.
# This implements a postgres asyncpg pool
data = await self.bot.pool.fetch('SELECT * FROM prune') # type: ignore
for entry in data:
entry: asyncpg.Record
# Enter in the guild id and the member id
if (guild_id := entry['guild_id']) not in self.presence_cache:
self.presence_cache[guild_id] = {}
self.presence_cache[guild_id][entry['member_id']] = entry['last_login']
async def cog_unload(self) -> None:
# We need to dump back into our database here, so we can save our data.
async with self.bot.pool.acquire() as connection:
async with connection.transaction():
query = 'INSERT INTO prune (member_id, guild_id, last_login) VALUES ($1, $2, $3) ON CONFLICT (member_id, guild_id) DO UPDATE SET last_login = $3'
# Let's format our data so we can use executemany
# The formatted data should be a list of tuples, (member_id, guild_id, last_login)
formatted_data = [(member_id, guild_id, last_login) for guild_id, guild_data in self.presence_cache.items() for member_id, last_login in guild_data.items()]
await connection.executemany(query, formatted_data)
@commands.Cog.listener('on_presence_update')
async def on_presence_update(self, before: discord.Member, after: discord.Member) -> None:
if before.status == after.status: # The two statuses are the same, return
return
if not (before.status is discord.Status.offline and after.status is discord.Status.online): # The member did NOT come onine
return
# This means the member's last login was just now, let's add it to our cache to update
if (guild_id := after.guild.id) not in self.presence_cache:
self.presence_cache[guild_id] = {}
self.presence_cache[guild_id][after.id] = discord.utils.utcnow() # utc now (dpy is timezone aware in 2.0)
@commands.command(
name='get_prune',
brief='Get the members who will be pruned',
description='Get the members who will be pruned',
)
@commands.guild_only()
async def get_prune(self, ctx: commands.Context[Bot], days: int) -> None:
assert ctx.guild is not None
# Let's find the members who would be pruned
query = 'SELECT member_id FROM prune WHERE guild_id = $1 AND last_login < $2'
data = await self.bot.pool.fetch(query, ctx.guild.id, (discord.utils.utcnow() - datetime.timedelta(days=days)).replace(tzinfo=None))
# Now let's format an embed
embed = discord.Embed(
title=f'{len(data)} members will be pruned',
)
members: List[discord.Member] = []
for entry in data:
member = ctx.guild.get_member(entry['member_id']) or (await ctx.guild.fetch_member(entry['member_id']))
members.append(member)
embed.description = ', '.join(member.mention for member in members)
await ctx.send(embed=embed, allowed_mentions=discord.AllowedMentions.none())
In this example, we used the async cog_load
and cog_unload
functions to keep a cache up to date. To append to our cache we used on_presence_update
to check when the member came online. I've also implemented a simple command to get the members who would be pruned after X days. It can be invoked as so:
[pr]get_prune 20
If you have any questions feel free to reach out on this thread :)
Upvotes: 2