Skip to content

Commit

Permalink
2024.10.10
Browse files Browse the repository at this point in the history
- disco.api.client: define a repr for Disco's APIClient object;
- disco.bot.bot: define a repr for Disco's Bot object;
- disco.bot.bot: log invalid plugin paths as errors instead of exceptions;
- disco.gateway.client: define a repr for Disco's GatewayClient object;
- disco.gateway.client: use the websocket's ping to calculate latency instead of running our own calculation;
- disco.voice.client: use the websocket's ping to calculate latency instead of running our own calculation;
- disco.voice.client: keep list of VoiceClient.experiments as sent from the gateway;
- disco.voice.client: define a repr for Disco's VoiceClient object;
- disco.voice.client: log invalid voice gateway packets as errors instead of exceptions;
- disco.voice.client: log failed voice channel connections as errors instead of exceptions;
- disco.voice.packets: map `CLIENT_CONNECT` as OP 11 (not 18), `CLIENT_FLAGS` as OP 18;
- disco.voice.udp: fix `UDPVoiceClient.connect()` IP stringification;
- disco.client: define a repr for Disco's Client object;
- disco.state: define a repr for Disco's State object;
- disco.state: do not cache webhooks in `User` cache on `MessageCreate` events;
- pyproject.toml: what could possibly go wrong?;
- README.md: there's a meme in here somewhere?;
Update requirements.txt:
- gevent (24.2.1 >> 24.10.2)
  • Loading branch information
elderlabs committed Oct 12, 2024
1 parent 6765864 commit 4035e87
Show file tree
Hide file tree
Showing 16 changed files with 215 additions and 166 deletions.
110 changes: 65 additions & 45 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,80 +1,100 @@
name: Dance
name: "DANCE"

on:
push:
branches:
- main

permissions:
contents: read
- main
paths:
- 'disco/**'
workflow_dispatch:

jobs:
build:

runs-on: ubuntu-latest
name: Build
runs-on: self-hosted

steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Install Dependencies
run: |
python -m pip install --no-cache-dir --upgrade pip
python -m pip install --no-cache-dir --upgrade build
- name: Build binary wheel and source tarball
run: python -m build
- name: Store distribution packages
uses: actions/upload-artifact@v3
with:
name: python-package-distributions
path: dist/
- name: Pull Repository
uses: actions/checkout@v4
- name: Setup Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Dependencies
run: |
python -m pip install --no-cache-dir --upgrade pip
python -m pip install --no-cache-dir --upgrade setuptools
python -m pip install --no-cache-dir --upgrade wheel
python -m pip install --no-cache-dir --upgrade -r requirements.txt
python -m pip install --no-cache-dir --upgrade build
- name: Build Wheel and Tarball Packages
run: python3 -m build
- name: Store distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/

publish:
name: Publish
needs:
- build
runs-on: ubuntu-latest
- build
runs-on: self-hosted
environment:
name: pypi
url: https://pypi.org/p/betterdisco
url: https://pypi.org/p/betterdisco-py
permissions:
id-token: write

steps:
- name: Download distributions
uses: actions/download-artifact@v3
- name: Download Distributions
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true

github-release:
name: >-
Sign with Sigstore, upload to GitHub Releases
name: Sign and Release
needs:
- publish-to-pypi
runs-on: ubuntu-latest
- build
runs-on: self-hosted

permissions:
contents: write
id-token: write
contents: write # IMPORTANT: mandatory for making GitHub Releases
id-token: write # IMPORTANT: mandatory for sigstore

steps:
- name: Download distributions
uses: actions/download-artifact@v3
- name: Setup Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Download Distributions
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Sign dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v1.2.3
- name: Sign
uses: sigstore/gh-action-sigstore-python@v2.1.1
with:
inputs: >-
inputs: |
./dist/*.tar.gz
./dist/*.whl
- name: Create GitHub Release
- name: Get Version from Commit Title
id: get_version
run: |
VERSION=$(git log --format=%B -n 1 HEAD | head -n 1)
echo "::set-output name=VERSION::$VERSION"
- name: Create Release # TODO: make this better
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release create '${{ github.ref_name }}' --repo '${{ github.repository }}' --notes ""
- name: Upload artifact signatures to GitHub Releases
run: |
gh release create '${{ steps.get_version.outputs.VERSION }}' --repo '${{ github.repository }}' --notes ""
- name: Upload Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release upload '${{ github.ref_name }}' dist/** --repo '${{ github.repository }}'
run: |
gh release upload '${{ steps.get_version.outputs.VERSION }}' dist/** --repo '${{ github.repository }}'
22 changes: 11 additions & 11 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ name: "CodeQL"

on:
push:
branches: [ "main", "staging/dooley" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main", "staging/dooley" ]
schedule:
- cron: '23 17 * * 1'
branches:
- main
- staging/dooley
paths:
- 'disco/**'
workflow_dispatch:

jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
runs-on: self-hosted
permissions:
actions: read
contents: read
Expand All @@ -28,12 +28,12 @@ jobs:
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support

steps:
- name: Checkout repository
- name: Pull Repository
uses: actions/checkout@v3

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand All @@ -47,7 +47,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3

# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
Expand All @@ -60,6 +60,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
2 changes: 0 additions & 2 deletions MANIFEST.in

This file was deleted.

44 changes: 25 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
# BetterDisco
_BetterDisco_ is an up-to-date modernized fork of Disco, a library witten by b1nzy, the creator of Discord's API, iirc.
_Disco_ is a _library_, written in Python 3 to interface with [Discord's API](https://discord.com/developers/docs/intro) as _efficiently_ and _effectively_ as possible.
Disco is _expressive_, and contains a _functional interface_.
Disco is built for _performance_ and _efficiency_.
Disco is _scalable_ and works well in large and small deployments.
Disco is _configurable_ and _modular_.
Disco contains evented network and IO pipes, courtesy of `gevent`.
_Buzzwords **100**._ WYSIWYG.

Disco is an extensive and extendable Python 3.x library for the [Discord API](https://discord.com/developers/docs/intro). Disco boasts the following major features:

- Expressive, functional interface that gets out of the way
- Built for high-performance and efficiency
- Configurable and modular, take the bits you need
- Full support for Python 3.x
- Evented networking and IO using Gevent

## Installation
Disco is designed to run both as a generic-use library, and as a standalone bot toolkit. Installing disco is as easy as running `pip install betterdisco-py --upgrade --no-cache-dir`, however, additional options are available for extended features, performance, and support:

Disco was built to run both as a generic-use library, and a standalone bot toolkit. Installing disco is as easy as running `pip install disco-py`, however some extra packages are recommended for power-users, namely:
| _This_ | Installs _these_ | _Why?_ |
|-------------------------------|-------------------------------------------------------------|--------------------------------------------------------------------------------|
| `betterdisco-py` | `gevent`, `requests`, `websocket-client` | Required for base Disco functionality. |
| `betterdisco-py[http]` | `flask` | Useful for hosting an API to interface with your bot. |
| `betterdisco-py[performance]` | `erlpack`, `isal`, `regex`, `pylibyaml`, `ujson`, `wsaccel` | Useful for performance improvement in several areas. _I am speed._ |
| `betterdisco-py[sharding]` | `gipc`, `dill` | Required for auto-sharding and inter-process communication. |
| `betterdisco-py[voice]` | `libnacl` | Required for VC connectivity and features. |
| `betterdisco-py[yaml]` | `pyyaml` | Required for YAML support, particularly if using `config.yaml`. |
| `betterdisco-py[all]` | _**All of the above**, unless otherwise noted._ | **All additional packages**, for the poweruser that _absolutely needs it all_. |

|Name|Reason|
|----|------|
|requests[security]|adds packages for a proper SSL implementation|
|earl-etf (3.x)|ETF parser run with the --encoder=etf flag|
|gipc|Gevent IPC, required for autosharding|

## Examples

Simple bot using the builtin bot authoring tools:
Simple bot using the built-in bot authoring tools:

```python
from disco.bot import Bot, Plugin
from disco.bot import Plugin


class SimplePlugin(Plugin):
Expand All @@ -35,18 +39,20 @@ class SimplePlugin(Plugin):
# They also provide an easy-to-use command component
@Plugin.command('ping')
def on_ping_command(self, event):
event.msg.reply('Pong!')
event.reply('Pong!')

# Which includes command argument parsing
@Plugin.command('echo', '<content:str...>')
def on_echo_command(self, event, content):
event.msg.reply(content)
event.reply(content)
```

Using the default bot configuration, we can now run this script like so:

`python -m disco.cli --token="MY_DISCORD_TOKEN" --run-bot --plugin simpleplugin`

And commands can be triggered by mentioning the bot (configured by the BotConfig.command\_require\_mention flag):
And commands can be triggered by mentioning the bot (configured by the BotConfig.command_require_mention flag):

![](http://i.imgur.com/Vw6T8bi.png)

### For further information and configuration options, please refer to our documentation first and foremost.
2 changes: 1 addition & 1 deletion disco/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = '1.0.0'
VERSION = "2024.10.10"
3 changes: 3 additions & 0 deletions disco/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ def __init__(self, token, client=None):

self._captures = local()

def __repr__(self):
return '<Disco APIClient{}>'.format(f' shard_id={self.client.config.shard_id}' if self.client else '')

def _after_requests(self, response):
if not hasattr(self._captures, 'responses'):
return
Expand Down
5 changes: 4 additions & 1 deletion disco/bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ def __init__(self, client, config=None):
level = int(level) if str(level).isdigit() else get_enum_value_by_name(CommandLevels, level)
self.config.levels[entity_id] = level

def __repr__(self):
return f'<DiscoBot bot_id={self.client.state.me.id} shard_id={self.client.config.shard_id}>'

@classmethod
def from_cli(cls, *plugins):
"""
Expand Down Expand Up @@ -577,7 +580,7 @@ def add_plugin_module(self, path, config=None):
self.add_plugin(plugin, config)

if not loaded:
raise Exception(f'Could not find any plugins to load within module {path}')
self.log.error(f'Could not find plugins to load within module {path}')

def load_plugin_config(self, cls):
name = cls.__name__.lower()
Expand Down
3 changes: 3 additions & 0 deletions disco/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ def __init__(self, config):
localf=lambda: self.manhole_locals)
self.manhole.start()

def __repr__(self):
return '<DiscoClient{}{}>'.format(f' bot_id={self.state.me.id}' if self.state and self.state.me else '', f' shard_id={self.config.shard_id}')

def update_presence(self, status, game=None, afk=False, since=0.0):
"""
Updates the current client's presence.
Expand Down
11 changes: 7 additions & 4 deletions disco/gateway/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from gevent import sleep as gevent_sleep, spawn as gevent_spawn
from gevent.event import Event as GeventEvent
from platform import system as platform_system
from time import perf_counter as time_perf_counter, perf_counter_ns as time_perf_counter_ns
from time import time, perf_counter_ns as time_perf_counter_ns
from websocket import ABNF, WebSocketConnectionClosedException, WebSocketTimeoutException
from zlib import decompress as zlib_decompress, decompressobj as zlib_decompressobj

Expand Down Expand Up @@ -76,6 +76,9 @@ def __init__(self, client, max_reconnects=5, encoder='json', zlib_stream_enabled
self._last_heartbeat = 0
self.latency = -1

def __repr__(self):
return f'<GatewayClient shard_id={self.client.config.shard_id} endpoint={self._cached_gateway_url}>'

def send(self, op, data):
if not self.ws.is_closed:
self.limiter.check()
Expand All @@ -98,7 +101,7 @@ def heartbeat_task(self, interval):
self.ws.close(status=1000)
self.client.gw.on_close(0, 'HEARTBEAT failure')
return
self._last_heartbeat = time_perf_counter()
self._last_heartbeat = time()

self._send(OPCode.HEARTBEAT, self.seq)
self._heartbeat_acknowledged = False
Expand All @@ -125,7 +128,7 @@ def handle_heartbeat(self, _):
def handle_heartbeat_acknowledge(self, _):
self.log.debug('Received HEARTBEAT_ACK')
self._heartbeat_acknowledged = True
self.latency = float('{:.2f}'.format((time_perf_counter() - self._last_heartbeat) * 1000))
self.latency = self.ws.last_pong_tm and float('{:.2f}'.format((self.ws.last_pong_tm - self.ws.last_ping_tm) * 1000))

def handle_reconnect(self, _):
self.log.warning('Received RECONNECT request; resuming')
Expand Down Expand Up @@ -175,7 +178,7 @@ def connect_and_run(self, gateway_url=None):
self.ws.emitter.on('on_close', self.on_close)
self.ws.emitter.on('on_message', self.on_message)

self.ws.run_forever()
self.ws.run_forever(ping_interval=60, ping_timeout=5)

def on_message(self, msg):
if self.zlib_stream_enabled:
Expand Down
5 changes: 4 additions & 1 deletion disco/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ def __init__(self, client, config):
self.listeners = []
self.bind()

def __repr__(self):
return f'<DiscoState bot_id={self.me.id} shard_id={self.client.config.shard_id}>'

def unbind(self):
"""
Unbinds all bound event listeners for this state object.
Expand Down Expand Up @@ -172,7 +175,7 @@ def on_user_update(self, event):
self.me.inplace_update(event.user)

def on_message_create(self, event):
if self.config.cache_users and event.message.author.id not in self.users:
if self.config.cache_users and event.message.author.id not in self.users and not event.message.webhook_id:
self.users[event.message.author.id] = event.message.author

if self.config.sync_guild_members and event.message.member:
Expand Down
Loading

0 comments on commit 4035e87

Please sign in to comment.