Audit logs, polls, events, voice & more¶
SimCord covers the extended guild surface most real bots reach for. Every feature is driven through real discord.py code paths and is permission-checked exactly as on Discord.
Audit logs¶
Privileged actions the bot performs over the API — bans, kicks, role/member/channel/role
edits, scheduled-event CRUD — are recorded in the guild audit log and announced via
on_audit_log_entry_create. (Omnipotent test setup through builders is not logged, just as
Discord only logs real API actions.) The reason a bot passes is captured too:
guild = env.bot.get_guild(env.guild.id)
await guild.get_member(target.id).ban(reason="spam")
entries = [e async for e in guild.audit_logs(action=discord.AuditLogAction.ban)]
assert entries[0].target.id == target.id
assert entries[0].reason == "spam"
guild.audit_logs() supports user=, action=, before/after and limit filtering.
For assertions you can also read env.guild.audit_log() directly.
Polls¶
A bot sends a poll like any message; a user votes with actor.vote:
poll = discord.Poll(question="Lunch?", duration=datetime.timedelta(hours=1))
poll.add_answer(text="Pizza"); poll.add_answer(text="Sushi")
message = await env.bot.get_channel(channel.id).send(poll=poll)
await alice.vote(message, answer=1) # fires MESSAGE_POLL_VOTE_ADD
await alice.remove_vote(message, answer=1)
Polls finalize when their deadline passes — either explicitly (await message.end_poll()) or
by fast-forwarding the virtual clock with env.advance_time(...).
Scheduled events¶
event = await guild.create_scheduled_event(
name="Community Meetup", start_time=start, end_time=end,
entity_type=discord.EntityType.external, location="Online",
)
await alice.subscribe_event(event.id) # fires GUILD_SCHEDULED_EVENT_USER_ADD
Voice/stage events take a channel=; external events require location and end_time.
Voice state (state only — never audio)¶
SimCord models who is connected where, plus mute/deaf — it never touches audio.
voice = env.guild.create_voice_channel("General Voice")
await alice.join_voice(voice, self_mute=True) # fires VOICE_STATE_UPDATE
await guild.get_member(alice.id).edit(mute=True) # server mute
await guild.get_member(alice.id).move_to(other) # move (audit-logged)
await alice.leave_voice()
Invites, emojis & stickers¶
invite = await env.bot.get_channel(channel.id).create_invite(max_uses=5)
emoji = await guild.create_custom_emoji(name="party", image=png_bytes)
sticker = env.guild.create_sticker("wave", tags="wave") # builder setup
Create/list/fetch/delete all work and fire the matching gateway events
(INVITE_CREATE/INVITE_DELETE, GUILD_EMOJIS_UPDATE, GUILD_STICKERS_UPDATE).
Auto-moderation¶
Keyword rules are evaluated on message send. A block_message action drops the message and
fires AUTO_MODERATION_ACTION_EXECUTION:
await guild.create_automod_rule(
name="No badwords",
event_type=discord.AutoModRuleEventType.message_send,
trigger=discord.AutoModTrigger(
type=discord.AutoModRuleTriggerType.keyword, keyword_filter=["badword"]
),
actions=[discord.AutoModRuleAction(custom_message="blocked")],
enabled=True,
)
await alice.send(channel, "contains a badword") # blocked — appears nowhere
assert channel.history() == []
Keywords follow Discord's wildcard rules: a bare badword matches only as a whole word,
while *badword* matches it as a substring anywhere (and badword* / *badword match as
a prefix / suffix). Mention-spam rules (AutoModRuleTriggerType.mention_spam with a
mention_limit) block a message whose user/role mention count exceeds the limit. Exempt
roles and channels are honored; rules in a guild without auto-mod have zero effect.
Bulk message deletion¶
TextChannel.purge() and delete_messages() remove up to 100 messages with one
MESSAGE_DELETE_BULK event and a bulk-delete audit entry:
channel = env.bot.get_channel(general.id)
await channel.delete_messages(recent_messages) # 2–100 at once
Reaction clearing¶
message = await channel.fetch_message(mid)
await message.clear_reactions() # MESSAGE_REACTION_REMOVE_ALL
await message.clear_reaction("👍") # MESSAGE_REACTION_REMOVE_EMOJI
Guilds & channels at runtime¶
The bot can create and edit guilds and channels mid-test, not just the omnipotent
builder — both go through the same backend, so CHANNEL_CREATE / GUILD_UPDATE reach
the cache:
guild = env.bot.get_guild(env.guild.id)
await guild.create_text_channel("runtime") # POST /guilds/{id}/channels
await guild.edit(name="Renamed") # GUILD_UPDATE, audit-logged
members = [m async for m in guild.fetch_members(limit=None)]
Builders still create the channel kinds your test needs up front:
env.guild.create_voice_channel("General Voice", user_limit=10)
env.guild.create_stage_channel("Town Hall")
env.guild.create_category("Community")
env.guild.create_forum_channel("help")
Forum posts¶
A forum post is a thread with a mandatory starter message:
forum = env.bot.get_channel(help_forum.id)
await forum.edit(available_tags=[discord.ForumTag(name="bug")])
thread, message = await forum.create_thread(name="It crashes", content="stack trace…")
Webhooks¶
Beyond create + execute, webhooks can be fetched, edited and deleted (by id or token), and listed per guild:
hook = await channel.create_webhook(name="Announcer")
await hook.edit(name="News")
await hook.delete()
hooks = await guild.webhooks()
Bot, system & webhook message sources¶
A moderation bot usually branches on who sent a message — delete a human's message, but leave a bot's, an integration's webhook post, or a system notice alone. SimCord lets a test produce each source faithfully.
A bot/application account is just a user created with bot=True; messages it
posts arrive with message.author.bot set (and no webhook_id):
app = env.guild.add_member(env.create_user("ReactionRoles", bot=True))
msg = await app.send(channel, "pick a role")
assert msg.author.bot and msg.webhook_id is None
create_user seeds the rest of the account surface too — system=True for an
official Discord account, global_name for a display name distinct from the
username, discriminator for legacy tags, and public_flags for badges:
verified = env.create_user(
"GoodBot", bot=True, public_flags=discord.PublicUserFlags(verified_bot=True)
)
A webhook is a different source: its messages have author.bot True and
message.webhook_id set. Create one on a channel and post through it — this is
the test-driven counterpart to the bot creating and executing a webhook itself:
hook = env.guild.create_webhook(channel, "CI")
msg = await hook.send(embed=discord.Embed(title="Build passed"), username="CI Bot")
assert msg.webhook_id == hook.id and msg.author.bot
assert msg.author.name == "CI Bot" # per-message username override
So a "delete everything except bots and webhooks" rule can be tested against all three rows at once:
human = await env.guild.add_member(env.create_user("spammer")).send(channel, "spam")
botmsg = await app.send(channel, "ok")
hookmsg = await hook.send("ok")
# human.author.bot is False; botmsg/hookmsg.author.bot is True; only hookmsg has a webhook_id
Guild settings up front¶
create_guild seeds the settings a bot reads off discord.Guild, so you don't
have to drive an edit just to arrange state:
guild = env.create_guild(
"My Server",
owner=env.create_user("Boss"), # owners bypass permission checks
description="a community",
verification_level=discord.VerificationLevel.high,
notifications=discord.NotificationLevel.only_mentions,
content_filter=discord.ContentFilter.all_members,
preferred_locale="en-GB",
afk_timeout=900,
)