from collections import defaultdict
from datetime import datetime, timedelta, timezone
from fastcore.meta import *
from fastcore.utils import *
from fasthtml.common import *
import asyncio,httpx,json,os,recordslite API
Discord has three main APIs: - REST API - for actions (send message, get channels, fetch history). Request/response style. - Gateway API - for real-time events via WebSocket (new messages, reactions, user joins). - Voice API - for real-time streaming of audio via UDP
We start with the REST API. To use it, you need a bot token from the Discord Developer Portal: 1. Create an application 2. Go to “Bot” section and create a bot 3. Copy the token (keep it secret!) 4. Under “OAuth2 → URL Generator”, select bot scope and choose permissions (e.g., Send Messages, Read Message History) 5. Use the generated URL to invite the bot to your server
The DiscordClient wraps httpx.Client with Discord’s base URL (https://discord.com/api/v10) and auth headers pre-configured.
class DiscordClient:
def __init__(self, token=None, user_token=None, name='cordslite', ver='0.1'):
self.token = token or os.environ.get('DISCORD_BOT_TOKEN')
self.user_token = user_token or os.environ.get('DISCORD_USER_TOKEN')
self.base_url = 'https://discord.com/api/v10'
auth = self.user_token if not self.token else f'Bot {self.token}'
self.headers = {'Authorization': auth, 'User-Agent': f'DiscordBot ({name}, {ver})'}
self.cli = httpx.AsyncClient(base_url=self.base_url, headers=self.headers)The token defaults to the DISCORD_BOT_TOKEN environment variable if none is provided.
dc = DiscordClient()REST endpoints
Every REST call goes through _req, which handles two concerns automatically: - Rate limiting — if Discord returns 429 Too Many Requests, we wait the Retry-After duration and retry. - Error handling — non-2xx responses are raised as DiscordError with Discord’s error code, message, and HTTP status, so callers don’t need to check responses manually.
class DiscordError(Exception):
def __init__(self, code, msg, status): self.code,self.msg,self.status = code,msg,status
def __str__(self): return f"DiscordError({self.status}): [{self.code}] {self.msg}"
@patch
async def _req(self:DiscordClient, method, path, data=None, files=None, use_user=False, **kw):
kw = filter_values(kw, negate(is_(None)))
headers = None
if use_user:
if not self.user_token: raise ValueError("User token required for this operation")
headers = {'Authorization': self.user_token}
if method=='GET': params,json = kw,None
else: params,json = None,kw
r = await self.cli.request(method, path, data=data, files=files, params=params, json=json, headers=headers)
if r.status_code == 429:
await asyncio.sleep(float(r.headers.get('Retry-After', 1))+0.1)
return await self._req(method, path, use_user=use_user, **kw)
if r.status_code >= 400:
try: d = r.json()
except Exception: d = {}
raise DiscordError(d.get('code', 0), d.get('message', r.text[:200]), r.status_code)
if r.status_code == 204: return 'done'
try: return r.json()
except Exception as e: raise Exception(f"Failed to parse response: {r.text}; {e}")Discord’s REST endpoints return JSON with many fields. Rather than defining properties for every field, we use a flexible base class pattern:
DiscordObject provides __getitem__ and __getattr__ so you can access data as obj.name or obj['name']. The __dir__ method enables autocomplete in notebooks and solveit dialogs—just type obj. and see all available fields!
This means when Discord adds new fields to their API, our code automatically supports them without changes.
class DiscordObject(AttrDict):
def __init__(self, data, client):
super().__init__(data)
self._client = getattr(client, '_client', client)
self._parent = client
async def __call__(self, meth, path, **kw): return await self._client._req(meth, path, **kw)
def obj(self, nm, d): return globals()[nm](d, self)
def coll(self, nm, ds): return globals()[nm+'s'](self.obj(nm,d) for d in ds)Discord’s hierarchy is Guild → Channels → Messages. A Guild (server) contains channels, and channels contain messages. Each has its own REST endpoints: - GET /guilds/{id} - fetch guild info - GET /guilds/{id}/channels - list channels - GET /channels/{id}/messages - fetch messages - POST /channels/{id}/messages - send a message
We build wrapper classes for each, inheriting from DiscordObject and just adding a nice __repr__.
class Guild(DiscordObject):
def __repr__(self): return f"Guild(id={self.id}, name={self.name!r})"We use @patch from fastcore to add methods incrementally—great for interactive development where you want to test each piece as you build it. This guild method fetches guild data and wraps it in our Guild class.
@patch
async def guild(self:DiscordClient, guild_id):
return Guild(await self._req('GET', f'/guilds/{guild_id}'), self)gid = '1493461895615873044'
gld = await dc.guild(gid); gldGuild(id=1327046393453613076, name="natedog's server")Channels have a type field: 0=text, 2=voice, 4=category. The Channels class inherits from list and adds _repr_html_ for nice table display in notebooks/solveit. This pattern—a wrapper class plus a collection class with HTML repr—makes exploring the API much more pleasant.
def html_table(items, hdrs, fn):
if not items: return ""
rows = (Tr(map(Td, fn(o))) for o in items)
return to_xml(Table(Thead(Tr(map(Th, hdrs))), Tbody(rows), cls="prose"))
class Channel(DiscordObject):
@property
async def guild(self):
if not hasattr(self, '_gld'): self._gld = await self._client.guild(self.guild_id)
return self._gld
def __repr__(self): return f"Channel(id={self.id}, name={self.name!r}, type={self.type})"
class Channels(list):
def _repr_html_(self): return html_table(self, ("ID", "Name", "Type"), lambda c: (c.id, c.name, c.type))
@patch
async def channels(self:Guild, limit=None):
data = await self('GET', f'/guilds/{self.id}/channels')
if limit: data = data[:limit]
return self.coll('Channel', data)chs = await gld.channels()
chs| ID | Name | Type |
|---|---|---|
| 1327046393453613077 | Text Channels | 4 |
| 1327046393453613078 | Voice Channels | 4 |
| 1327046393453613079 | general | 0 |
| 1327046393453613080 | General | 2 |
| 1327954661960978512 | private | 0 |
| 1475600987627458812 | forwarded | 0 |
| 1501265913222266961 | test-webhooks | 0 |
| 1506656999293849753 | test | 0 |
| 1506657701688508456 | test-vch | 2 |
@patch
async def channel(self:DiscordClient, channel_id):
return Channel(await self._req('GET', f'/channels/{channel_id}'), self)Sometimes you don’t know the guild ID up front. dc.guilds() lists every guild the bot has been invited to, using Discord’s /users/@me/guilds endpoint (which returns partial guild objects—id, name, owner, permissions). Handy for building navigation UIs without hardcoding IDs.
class Guilds(list):
def _repr_html_(self): return html_table(self, ("ID", "Name"), lambda g: (g.id, g.name))
@patch
async def guilds(self:DiscordClient, limit=None):
"List the guilds the bot is a member of"
data = await self._req('GET', '/users/@me/guilds', limit=limit)
return Guilds(Guild(d, self) for d in data)glds = await dc.guilds()
assert any(g.id == gid for g in glds)
glds| ID | Name |
|---|---|
| 1200111522916094103 | Answer.ai |
| 1311580085442318366 | erikgaas's server |
| 1327046393453613076 | natedog's server |
| 1344332715226300458 | Kerem's server |
chid = '1493461896139903028'
ch = await dc.channel(chid)
chMessages are the core of most bot functionality. Note that Discord returns messages in reverse chronological order (newest first), so we reverse() the data to get chronological order. The table shows a preview of content, author, and timestamp.
Note: To read message content, your bot needs the MESSAGE_CONTENT privileged intent enabled in the Developer Portal! Without it, the content field will be empty for messages not sent by your bot or mentioning it.
class Message(DiscordObject):
def __init__(self, data, dobj):
super().__init__(data, dobj)
self.raw_content = self.content
mentions = {m['id']: m['username'] for m in data.get('mentions', [])}
def repl(m): return f"@{mentions.get(m.group(1), 'unknown')}"
self['content'] = re.sub(r'<@!?(\d+)>', repl, self.content)
def __repr__(self):
author = self.author['username']
preview = self.content[:30] + '…' if len(self.content) > 30 else self.content
return f"Message(id={self.id}, author={author!r}, content={preview!r})"
@property
async def channel(self):
if not hasattr(self, '_ch'): self._ch = Channel(await self('GET', f'/channels/{self.channel_id}'), self._client)
return self._ch
class Messages(list):
def _repr_html_(self):
return html_table(self, ("ID", "Author", "Content", "Date"),
lambda m: (m.id, m.author['username'], m.content[:50], m.timestamp[:10]))
@patch
async def messages(self:Channel, limit=50, before=None, after=None, around=None):
"Fetch channel messages. `before`, `after`, and `around` are mutually exclusive message IDs."
if sum(x is not None for x in [before, after, around]) > 1:
raise ValueError("Pass only one of `before`, `after`, or `around`")
def mid(x): return x.id if isinstance(x, Message) else x
data = await self('GET', f'/channels/{self.id}/messages',
limit=limit, before=mid(before), after=mid(after), around=mid(around))
return self.coll('Message', reversed(data))msgs = await ch.messages(5)
msgs| ID | Author | Content | Date |
|---|---|---|---|
| 1507463593162047658 | DBuddy | Test our event listener! Otters are awesome 🦦 | 2026-05-22 |
| 1507463611893809202 | DBuddy | Houston, do we have a problem? | 2026-05-22 |
| 1507463630873039008 | DBuddy | Did we re-identify? | 2026-05-22 |
| 1523685329700393064 | DBuddy | cordslite voice-robustness validation: gateway res | 2026-07-06 |
| 1523696905593557075 | DBuddy | Back from the dead? (listener auto-resume) | 2026-07-06 |
@patch
async def before(self:Message, limit=5):
"Fetch messages before this message in the same channel/thread."
return await (await self.channel).messages(before=self, limit=limit)
@patch
async def after(self:Message, limit=5):
"Fetch messages after this message in the same channel/thread."
return await (await self.channel).messages(after=self, limit=limit)
@patch
async def around(self:Message, limit=11):
"Fetch messages around this message in the same channel/thread."
return await (await self.channel).messages(around=self, limit=limit)DISCORD_WEB = 'https://discord.com/channels'
@patch(as_prop=True)
def url(self:Guild): return f'{DISCORD_WEB}/{self.id}'
@patch(as_prop=True)
def url(self:Channel): return f'{DISCORD_WEB}/{self.guild_id}/{self.id}'
@patch(as_prop=True)
def url(self:Message):
gid = self.get('guild_id') or self._parent.get('guild_id') or self._parent.id
return f'{DISCORD_WEB}/{gid}/{self.channel_id}/{self.id}'gld.url, ch.url, msgs[0].url('https://discord.com/channels/1327046393453613076',
'https://discord.com/channels/1327046393453613076/1327046393453613079',
'https://discord.com/channels/1327046393453613076/1327046393453613079/1507463593162047658')
Guild search uses snowflake IDs for date filtering (min_id/max_id), but we autoconvert 'YYYY-MM-DD' strings for convenience for before/after.
depoch = 1420070400000
def date2snowflake(date_str):
"Convert 'YYYY-MM-DD' to a Discord snowflake ID"
dt = datetime.fromisoformat(date_str).replace(tzinfo=timezone.utc)
return str(int((dt.timestamp() * 1000 - depoch) * (2**22)))
@patch
async def search(self:Guild, content=None, author_id=None, channel_id=None, mentions=None,
has=None, before=None, after=None, pinned=None, sort_by=None, sort_order=None,
offset=None, limit=None, use_user=False, nothread:bool=True):
"Search guild messages. `before`/`after` accept 'YYYY-MM-DD' strings or snowflake IDs."
if before and not str(before).isdigit(): before = date2snowflake(before)
if after and not str(after).isdigit(): after = date2snowflake(after)
r = await self('GET', f'/guilds/{self.id}/messages/search', use_user=use_user,
content=content, author_id=author_id, channel_id=channel_id,
mentions=mentions, has=has, min_id=after, max_id=before, pinned=pinned,
sort_by=sort_by, sort_order=sort_order, offset=offset, limit=limit)
msgs = [m for m in [m[0] for m in r['messages']]
if not (nothread and channel_id and m['channel_id'] != channel_id)]
return self.coll('Message', msgs)msgs = await gld.search(after='2026-02-16', limit=5)
msgs| ID | Author | Content | Date |
|---|---|---|---|
| 1523696905593557075 | DBuddy | Back from the dead? (listener auto-resume) | 2026-07-06 |
| 1523685329700393064 | DBuddy | cordslite voice-robustness validation: gateway res | 2026-07-06 |
| 1516773826589888522 | Captain Hook | Usage logging failed for instance -1: instance[-1] | 2026-06-17 |
| 1516773813948518441 | Captain Hook | Usage billing failed: 0 row(s) completed, usage_id | 2026-06-17 |
| 1516773578807316492 | Captain Hook | Usage logging failed for instance -1: instance[-1] | 2026-06-17 |
Sometimes you need to search by name rather than snowflake ID. find_member searches the guild’s members by username, nickname, or display name using Discord’s member search endpoint, and returns the first match’s user ID. This makes it easy to chain into search.
@patch
async def find_member(self:Guild, name):
"Search guild members by name/nick, return first match's user ID or None"
data = await self( 'GET', f'/guilds/{self.id}/members/search', query=name, limit=1)
return data[0]['user']['id'] if data else Noneuid = await gld.find_member('nate.dawgg')
assert uid
await gld.search(author_id=uid, limit=5)| ID | Author | Content | Date |
|---|---|---|---|
| 1506695870760878161 | nate.dawgg | 2026-05-20 | |
| 1503844716091936919 | nate.dawgg | 2026-05-12 | |
| 1483815592933720094 | nate.dawgg | 2026-03-18 | |
| 1483812396307976363 | nate.dawgg | 2026-03-18 | |
| 1481403515963183155 | nate.dawgg | 2026-03-11 |
Sending messages is a POST request with JSON body. Pass reply_id to thread a reply under an existing message. For file attachments, we switch to multipart/form-data—Discord expects a payload_json field with the message JSON, plus files[n] fields for each file.
@patch
async def send(self:Channel, content='', files=None, reply_id=None):
"Send a message with optional file attachments"
path = f'/channels/{self.id}/messages'
mref = {'message_id': reply_id} if reply_id else None
payload = filter_values(dict(content=content, message_reference=mref), negate(is_(None)))
fs = [('files[%d]' % i, (f.name, f.read_bytes(), None)) for i,f in enumerate(map(Path, listify(files)))]
r = await self('POST', path, data={'payload_json': json.dumps(payload)}, files=fs)
return Message(r, self)msg = await ch.send('Hi, from Solveit!'); msgMessage(id=1524525340767289389, author='DBuddy', content='Hi, from Solveit!')reply_msg = await ch.send("I'm replying to myself 🤓", reply_id=msg.id); reply_msgMessage(id=1524525362850304280, author='DBuddy', content="I'm replying to myself 🤓")await msg.channelChannel(id=1327046393453613079, name='general', type=0)msg = await ch.send('Here is a file!', files=['../README.md']); msgMessage(id=1524525372698529911, author='DBuddy', content='Here is a file!')@patch
@delegates(Guild.search, but=['channel_id'])
async def search(self:Channel, **kwargs): return await (await self.guild).search(channel_id=self.id, **kwargs)await ch.search(has='file', limit=1)| ID | Author | Content | Date |
|---|---|---|---|
| 1507463577617825936 | DBuddy | Here is a file! | 2026-05-22 |
Discord messages can have file attachments. The Attachment class wraps them as DiscordObjects—giving you attribute access to filename, size, content_type, url, etc. The fetch method downloads the file content using the existing httpx client.
class Attachment(DiscordObject):
async def fetch(self): return (await self._client.cli.get(self.url)).content
def __repr__(self): return f"Attachment(filename={self.filename!r}, size={self.size}, type={self.content_type})"
def _repr_html_(self):
if self.content_type.startswith('image/'):
return f'<img src="{self.url}" style="max-width:400px"><br><small>{self.filename} ({self.size:,}B)</small>'
return f'<code>{self.filename}</code> ({self.content_type}, {self.size:,}B)'
@patch(as_prop=True)
def attachments(self:Message): return [Attachment(a, self._client) for a in self.get('attachments', [])]atts = msg.attachments; atts[Attachment(filename='README.md', size=28163, type=text/markdown; charset=utf-8)]
readme = (await atts[0].fetch()).decode()
print(readme[:16])# cordslite 🍺
DMs (Direct Messages) are just regular channels in Discord’s API — no special handling needed! To start a DM conversation, POST to /users/@me/channels with a recipient_id. Discord returns a standard channel object, so send() and messages() work exactly as they do for guild channels.
To detect DMs in the gateway, check for the absence of guild_id — DM messages don’t belong to any guild, so this field is missing or None. This makes it easy to route DM vs guild messages in your bot’s handler.
@patch
async def create_dm(self:DiscordClient, user_id):
r = await self._req('POST', '/users/@me/channels', recipient_id=user_id)
return Channel(r, self)# # Commented out so we don't spam Nate
# dm = await dc.create_dm('346450717025894400') # nathan's user ID
# await dm.send('Hello from DMs!')Members vs Users: A User is a global Discord account. A Member is a user within a specific guild—it has guild-specific data like nickname, roles, and join date. The nick or user['username'] pattern shows the server nickname if set, otherwise falls back to the global username.
Note: The members endpoint requires the GUILD_MEMBERS privileged intent enabled in your bot settings on the Developer Portal!
class User(DiscordObject):
def __repr__(self): return f"User(id={self.id}, username={self.username!r})"
class Member(DiscordObject):
def __repr__(self):
name = self.nick or self.user['username']
return f"Member(id={self.user['id']}, name={name!r}, joined={self.joined_at[:10]})"
class Members(list):
def _repr_html_(self): return html_table(self, ("ID", "Name", "Joined", "Roles"), lambda m: (m.user['id'], m.nick or m.user['username'], m.joined_at[:10], len(m.roles)))
@patch
async def members(self:Guild, limit=100):
data = await self('GET', f'/guilds/{self.id}/members', limit=limit)
return self.coll('Member', data)mems = await gld.members(5); mems| ID | Name | Joined | Roles |
|---|---|---|---|
| 346450717025894400 | nathan | 2025-01-09 | 0 |
| 1327047896436178954 | SearchBuddy | 2025-01-09 | 1 |
| 1361823507679543306 | DBuddy | 2025-04-15 | 1 |
| 1448038710229733398 | Dizcord Util Bot | 2025-12-09 | 1 |
| 1467222191182712986 | Search Agent | 2026-01-31 | 1 |
def lbl(ch):
l = "#" if ch.type==0 else "🔊"
l += ch.name
if t := getattr(ch, 'topic', ''): l += ': ' + t
return l
@patch
async def tree(self:Guild, include_members=True, member_limit=1000):
by_parent,cats = defaultdict(list),[None]
for c in await self.channels():
if c.type == 4: cats.append(c)
elif c.type in (0,2): by_parent[c.parent_id].append(c)
lines = [f"{self.name} [{self.id}]"]
for cat in cats:
lines.append(f"|-- {getattr(cat, 'name', 'Uncategorized')}")
for ch in by_parent[getattr(cat, 'id', None)]:
lines.append(f"| |-- {lbl(ch)} [{ch.id}]")
if include_members:
lines.append("|-- Members")
for m in await self.members(member_limit):
u = m.user
name = m.nick or u.get('global_name') or u['username']
lines.append(f"| |-- {name} [{u['id']}]")
return "\n".join(lines)print(await gld.tree())natedog's server [1327046393453613076]
|-- Uncategorized
| |-- #test [1506656999293849753]
| |-- 🔊test-vch [1506657701688508456]
|-- Text Channels
| |-- #general: General conversations [1327046393453613079]
| |-- #private [1327954661960978512]
| |-- #forwarded [1475600987627458812]
| |-- #test-webhooks [1501265913222266961]
|-- Voice Channels
| |-- 🔊General [1327046393453613080]
|-- Members
| |-- nathan [346450717025894400]
| |-- SearchBuddy [1327047896436178954]
| |-- DBuddy [1361823507679543306]
| |-- Dizcord Util Bot [1448038710229733398]
| |-- Search Agent [1467222191182712986]
@patch
async def search_all(self:Guild, limit=500, delay=1.0, max_age_days=None, show=False, **kwargs):
"Paginated search returning up to `limit` messages"
all_msgs, offset = [], 0
while len(all_msgs) < limit:
msgs = await self.search(offset=offset, limit=25, **kwargs)
if not msgs: break
if max_age_days is not None:
cutoff = datetime.now(timezone.utc) - timedelta(days=max_age_days)
msgs = [m for m in msgs if datetime.fromisoformat(m.timestamp) > cutoff]
all_msgs.extend(msgs)
offset += 25
if show: print(f'Fetched {len(msgs)} more')
if len(msgs) < 25: break
await asyncio.sleep(delay)
return Messages(all_msgs[:limit])
@patch
@delegates(Guild.search_all, but=['channel_id'])
async def search_all(self:Channel, **kwargs):
gld = await self.guild
return await gld.search_all(channel_id=self.id, **kwargs)# r = await ch.search_all()
# len(r)@patch
async def delete(self:Message):
"Delete this message"
return await self('DELETE', f'/channels/{self.channel_id}/messages/{self.id}')
@patch
async def delete_message(self:Channel, message_id):
"Delete a message by ID"
return await self('DELETE', f'/channels/{self.id}/messages/{message_id}')
@patch
async def bulk_delete(self:Channel, message_ids):
"Bulk delete messages (must be <14 days old)"
return await self('POST', f'/channels/{self.id}/messages/bulk-delete', messages=[str(i) for i in message_ids])@patch
async def create_thread(self:Message, name):
"Create a thread from this message"
r = await self('POST', f'/channels/{self.channel_id}/messages/{self.id}/threads', name=name)
return Channel(r, self)
@patch
async def thread(self:DiscordClient, thread_id):
"Fetch a thread (which is a Channel)"
return Channel(await self._req('GET', f'/channels/{thread_id}'), self)@patch
async def _del_rotating(self:Channel, message_ids, delay=0.5, show=False):
if show: print(f'Deleting {len(message_ids)} messages...')
for i,mid in enumerate(message_ids):
use_user = i % 2 == 1 and bool(self._client.user_token)
try: await self('DELETE', f'/channels/{self.id}/messages/{mid}', use_user=use_user)
except DiscordError as e:
if e.args[0] == 10008: print(f"Already deleted: {mid}"); continue
raise
await asyncio.sleep(delay)
if show: print('.', end='', flush=True)
return len(message_ids)@patch
async def search_and_delete_all(self:Channel, content, delay=2, show=False, **kwargs):
"Bulk delete recent msgs, individually delete older ones"
assert content, "Must include content to search for"
cutoff = datetime.now(timezone.utc) - timedelta(days=13)
gld = await self.guild
msgs = await gld.search_all(content=content, channel_id=self.id, show=show, **kwargs)
seen = set()
msgs = [m for m in msgs if m.id not in seen and not seen.add(m.id)]
recent = [m for m in msgs if datetime.fromisoformat(m.timestamp) > cutoff]
old = [m for m in msgs if datetime.fromisoformat(m.timestamp) <= cutoff]
for i in range(0, len(recent), 20):
ms = recent[i:i+20]
if len(ms)>1: await self.bulk_delete([m.id for m in ms])
else: await self._del_rotating([m.id for m in ms], show=show)
if show: print(f'Bulk deleted {len(ms)}')
await asyncio.sleep(delay)
if old: await self._del_rotating([m.id for m in old], show=show, delay=delay)
return len(msgs)Webhooks let external services post to a channel without a bot token—executing one only needs its id and token. We wrap them in a Webhook class, with webhooks listings on both Channel and Guild, and edit/delete/send on the webhook itself. send uses ?wait=true so Discord returns the created message, and supports per-message username/avatar_url overrides.
class Webhook(DiscordObject):
def __repr__(self): return f"Webhook(id={self.id}, name={self.name!r})"
class Webhooks(list):
def _repr_html_(self): return html_table(self, ("ID", "Name", "Channel"), lambda w: (w.id, w.name, w.channel_id))
@patch
async def webhooks(self:Channel):
"List this channel's webhooks"
return self.coll('Webhook', await self('GET', f'/channels/{self.id}/webhooks'))
@patch
async def webhooks(self:Guild):
"List all webhooks in this guild"
return self.coll('Webhook', await self('GET', f'/guilds/{self.id}/webhooks'))
@patch
async def webhook(self:DiscordClient, webhook_id):
"Fetch a webhook by ID"
return Webhook(await self._req('GET', f'/webhooks/{webhook_id}'), self)@patch
async def create_webhook(self:Channel, name):
"Add webhook `name` to Channel"
r = await self('POST', f'/channels/{self.id}/webhooks', name=name)
return Webhook(r, self)@patch
async def edit(self:Webhook, name=None, channel_id=None):
"Modify this webhook's `name` or move it to `channel_id`"
return Webhook(await self('PATCH', f'/webhooks/{self.id}', name=name, channel_id=channel_id), self._parent)
@patch
async def delete(self:Webhook):
"Delete this webhook"
return await self('DELETE', f'/webhooks/{self.id}')
@patch
async def send(self:Webhook, content='', username=None, avatar_url=None):
"Execute this webhook, optionally overriding the display `username`/`avatar_url`"
r = await self('POST', f'/webhooks/{self.id}/{self.token}?wait=true', content=content, username=username, avatar_url=avatar_url)
return Message(r, self)