Welcome and goodbye messages for a Discord bot, in discord.py and discord.js

A message when someone joins, another when they leave. It is one of the first things people build, and the two common snags are an intent that has to be switched on in two places and goodbyes that never arrive. Here is the whole bot for each library.

6 min readIntermediateUpdated Examples tested with discord.py 2.7.1 and discord.js 14.27

Each example below was run against a test copy of Discord, with people joining and leaving (some of them unknown to the bot) to check that every greeting and goodbye goes out.

Switch on Server Members

Joins and leaves only reach a bot that asks for the Server Members intent. It is privileged, so it has to be switched on twice: in the code, as below, and in the Developer Portal on your application's Bot page under Privileged Gateway Intents. With only the code, the bot does not connect at all (PrivilegedIntentsRequired in discord.py, Used disallowed intents in discord.js). With only the portal, it connects and the events never fire. Once the bot is in 100 servers, Discord asks you to apply for it.

The bot also needs View Channel, Send Messages and Embed Links in the channel it greets in.

discord.py

Python
import os

import discord
from discord.ext import commands

intents = discord.Intents.default()
intents.members = True  # the Server Members intent: switch it on in the Developer Portal too

bot = commands.Bot(command_prefix="!", intents=intents)

@bot.event
async def on_member_join(member: discord.Member):
    channel = member.guild.system_channel
    if channel is None:
        return
    embed = discord.Embed(
        title=f"Welcome to {member.guild.name}!",
        description=f"Hi {member.mention}, you are member number {member.guild.member_count}.",
        colour=discord.Colour.green(),
    )
    embed.set_thumbnail(url=member.display_avatar.url)
    await channel.send(embed=embed)

@bot.event
async def on_raw_member_remove(payload: discord.RawMemberRemoveEvent):
    guild = bot.get_guild(payload.guild_id)
    if guild and guild.system_channel:
        await guild.system_channel.send(f"{payload.user.display_name} has left the server.")

bot.run(os.environ["BOT_TOKEN"])
  • Which channel. system_channel is the server's system messages channel, where Discord posts its own join messages, so the greeting lands where people expect it. To use another one, look it up by ID instead: member.guild.get_channel(123456789012345678).
  • Why the raw event for goodbyes. on_member_remove only fires for members the bot has in memory. on_raw_member_remove fires for everyone who leaves and carries their user, which is all a goodbye needs.
  • The mention. member.mention pings the newcomer, as Discord's own welcome does. The goodbye uses the name instead: there is nobody left to ping.

discord.js

JavaScript
const { Client, Colors, EmbedBuilder, Events, GatewayIntentBits, Partials } = require("discord.js");

const client = new Client({
  // GuildMembers is the Server Members intent: switch it on in the Developer Portal too.
  intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
  partials: [Partials.GuildMember], // goodbyes for people the bot never had in memory
});

client.on(Events.GuildMemberAdd, async (member) => {
  const channel = member.guild.systemChannel;
  if (!channel) return;
  const embed = new EmbedBuilder()
    .setTitle(`Welcome to ${member.guild.name}!`)
    .setDescription(`Hi ${member}, you are member number ${member.guild.memberCount}.`)
    .setColor(Colors.Green)
    .setThumbnail(member.displayAvatarURL());
  await channel.send({ embeds: [embed] });
});

client.on(Events.GuildMemberRemove, async (member) => {
  const channel = member.guild.systemChannel;
  if (channel) await channel.send(`${member.user.displayName} has left the server.`);
});

client.login(process.env.BOT_TOKEN);
  • Why Partials.GuildMember. discord.js only fires GuildMemberRemove for members it has in memory, and a bot rarely has everyone: in a bigger server, the people who were there before it started are mostly not. Without the partial, their goodbyes are skipped with no error. With it, the event comes with what Discord sends, which includes the user.
  • ${member} in a string is a mention, and member.guild.memberCount already counts the newcomer.

A welcome DM as well

Some people only accept DMs from friends, and Discord then refuses the message. That is normal, not a bug, so catch it rather than letting it fill the logs. In discord.py, inside on_member_join:

Python
    try:
        await member.send(f"Welcome to {member.guild.name}! The rules are pinned in the first channel.")
    except discord.Forbidden:
        pass  # they do not take DMs from server members

In discord.js, inside the GuildMemberAdd handler, with RESTJSONErrorCodes added to the require:

JavaScript
  await member.send(`Welcome to ${member.guild.name}! The rules are pinned in the first channel.`).catch((err) => {
    if (err.code !== RESTJSONErrorCodes.CannotSendMessagesToThisUser) console.error(err); // 50007: DMs closed
  });

Common problems

  • Nothing happens when someone joins: Server Members is on in the portal but not in the code, or the other way round, or the server has no system messages channel (system_channel is None).
  • Welcomes work, goodbyes do not: in discord.js, Partials.GuildMember is missing. In discord.py, the code uses on_member_remove instead of the raw event.
  • 50013 Missing Permissions when posting: the bot cannot send messages or embeds in that channel. Check the channel's own permission overrides, not only the bot's role.
  • 50007 Cannot send messages to this user: their DMs are closed. Catch it, as above.
  • The member number is off: it is the server's member count at that moment, bots included.

Press / or Ctrl K on any page to search.