Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f08b7e2
fix(presence): correct IPC method annotations
Senophyx Aug 2, 2026
69f6519
refactor(presence): make Windows imports conditional
Senophyx Aug 2, 2026
6cf4fbd
ci: add Python compile and import checks
Senophyx Aug 2, 2026
33b7abc
test: add minimal test runner
Senophyx Aug 2, 2026
c2b280e
fix(ipc): centralize exact frame reads and preserve opcodes
Senophyx Aug 2, 2026
7b7840d
fix(ipc): route responses by nonce through a single reader
Senophyx Aug 2, 2026
35051b4
fix(ipc): validate handshake and preserve error details
Senophyx Aug 2, 2026
0a1f308
fix(ipc): handle PING and PONG without corrupting requests
Senophyx Aug 2, 2026
3ea902d
refactor(ipc): use blocking reader and drop Windows polling
Senophyx Aug 2, 2026
f2b411a
fix(events): track subscriptions separately and stop idle readers
Senophyx Aug 2, 2026
2bc07b0
feat(events): export event exceptions from package root
Senophyx Aug 2, 2026
9361d74
fix(url): validate URL structure and required fields
Senophyx Aug 2, 2026
d33ce1e
refactor(presence): improve logging and disconnect cleanup
Senophyx Aug 2, 2026
1ef1bcb
docs: document event subscription API and update changelog
Senophyx Aug 2, 2026
d7abe16
fix(events): release state lock during subscription requests
Senophyx Aug 2, 2026
8b9d6a8
fix(packaging): use PEP 621 license table to unblock wheel builds
Senophyx Aug 2, 2026
3d58807
fix(ipc): read handshake frame directly without nonce routing
Senophyx Aug 2, 2026
9b277d5
docs: document party and secret requirements, add subscribe example
Senophyx Aug 2, 2026
2d11f21
docs: rename subscribe example to rpc-events
Senophyx Aug 2, 2026
f79f11e
fix(events): allow stacked handlers and mark pipe disconnected when idle
Senophyx Aug 2, 2026
2bdf755
fix(activity): validate button URLs in set_activity
Senophyx Aug 2, 2026
705033b
chore(release): bump version to 6.5b2
Senophyx Aug 2, 2026
5aa4502
docs(changelog): remove duplicate entries from unreleased
Senophyx Aug 2, 2026
ae17e26
ci: run test suite on all platforms
Senophyx Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
checks:
name: Compile, import, and tests
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Compile package
run: python -m compileall discordrpc
- name: Import package
run: python -c "import discordrpc"
- name: Run tests
run: python tests/run_tests.py
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [6.5b2] - Unreleased

### Added
- Cross-platform event subscription with `Event` enum and `@rpc.on()` decorator (PR [#66](https://github.com/Senophyx/Discord-RPC/pull/66) by @SuperZombi)
- `InvalidEvent` and `InvalidEventType` exceptions exported from the package root
- `utils.required_url()` for validating required button URLs
- `examples/rpc-events.py` showing event subscription usage

### Changed
- IPC reads now preserve the opcode and route responses by nonce through a single background reader
- Windows imports are now loaded only on Windows; the blocking reader no longer needs `msvcrt` or `win32pipe`
- URL validation now checks the scheme and host instead of only a prefix
- `set_activity()` now validates button URLs client-side

### Fixed
- Invalid method annotations in `_send()` and `_request()` that prevented importing the package
- Unread `PONG` responses no longer corrupt subsequent RPC requests
- Incoming `PING` packets are answered with `PONG`
- Handshake no longer requires a nonce and reads the READY frame directly
- Event subscription state no longer mixes callback storage with subscription state
- Failed subscriptions no longer register callbacks locally
- Reader thread stops after the last unsubscribe and is cleaned up during disconnect
- `subscribe()` returns `True` for already-subscribed events so stacked handlers work
- Pipe is marked disconnected when the reader stops idle, allowing reconnect
- `pyproject.toml` license uses the PEP 621 table so wheel builds succeed

## [Unreleased]

### Added
Expand Down
74 changes: 73 additions & 1 deletion DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,76 @@ from discordrpc import StatusDisplay

---

## Events

You can subscribe to Rich Presence events and receive callbacks when they fire.

```python
import discordrpc
from discordrpc import Event

rpc = discordrpc.RPC(app_id=123456789)

@rpc.on(Event.JOIN_REQUEST)
def on_join_request(data):
print("Ask to Join:", data)

rpc.run()
```

Supported events (from `discordrpc.Event`):

- `Event.JOIN` (`ACTIVITY_JOIN`)
- `Event.JOIN_REQUEST` (`ACTIVITY_JOIN_REQUEST`)
- `Event.SPECTATE` (`ACTIVITY_SPECTATE`)
- `Event.INVITE` (`ACTIVITY_INVITE`)

Only these activity events are currently exposed. Other Discord RPC events are not supported yet.

### Enabling JOIN and SPECTATE events

- `party_id` is **required** for the "Ask to Join" button and the `ACTIVITY_JOIN_REQUEST` event to work. Without it, Discord cannot resolve the party, the event is never delivered, and the requester gets "Your message could not be delivered."
- `join_secret` and `spectate_secret` must have **different values**. Discord rejects the activity with `secrets must be unique` when they match.

A minimal working setup:

```python
rpc.set_activity(
name="VALORANT",
details="Valorant Ranked",
party_id=1234,
join_secret="anything",
spectate_secret="idk",
)
```

### Direct subscribe and unsubscribe

```python
rpc.subscribe("ACTIVITY_JOIN") # string form accepted
rpc.subscribe(Event.JOIN) # enum form accepted
rpc.unsubscribe(Event.JOIN)
```

- `subscribe()` / `unsubscribe()` accept either an `Event` member or a valid event string.
- An unknown event name raises `InvalidEvent`.
- A non-string, non-enum value raises `InvalidEventType`.
- The `@rpc.on()` decorator raises `RPCException` if the subscription is rejected by Discord.

### Callback behavior

- Callbacks run on the internal IPC reader thread. Keep them short and non-blocking.
- A slow callback delays processing of other events and responses.
- An exception raised inside a callback is logged and does not stop the reader.
- Use locks or queues if your callback touches shared state.

### Disconnect and reconnect

- `disconnect()` clears all subscriptions and callbacks.
- After a reconnect, call `subscribe()` again if you want events.

---

## Exceptions

All exceptions extend `RPCException`.
Expand All @@ -305,12 +375,14 @@ All exceptions extend `RPCException`.
| `Error(message)` | Generic user error |
| `DiscordNotOpened()` | Discord not found/running |
| `ActivityError()` | Invalid activity payload |
| `InvalidURL()` | URL not starting with http/https |
| `InvalidURL(message)` | URL is not a valid http/https URL |
| `InvalidID()` | Invalid Application ID |
| `ButtonError(message)` | Button limit exceeded |
| `ProgressbarError(message)` | Invalid progress values |
| `InvalidActivityType(message)` | act_type not a valid Activity |
| `ActivityTypeDisabled()` | Streaming/Custom blocked by Discord |
| `InvalidEvent(message)` | Event name is not subscribable |
| `InvalidEventType(message)` | Event input is not a string or Event |

---

Expand Down
3 changes: 2 additions & 1 deletion discordrpc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
RPCException, Error, DiscordNotOpened, ActivityError,
InvalidURL, InvalidID, ButtonError, ProgressbarError,
InvalidActivityType, ActivityTypeDisabled,
InvalidEvent, InvalidEventType,
)
from .types import Activity, StatusDisplay, User, Application, Event
from .utils import remove_none, timestamp, date_to_timestamp, use_local_time, progress_bar, get_app_info
Expand All @@ -13,7 +14,7 @@
try:
__version__ = _pkg_ver('discord-rpc')
except PackageNotFoundError:
__version__ = "6.5b1"
__version__ = "6.5b2"
__authors__ = "Senophyx"
__license__ = "MIT License"
__copyright__ = "Copyright 2021-2025 Senophyx"
7 changes: 3 additions & 4 deletions discordrpc/button.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from .exceptions import InvalidURL
from .utils import valid_url
from .utils import required_url


def button(text:str, url:str):
return {"label": text, "url": valid_url(url)}
def button(text: str, url: str):
return {"label": text, "url": required_url(url)}
6 changes: 4 additions & 2 deletions discordrpc/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ def __init__(self):
super().__init__("An error has occurred in activity payload, do you have set your activity correctly?")

class InvalidURL(RPCException):
def __init__(self):
super().__init__("URL must start with http:// or https://")
def __init__(self, message: str = None):
if message is None:
message = "URL must be a valid http:// or https:// URL"
super().__init__(message)

class InvalidID(RPCException):
def __init__(self):
Expand Down
Loading
Loading