customrp-core 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KeiraOMG0
4
+ Copyright (c) 2020-2026 maximmax42 (CustomRP, from which the presence rules and preset format are derived)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: customrp-core
3
+ Version: 0.1.2
4
+ Summary: Discord Rich Presence library implementing CustomRP's behaviour, for Linux, macOS and Windows
5
+ Author: KeiraOMG0
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/KeiraOMG0/customrp-core
8
+ Project-URL: Documentation, https://github.com/KeiraOMG0/customrp-core/tree/main/docs
9
+ Project-URL: Issues, https://github.com/KeiraOMG0/customrp-core/issues
10
+ Keywords: discord,rich presence,rpc,customrp
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Communications :: Chat
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest>=7; extra == "test"
22
+ Dynamic: license-file
23
+
24
+ # customrp-core
25
+
26
+ A small Python library for setting a custom Discord Rich Presence, and the engine behind
27
+ [customrp-cli](https://github.com/KeiraOMG0/customrp-cli). It follows the rules of
28
+ [CustomRP](https://www.customrp.xyz), the Windows app by maximmax42, but has no GUI and no Windows-only parts.
29
+
30
+ > **Independent project.** customrp-core is not part of CustomRP and is not maintained by its developer.
31
+ > Report problems here, not on the CustomRP repository.
32
+
33
+ ## What it does
34
+
35
+ - **Talks to the Discord desktop client** over its local IPC socket (Linux, macOS) or named pipe (Windows).
36
+ No extra libraries are needed.
37
+ - **Models everything CustomRP can set:** activity type, name, details and state (with links), party size,
38
+ five timestamp modes, large and small images (asset keys or URLs) with tooltips and links, two buttons,
39
+ and which field shows in the member list.
40
+ - **Validates a presence the way Discord will.** It checks length limits (in characters or UTF-8 bytes),
41
+ single-character fields, bad URLs, and image URLs Discord would truncate, so you get a readable error
42
+ instead of a rejection.
43
+ - **Cleans up the input the same way CustomRP does:** it adds `https://`, converts hosts to punycode,
44
+ percent-encodes characters URLs can't contain, keeps a leading no-break space, and routes images from
45
+ Discord's own CDN through CustomRP's proxy.
46
+ - **Keeps a presence up** with `PresenceSession`. It retries every 10 seconds when Discord isn't running,
47
+ reconnects when Discord restarts, refreshes the local time display at midnight, and resends when the
48
+ presence changes.
49
+ - **Reads and writes `.crp` presets** that open in the Windows app and the other way round.
50
+ - **Lists an application's art assets**, using the public endpoint CustomRP uses.
51
+ - **Ships a fake Discord server** (`customrp_core.testing`) for testing your own code without Discord.
52
+
53
+ It has no dependencies outside the Python standard library.
54
+
55
+ ## Install
56
+
57
+ Python 3.9 or newer. Install from [PyPI](https://pypi.org/project/customrp-core/), ideally inside a
58
+ virtual environment:
59
+
60
+ ```sh
61
+ python3 -m venv .venv
62
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
63
+ pip install customrp-core
64
+ ```
65
+
66
+ Or straight from Git, at a release tag:
67
+
68
+ ```sh
69
+ pip install "customrp-core @ git+https://github.com/KeiraOMG0/customrp-core.git@v0.1.2"
70
+ ```
71
+
72
+ In a `requirements.txt`, either works:
73
+
74
+ ```
75
+ customrp-core>=0.1.2,<0.2
76
+ customrp-core @ git+https://github.com/KeiraOMG0/customrp-core.git@v0.1.2
77
+ ```
78
+
79
+ ## Example
80
+
81
+ ```python
82
+ import logging
83
+ from customrp_core import Button, Presence, PresenceSession, validate
84
+
85
+ logging.basicConfig(level=logging.INFO)
86
+
87
+ presence = Presence(
88
+ application_id="", # empty: CustomRP's default application
89
+ details="Writing a library",
90
+ state="in Python",
91
+ large_image="https://example.com/logo.png",
92
+ button1=Button("Source", "github.com/KeiraOMG0/customrp-core"),
93
+ )
94
+
95
+ for issue in validate(presence):
96
+ print(issue) # e.g. "details: must be at least 2 characters long ..."
97
+
98
+ session = PresenceSession(presence) # retries 30 times, every 10 s, like CustomRP
99
+ try:
100
+ session.run() # blocks; Ctrl+C to stop
101
+ except KeyboardInterrupt:
102
+ pass
103
+ ```
104
+
105
+ More in [docs/getting-started.md](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/getting-started.md).
106
+
107
+ ## Documentation
108
+
109
+ | Page | Contents |
110
+ |---|---|
111
+ | [Getting started](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/getting-started.md) | Installing, a first presence, common tasks |
112
+ | [Presence model](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/presence.md) | Every field, its limits, validation, and how the payload is built |
113
+ | [Sessions](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/sessions.md) | `PresenceSession`: connecting, retries, updates, threads |
114
+ | [Low-level IPC](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/ipc.md) | `DiscordIPC`, where Discord's socket is looked for, the wire protocol |
115
+ | [Presets](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/presets.md) | The `.crp` format and how it maps to `Presence` |
116
+ | [Assets](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/assets.md) | Listing an application's uploaded images |
117
+ | [Testing with FakeDiscord](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/testing.md) | Testing code that uses this library |
118
+ | [Compatibility with CustomRP](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/compatibility.md) | What matches the Windows app, what differs, what isn't verified |
119
+ | [Development](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/development.md) | Running the tests, project layout, releasing |
120
+
121
+ ## Platform support
122
+
123
+ | Platform | Status |
124
+ |---|---|
125
+ | Windows 10/11 | Unit tests pass. A handshake with a real Discord client over its named pipe works. |
126
+ | Linux | Unit tests run in CI on Ubuntu, including a fake Discord over a real Unix socket. Not yet tried against a real Discord client. |
127
+ | macOS | Unit tests run in CI, as on Linux. Not yet tried against a real Discord client. |
128
+
129
+ The Discord client has to run on the same machine and user account, since Rich Presence only works
130
+ through its local IPC endpoint. Flatpak, Snap and Vesktop socket locations are searched too (see
131
+ [docs/ipc.md](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/ipc.md)); those paths come from other Rich Presence projects, and none has been tested here.
132
+
133
+ ## Credits
134
+
135
+ The presence rules, preset format and default application come from
136
+ [CustomRP](https://github.com/maximmax42/Discord-CustomRP) by maximmax42 (MIT license). Thanks to them for
137
+ the original app and for being open to an independent alternative.
138
+
139
+ ## License
140
+
141
+ MIT. See [LICENSE](https://github.com/KeiraOMG0/customrp-core/blob/main/LICENSE).
@@ -0,0 +1,118 @@
1
+ # customrp-core
2
+
3
+ A small Python library for setting a custom Discord Rich Presence, and the engine behind
4
+ [customrp-cli](https://github.com/KeiraOMG0/customrp-cli). It follows the rules of
5
+ [CustomRP](https://www.customrp.xyz), the Windows app by maximmax42, but has no GUI and no Windows-only parts.
6
+
7
+ > **Independent project.** customrp-core is not part of CustomRP and is not maintained by its developer.
8
+ > Report problems here, not on the CustomRP repository.
9
+
10
+ ## What it does
11
+
12
+ - **Talks to the Discord desktop client** over its local IPC socket (Linux, macOS) or named pipe (Windows).
13
+ No extra libraries are needed.
14
+ - **Models everything CustomRP can set:** activity type, name, details and state (with links), party size,
15
+ five timestamp modes, large and small images (asset keys or URLs) with tooltips and links, two buttons,
16
+ and which field shows in the member list.
17
+ - **Validates a presence the way Discord will.** It checks length limits (in characters or UTF-8 bytes),
18
+ single-character fields, bad URLs, and image URLs Discord would truncate, so you get a readable error
19
+ instead of a rejection.
20
+ - **Cleans up the input the same way CustomRP does:** it adds `https://`, converts hosts to punycode,
21
+ percent-encodes characters URLs can't contain, keeps a leading no-break space, and routes images from
22
+ Discord's own CDN through CustomRP's proxy.
23
+ - **Keeps a presence up** with `PresenceSession`. It retries every 10 seconds when Discord isn't running,
24
+ reconnects when Discord restarts, refreshes the local time display at midnight, and resends when the
25
+ presence changes.
26
+ - **Reads and writes `.crp` presets** that open in the Windows app and the other way round.
27
+ - **Lists an application's art assets**, using the public endpoint CustomRP uses.
28
+ - **Ships a fake Discord server** (`customrp_core.testing`) for testing your own code without Discord.
29
+
30
+ It has no dependencies outside the Python standard library.
31
+
32
+ ## Install
33
+
34
+ Python 3.9 or newer. Install from [PyPI](https://pypi.org/project/customrp-core/), ideally inside a
35
+ virtual environment:
36
+
37
+ ```sh
38
+ python3 -m venv .venv
39
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
40
+ pip install customrp-core
41
+ ```
42
+
43
+ Or straight from Git, at a release tag:
44
+
45
+ ```sh
46
+ pip install "customrp-core @ git+https://github.com/KeiraOMG0/customrp-core.git@v0.1.2"
47
+ ```
48
+
49
+ In a `requirements.txt`, either works:
50
+
51
+ ```
52
+ customrp-core>=0.1.2,<0.2
53
+ customrp-core @ git+https://github.com/KeiraOMG0/customrp-core.git@v0.1.2
54
+ ```
55
+
56
+ ## Example
57
+
58
+ ```python
59
+ import logging
60
+ from customrp_core import Button, Presence, PresenceSession, validate
61
+
62
+ logging.basicConfig(level=logging.INFO)
63
+
64
+ presence = Presence(
65
+ application_id="", # empty: CustomRP's default application
66
+ details="Writing a library",
67
+ state="in Python",
68
+ large_image="https://example.com/logo.png",
69
+ button1=Button("Source", "github.com/KeiraOMG0/customrp-core"),
70
+ )
71
+
72
+ for issue in validate(presence):
73
+ print(issue) # e.g. "details: must be at least 2 characters long ..."
74
+
75
+ session = PresenceSession(presence) # retries 30 times, every 10 s, like CustomRP
76
+ try:
77
+ session.run() # blocks; Ctrl+C to stop
78
+ except KeyboardInterrupt:
79
+ pass
80
+ ```
81
+
82
+ More in [docs/getting-started.md](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/getting-started.md).
83
+
84
+ ## Documentation
85
+
86
+ | Page | Contents |
87
+ |---|---|
88
+ | [Getting started](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/getting-started.md) | Installing, a first presence, common tasks |
89
+ | [Presence model](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/presence.md) | Every field, its limits, validation, and how the payload is built |
90
+ | [Sessions](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/sessions.md) | `PresenceSession`: connecting, retries, updates, threads |
91
+ | [Low-level IPC](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/ipc.md) | `DiscordIPC`, where Discord's socket is looked for, the wire protocol |
92
+ | [Presets](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/presets.md) | The `.crp` format and how it maps to `Presence` |
93
+ | [Assets](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/assets.md) | Listing an application's uploaded images |
94
+ | [Testing with FakeDiscord](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/testing.md) | Testing code that uses this library |
95
+ | [Compatibility with CustomRP](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/compatibility.md) | What matches the Windows app, what differs, what isn't verified |
96
+ | [Development](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/development.md) | Running the tests, project layout, releasing |
97
+
98
+ ## Platform support
99
+
100
+ | Platform | Status |
101
+ |---|---|
102
+ | Windows 10/11 | Unit tests pass. A handshake with a real Discord client over its named pipe works. |
103
+ | Linux | Unit tests run in CI on Ubuntu, including a fake Discord over a real Unix socket. Not yet tried against a real Discord client. |
104
+ | macOS | Unit tests run in CI, as on Linux. Not yet tried against a real Discord client. |
105
+
106
+ The Discord client has to run on the same machine and user account, since Rich Presence only works
107
+ through its local IPC endpoint. Flatpak, Snap and Vesktop socket locations are searched too (see
108
+ [docs/ipc.md](https://github.com/KeiraOMG0/customrp-core/blob/main/docs/ipc.md)); those paths come from other Rich Presence projects, and none has been tested here.
109
+
110
+ ## Credits
111
+
112
+ The presence rules, preset format and default application come from
113
+ [CustomRP](https://github.com/maximmax42/Discord-CustomRP) by maximmax42 (MIT license). Thanks to them for
114
+ the original app and for being open to an independent alternative.
115
+
116
+ ## License
117
+
118
+ MIT. See [LICENSE](https://github.com/KeiraOMG0/customrp-core/blob/main/LICENSE).
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "customrp-core"
7
+ dynamic = ["version"]
8
+ description = "Discord Rich Presence library implementing CustomRP's behaviour, for Linux, macOS and Windows"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "KeiraOMG0" }]
13
+ keywords = ["discord", "rich presence", "rpc", "customrp"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Communications :: Chat",
21
+ ]
22
+ dependencies = []
23
+
24
+ [project.optional-dependencies]
25
+ test = ["pytest>=7"]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/KeiraOMG0/customrp-core"
29
+ Documentation = "https://github.com/KeiraOMG0/customrp-core/tree/main/docs"
30
+ Issues = "https://github.com/KeiraOMG0/customrp-core/issues"
31
+
32
+ [tool.setuptools.dynamic]
33
+ version = { attr = "customrp_core.__version__" }
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
40
+ addopts = "-m 'not integration'"
41
+ markers = ["integration: needs a real Discord client running (see docs/development.md)"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,35 @@
1
+ """customrp-core: the platform-independent parts of CustomRP, as a library.
2
+
3
+ Build a :class:`Presence`, then either turn it into a payload with :func:`build_activity` and send it
4
+ with :class:`DiscordIPC`, or let :class:`PresenceSession` handle connecting, retrying and resending.
5
+ """
6
+
7
+ __version__ = "0.1.2"
8
+
9
+ from .errors import AssetsError, CustomRPError, PresenceError, PresetError # noqa: E402
10
+ from .ipc import DiscordIPC, display_name, ipc_paths, open_transport # noqa: E402
11
+ from .presence import ( # noqa: E402
12
+ DEFAULT_APPLICATION_ID,
13
+ MAX_LENGTH,
14
+ Activity,
15
+ ActivityType,
16
+ Button,
17
+ Issue,
18
+ Presence,
19
+ StatusDisplay,
20
+ TimestampMode,
21
+ build_activity,
22
+ check,
23
+ normalize,
24
+ validate,
25
+ )
26
+ from .preset import dump_preset, load_preset, parse_preset, save_preset # noqa: E402
27
+ from .session import PresenceSession, SessionState # noqa: E402
28
+
29
+ __all__ = [
30
+ "Activity", "ActivityType", "AssetsError", "Button", "CustomRPError", "DEFAULT_APPLICATION_ID",
31
+ "DiscordIPC", "Issue", "MAX_LENGTH", "Presence", "PresenceError", "PresenceSession", "PresetError",
32
+ "SessionState", "StatusDisplay", "TimestampMode", "build_activity", "check", "display_name",
33
+ "dump_preset", "ipc_paths", "load_preset", "normalize", "open_transport", "parse_preset",
34
+ "save_preset", "validate",
35
+ ]
@@ -0,0 +1,64 @@
1
+ """Looking up the art assets uploaded to a Discord application.
2
+
3
+ CustomRP fills its image dropdowns from the same public endpoint; it needs no token.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import urllib.error
10
+ import urllib.request
11
+ from dataclasses import dataclass
12
+
13
+ from . import __version__
14
+ from .errors import AssetsError
15
+
16
+ ASSETS_URL = "https://discord.com/api/oauth2/applications/{}/assets"
17
+ ASSETS_PAGE_URL = "https://discord.com/developers/applications/{}/rich-presence/assets"
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Asset:
22
+ id: str
23
+ name: str
24
+ #: Discord's asset type number, passed through as is (CustomRP doesn't use it)
25
+ type: int
26
+
27
+
28
+ def _check_id(application_id: str):
29
+ if not (application_id.isdigit() and application_id.isascii()):
30
+ raise AssetsError(f"{application_id!r} isn't a valid application ID")
31
+
32
+
33
+ def assets_page_url(application_id: str) -> str:
34
+ """The Developer Portal page where the application's assets are managed."""
35
+ _check_id(application_id)
36
+ return ASSETS_PAGE_URL.format(application_id)
37
+
38
+
39
+ def fetch_assets(application_id: str, *, timeout: float = 10.0) -> list[Asset]:
40
+ """Fetches the list of assets uploaded to an application. Raises :class:`AssetsError` on failure."""
41
+ _check_id(application_id)
42
+ request = urllib.request.Request(ASSETS_URL.format(application_id),
43
+ headers={"User-Agent": f"customrp-core/{__version__}"})
44
+ try:
45
+ with urllib.request.urlopen(request, timeout=timeout) as response:
46
+ data = json.load(response)
47
+ except urllib.error.HTTPError as e:
48
+ if e.code == 404:
49
+ raise AssetsError(f"There's no application with the ID {application_id}") from e
50
+ raise AssetsError(f"Discord answered with HTTP {e.code} {e.reason}") from e
51
+ except (urllib.error.URLError, OSError) as e:
52
+ reason = getattr(e, "reason", e)
53
+ raise AssetsError(f"Couldn't reach Discord: {reason}") from e
54
+ except ValueError as e:
55
+ raise AssetsError("Discord sent something that isn't JSON") from e
56
+
57
+ return parse_assets(data)
58
+
59
+
60
+ def parse_assets(data) -> list[Asset]:
61
+ if not isinstance(data, list):
62
+ raise AssetsError("Unexpected answer from Discord")
63
+ return [Asset(str(item.get("id", "")), str(item.get("name", "")), int(item.get("type", 0) or 0))
64
+ for item in data if isinstance(item, dict)]
@@ -0,0 +1,29 @@
1
+ """Exceptions raised by customrp-core."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class CustomRPError(Exception):
7
+ """Base class for every error this library raises on purpose."""
8
+
9
+
10
+ class PresenceError(CustomRPError, ValueError):
11
+ """A presence can't be sent as it is.
12
+
13
+ ``field`` names the :class:`~customrp_core.presence.Presence` attribute at fault
14
+ (for example ``"large_image"`` or ``"button1.url"``), or is ``None`` when the problem
15
+ isn't tied to one field.
16
+ """
17
+
18
+ def __init__(self, message: str, field: str | None = None):
19
+ super().__init__(f"{field}: {message}" if field else message)
20
+ self.message = message
21
+ self.field = field
22
+
23
+
24
+ class PresetError(CustomRPError):
25
+ """A ``.crp`` preset file can't be read or written."""
26
+
27
+
28
+ class AssetsError(CustomRPError):
29
+ """The list of an application's art assets couldn't be fetched."""