lupaxa-chat-server 0.1.0__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.
- lupaxa_chat_server-0.1.0/.gitignore +30 -0
- lupaxa_chat_server-0.1.0/LICENCE +21 -0
- lupaxa_chat_server-0.1.0/PKG-INFO +120 -0
- lupaxa_chat_server-0.1.0/README.md +56 -0
- lupaxa_chat_server-0.1.0/docs/README.md +23 -0
- lupaxa_chat_server-0.1.0/pyproject.toml +117 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/__init__.py +7 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/__main__.py +8 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/admin.py +291 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/channel.py +257 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/cli.py +97 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/client.py +62 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/colour.py +44 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/commands.py +837 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/commands_list.py +159 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/config.py +179 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/defaults.py +10 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/entry.py +189 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/exceptions.py +15 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/globals.py +34 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/help.py +132 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/login.py +151 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/parse.py +123 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/password.py +284 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/player.py +33 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/rooms.py +551 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/saved_player.py +366 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/server.py +682 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/ssh.py +245 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/su.py +337 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/time.py +95 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/timer.py +145 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/utilities.py +381 -0
- lupaxa_chat_server-0.1.0/src/lupaxa/chat_server/version.py +10 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# OS
|
|
2
|
+
.DS_Store
|
|
3
|
+
|
|
4
|
+
# Cursor
|
|
5
|
+
.cursor/
|
|
6
|
+
.superpowers/
|
|
7
|
+
.worktrees/
|
|
8
|
+
cursor-docs
|
|
9
|
+
|
|
10
|
+
# Make
|
|
11
|
+
.makefiles/
|
|
12
|
+
|
|
13
|
+
# MkDocs
|
|
14
|
+
site/
|
|
15
|
+
|
|
16
|
+
# Players
|
|
17
|
+
config/players/
|
|
18
|
+
|
|
19
|
+
# Python
|
|
20
|
+
.venv/
|
|
21
|
+
__pycache__/
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
dist/
|
|
26
|
+
build/
|
|
27
|
+
*.egg-info/
|
|
28
|
+
|
|
29
|
+
# SSH
|
|
30
|
+
config/ssh_host_key
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) The Lupaxa Project
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lupaxa-chat-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Multi-room TCP chat server.
|
|
5
|
+
Project-URL: Homepage, https://github.com/lupaxa-after-hours/chat-server
|
|
6
|
+
Project-URL: Documentation, https://chat-server.thelupaxaproject.org/
|
|
7
|
+
Project-URL: Repository, https://github.com/lupaxa-after-hours/chat-server
|
|
8
|
+
Project-URL: Issues, https://github.com/lupaxa-after-hours/chat-server/issues
|
|
9
|
+
Author: The Lupaxa Project
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) The Lupaxa Project
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENCE
|
|
32
|
+
Keywords: chat,server,tcp
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Environment :: Console
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
44
|
+
Classifier: Topic :: Communications :: Chat
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Requires-Dist: asyncssh>=2.14
|
|
47
|
+
Requires-Dist: bcrypt>=4.0
|
|
48
|
+
Requires-Dist: colorama>=0.4.6
|
|
49
|
+
Requires-Dist: pyyaml>=6.0
|
|
50
|
+
Provides-Extra: dev
|
|
51
|
+
Requires-Dist: bump-my-version>=1.2.0; extra == 'dev'
|
|
52
|
+
Requires-Dist: hatch>=1.12.0; extra == 'dev'
|
|
53
|
+
Requires-Dist: mkdocs-material==9.7.7; extra == 'dev'
|
|
54
|
+
Requires-Dist: mkdocs==1.6.1; extra == 'dev'
|
|
55
|
+
Requires-Dist: mypy>=1.12.0; extra == 'dev'
|
|
56
|
+
Requires-Dist: pip-audit>=2.7.0; extra == 'dev'
|
|
57
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
58
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
59
|
+
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
60
|
+
Provides-Extra: test
|
|
61
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'test'
|
|
62
|
+
Requires-Dist: pytest>=8.0; extra == 'test'
|
|
63
|
+
Description-Content-Type: text/markdown
|
|
64
|
+
|
|
65
|
+
<p align="center">
|
|
66
|
+
<a href="https://github.com/lupaxa-after-hours">
|
|
67
|
+
<img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/organisations/after-hours/readme-logo.png" alt="After Hours" />
|
|
68
|
+
</a>
|
|
69
|
+
</p>
|
|
70
|
+
|
|
71
|
+
<h1 align="center">Chat Server</h1>
|
|
72
|
+
|
|
73
|
+
Time for a little nostalgia, from the 1980s through the 2000s. Those years were full of
|
|
74
|
+
[talkers](https://en.wikipedia.org/wiki/Talker), [MUDs](https://en.wikipedia.org/wiki/Multi-user_dungeon) and
|
|
75
|
+
[MUSHes](https://en.wikipedia.org/wiki/MUSH): real-time chat and games, all of it text, reached by telnet or a
|
|
76
|
+
client made for the job. Most of them have long since left the internet, replaced by newer games and messaging.
|
|
77
|
+
|
|
78
|
+
This server is a throwback to that. The days we grew up online and spent playing these games.
|
|
79
|
+
|
|
80
|
+
It is a bare-bones chat system, and it works. Use it straight out of the box, or do what we used to do: tinker
|
|
81
|
+
with it, customise it, add to it, and make it your own.
|
|
82
|
+
|
|
83
|
+
A multi-room TCP chat server. Each player picks a name, starts in the
|
|
84
|
+
room named by `start`, and talks to the other people in that room.
|
|
85
|
+
|
|
86
|
+
## Install
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install lupaxa-chat-server
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Requires Python 3.10+. PyYAML, bcrypt, and AsyncSSH are the runtime
|
|
93
|
+
dependencies.
|
|
94
|
+
|
|
95
|
+
## Run
|
|
96
|
+
|
|
97
|
+
The server will not start without a config file. There is no default path.
|
|
98
|
+
From a checkout:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
chat-server -c config/config.yml
|
|
102
|
+
python -m lupaxa.chat_server -c config/config.yml
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The files under `config/` are one example. After
|
|
106
|
+
`pip install lupaxa-chat-server`, create the config, the rooms file, and
|
|
107
|
+
the help file yourself.
|
|
108
|
+
|
|
109
|
+
## Documentation
|
|
110
|
+
|
|
111
|
+
The site source is in `mkdocs/`.
|
|
112
|
+
|
|
113
|
+
- [Getting Started](mkdocs/getting-started.md) covers the config file and how to run.
|
|
114
|
+
- [Connect](mkdocs/usage/index.md), [Players](mkdocs/usage/players.md), [Rooms](mkdocs/usage/rooms.md), and [Commands](mkdocs/usage/commands.md) are how a player uses it.
|
|
115
|
+
- [Reference](mkdocs/reference.md) is the config keys, ranks, and public names.
|
|
116
|
+
- [Examples](mkdocs/examples.md) is a short telnet session and the SSH command.
|
|
117
|
+
|
|
118
|
+
<a href="https://github.com/the-lupaxa-project">
|
|
119
|
+
<img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/components/footer-for-child-orgs.svg" alt="The Lupaxa Project Footer" width="100%" />
|
|
120
|
+
</a>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/lupaxa-after-hours">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/organisations/after-hours/readme-logo.png" alt="After Hours" />
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<h1 align="center">Chat Server</h1>
|
|
8
|
+
|
|
9
|
+
Time for a little nostalgia, from the 1980s through the 2000s. Those years were full of
|
|
10
|
+
[talkers](https://en.wikipedia.org/wiki/Talker), [MUDs](https://en.wikipedia.org/wiki/Multi-user_dungeon) and
|
|
11
|
+
[MUSHes](https://en.wikipedia.org/wiki/MUSH): real-time chat and games, all of it text, reached by telnet or a
|
|
12
|
+
client made for the job. Most of them have long since left the internet, replaced by newer games and messaging.
|
|
13
|
+
|
|
14
|
+
This server is a throwback to that. The days we grew up online and spent playing these games.
|
|
15
|
+
|
|
16
|
+
It is a bare-bones chat system, and it works. Use it straight out of the box, or do what we used to do: tinker
|
|
17
|
+
with it, customise it, add to it, and make it your own.
|
|
18
|
+
|
|
19
|
+
A multi-room TCP chat server. Each player picks a name, starts in the
|
|
20
|
+
room named by `start`, and talks to the other people in that room.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install lupaxa-chat-server
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Requires Python 3.10+. PyYAML, bcrypt, and AsyncSSH are the runtime
|
|
29
|
+
dependencies.
|
|
30
|
+
|
|
31
|
+
## Run
|
|
32
|
+
|
|
33
|
+
The server will not start without a config file. There is no default path.
|
|
34
|
+
From a checkout:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
chat-server -c config/config.yml
|
|
38
|
+
python -m lupaxa.chat_server -c config/config.yml
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The files under `config/` are one example. After
|
|
42
|
+
`pip install lupaxa-chat-server`, create the config, the rooms file, and
|
|
43
|
+
the help file yourself.
|
|
44
|
+
|
|
45
|
+
## Documentation
|
|
46
|
+
|
|
47
|
+
The site source is in `mkdocs/`.
|
|
48
|
+
|
|
49
|
+
- [Getting Started](mkdocs/getting-started.md) covers the config file and how to run.
|
|
50
|
+
- [Connect](mkdocs/usage/index.md), [Players](mkdocs/usage/players.md), [Rooms](mkdocs/usage/rooms.md), and [Commands](mkdocs/usage/commands.md) are how a player uses it.
|
|
51
|
+
- [Reference](mkdocs/reference.md) is the config keys, ranks, and public names.
|
|
52
|
+
- [Examples](mkdocs/examples.md) is a short telnet session and the SSH command.
|
|
53
|
+
|
|
54
|
+
<a href="https://github.com/the-lupaxa-project">
|
|
55
|
+
<img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/components/footer-for-child-orgs.svg" alt="The Lupaxa Project Footer" width="100%" />
|
|
56
|
+
</a>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/lupaxa-after-hours">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/organisations/after-hours/readme-logo.png" alt="Organisation Logo" />
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<h1 align="center">Core Documentation</h1>
|
|
8
|
+
|
|
9
|
+
The following documents define the standards that apply across **The Lupaxa Project**:
|
|
10
|
+
|
|
11
|
+
| Document | Purpose |
|
|
12
|
+
| :------------------------------------ | :---------------------------------------------------------------- |
|
|
13
|
+
| [Code of Conduct](CODE_OF_CONDUCT.md) | Expected behaviour and reporting procedures. |
|
|
14
|
+
| [How to Contribute](CONTRIBUTING.md) | Development workflow, coding standards, and contribution process. |
|
|
15
|
+
| [Governance](GOVERNANCE.md) | Project governance, roles, and decision-making. |
|
|
16
|
+
| [Maintainers](MAINTAINERS.md) | Maintainer responsibilities and expectations. |
|
|
17
|
+
| [Projects Overview](PROJECTS.md) | Overview of organisations, repositories, and project structure. |
|
|
18
|
+
| [Security Policy](SECURITY.md) | Responsible disclosure and secure development practices. |
|
|
19
|
+
| [Support Guide](SUPPORT.md) | How to obtain help and support. |
|
|
20
|
+
|
|
21
|
+
<a href="https://github.com/the-lupaxa-project">
|
|
22
|
+
<img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/components/footer-for-child-orgs.svg" alt="The Lupaxa Project Footer" width="100%" />
|
|
23
|
+
</a>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.18"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lupaxa-chat-server"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Multi-room TCP chat server."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENCE" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"asyncssh>=2.14",
|
|
14
|
+
"bcrypt>=4.0",
|
|
15
|
+
"colorama>=0.4.6",
|
|
16
|
+
"pyyaml>=6.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
authors = [
|
|
20
|
+
{ name = "The Lupaxa Project" }
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
keywords = ["chat", "tcp", "server"]
|
|
24
|
+
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 3 - Alpha",
|
|
27
|
+
"Environment :: Console",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Programming Language :: Python :: 3.14",
|
|
37
|
+
"Topic :: Communications :: Chat",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/lupaxa-after-hours/chat-server"
|
|
42
|
+
Documentation = "https://chat-server.thelupaxaproject.org/"
|
|
43
|
+
Repository = "https://github.com/lupaxa-after-hours/chat-server"
|
|
44
|
+
Issues = "https://github.com/lupaxa-after-hours/chat-server/issues"
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
chat-server = "lupaxa.chat_server.cli:main"
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
dev = [
|
|
51
|
+
"pytest>=8.0",
|
|
52
|
+
"pytest-cov>=5.0",
|
|
53
|
+
"ruff>=0.6.0",
|
|
54
|
+
"mypy>=1.12.0",
|
|
55
|
+
"hatch>=1.12.0",
|
|
56
|
+
"bump-my-version>=1.2.0",
|
|
57
|
+
"pip-audit>=2.7.0",
|
|
58
|
+
"mkdocs==1.6.1",
|
|
59
|
+
"mkdocs-material==9.7.7",
|
|
60
|
+
]
|
|
61
|
+
test = [
|
|
62
|
+
"pytest>=8.0",
|
|
63
|
+
"pytest-cov>=5.0",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.hatch.version]
|
|
67
|
+
path = "src/lupaxa/chat_server/version.py"
|
|
68
|
+
|
|
69
|
+
[tool.hatch.build.targets.sdist]
|
|
70
|
+
include = [
|
|
71
|
+
"src/**",
|
|
72
|
+
"README.md",
|
|
73
|
+
"LICENCE",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.hatch.build.targets.wheel]
|
|
77
|
+
packages = ["src/lupaxa"]
|
|
78
|
+
|
|
79
|
+
[tool.ruff]
|
|
80
|
+
line-length = 100
|
|
81
|
+
target-version = "py310"
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint]
|
|
84
|
+
select = ["E", "F", "I", "B", "S", "UP", "SIM", "C4", "ISC", "D", "ANN", "PT"]
|
|
85
|
+
ignore = [
|
|
86
|
+
"D200", "D201", "D202", "D203", "D204", "D205", "D206", "D207",
|
|
87
|
+
"D208", "D209", "D210", "D211", "D212",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.ruff.lint.per-file-ignores]
|
|
91
|
+
"tests/**/*.py" = ["S101", "ANN001", "ANN201"]
|
|
92
|
+
"src/lupaxa/chat_server/server.py" = ["S104"]
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint.pydocstyle]
|
|
95
|
+
convention = "numpy"
|
|
96
|
+
|
|
97
|
+
[tool.ruff.format]
|
|
98
|
+
quote-style = "double"
|
|
99
|
+
|
|
100
|
+
[tool.pyright]
|
|
101
|
+
typeCheckingMode = "standard"
|
|
102
|
+
enableTypeIgnoreComments = true
|
|
103
|
+
pythonVersion = "3.10"
|
|
104
|
+
|
|
105
|
+
[tool.mypy]
|
|
106
|
+
python_version = "3.10"
|
|
107
|
+
files = ["src", "tests"]
|
|
108
|
+
mypy_path = "src"
|
|
109
|
+
explicit_package_bases = true
|
|
110
|
+
namespace_packages = true
|
|
111
|
+
warn_unused_configs = true
|
|
112
|
+
ignore_missing_imports = true
|
|
113
|
+
|
|
114
|
+
[tool.pytest.ini_options]
|
|
115
|
+
addopts = "-ra -q --disable-warnings"
|
|
116
|
+
testpaths = ["tests"]
|
|
117
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Commands only the admin can use."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
|
|
7
|
+
from .channel import broadcast_admin, broadcast_all, broadcast_staff, paint_admin, player_named
|
|
8
|
+
from .client import Client
|
|
9
|
+
from .colour import red
|
|
10
|
+
from .commands_list import COMMANDS, Command, Host
|
|
11
|
+
from .exceptions import ConfigError
|
|
12
|
+
from .globals import CANNOT_NOW
|
|
13
|
+
from .saved_player import (
|
|
14
|
+
RANK_ADMIN,
|
|
15
|
+
RANK_RESIDENT,
|
|
16
|
+
RANK_SUPER,
|
|
17
|
+
SavedPlayer,
|
|
18
|
+
rank_label,
|
|
19
|
+
save_player,
|
|
20
|
+
)
|
|
21
|
+
from .timer import shutdown_phrase
|
|
22
|
+
from .utilities import (
|
|
23
|
+
_is_admin,
|
|
24
|
+
_lookup,
|
|
25
|
+
_name_of,
|
|
26
|
+
center_block,
|
|
27
|
+
frame_width,
|
|
28
|
+
refuses_self,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def command_au(host: Host, client: Client, command: Command, text: str | None) -> None:
|
|
33
|
+
"""Speak on the admin channel."""
|
|
34
|
+
del command
|
|
35
|
+
_admin_line(host, client, f"You say '{text}'", f"{_name_of(client)} says '{text}'")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def command_ae(host: Host, client: Client, command: Command, text: str | None) -> None:
|
|
39
|
+
"""Emote on the admin channel."""
|
|
40
|
+
del command
|
|
41
|
+
_admin_line(host, client, f"You emote: {text}", f"{_name_of(client)} {text}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def command_at(host: Host, client: Client, command: Command, text: str | None) -> None:
|
|
45
|
+
"""Think on the admin channel."""
|
|
46
|
+
del command
|
|
47
|
+
_admin_line(
|
|
48
|
+
host,
|
|
49
|
+
client,
|
|
50
|
+
f"You think . o O ( {text} )",
|
|
51
|
+
f"{_name_of(client)} thinks . o O ( {text} )",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _admin_line(host: Host, client: Client, mine: str, theirs: str) -> None:
|
|
56
|
+
"""Send the sender ``mine`` and the rest of the admins ``theirs``."""
|
|
57
|
+
if not host.send(client, paint_admin(client, f"[ Admin ] {mine}\n")):
|
|
58
|
+
return
|
|
59
|
+
broadcast_admin(host, f"[ Admin ] {theirs}\n", skip=client)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def command_allow(host: Host, client: Client, command: Command, text: str | None) -> None:
|
|
63
|
+
"""Open or close newbies or residents. The config file stays as it was."""
|
|
64
|
+
del command
|
|
65
|
+
parts = (text or "").strip().split()
|
|
66
|
+
if len(parts) != 2 or parts[0] not in ("newbies", "residents"):
|
|
67
|
+
host.send(client, "Syntax: allow newbies|residents on|off\n")
|
|
68
|
+
return
|
|
69
|
+
if parts[1] not in ("on", "off"):
|
|
70
|
+
host.send(client, "Syntax: allow newbies|residents on|off\n")
|
|
71
|
+
return
|
|
72
|
+
opened = parts[1] == "on"
|
|
73
|
+
if parts[0] == "newbies":
|
|
74
|
+
host._allow_newbies = opened
|
|
75
|
+
else:
|
|
76
|
+
host._allow_residents = opened
|
|
77
|
+
verb = "opens" if opened else "closes"
|
|
78
|
+
who = _name_of(client)
|
|
79
|
+
broadcast_staff(host, f"< SU > {who} {verb} the talker to {parts[0]}.\n")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def command_broadcast(host: Host, client: Client, command: Command, text: str | None) -> None:
|
|
83
|
+
"""Send a centred broadcast message to every player who is in a room."""
|
|
84
|
+
del command
|
|
85
|
+
message = (text or "").strip()
|
|
86
|
+
if message == "":
|
|
87
|
+
host.send(client, "Syntax: broadcast <message>\n")
|
|
88
|
+
return
|
|
89
|
+
plain = f"-=> Broadcast Messages from {client.name}: {message} <=-"
|
|
90
|
+
for person in list(host._clients.values()):
|
|
91
|
+
if person.name is None or person.room is None:
|
|
92
|
+
continue
|
|
93
|
+
shown = red(plain) if person.player is not None and person.player.colour else plain
|
|
94
|
+
width = frame_width(person.discovered_columns, person.frame_columns)
|
|
95
|
+
padded = center_block(shown + "\n", width).removesuffix("\n")
|
|
96
|
+
host.send(person, f"\n\n\n{padded}\n\n\n\n")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def command_promote(host: Host, client: Client, command: Command, text: str | None) -> None:
|
|
100
|
+
"""Make a saved resident a super user, or a super user an admin.
|
|
101
|
+
|
|
102
|
+
Any admin may promote to super user. Only the config admin may promote to admin.
|
|
103
|
+
The name must match in full.
|
|
104
|
+
"""
|
|
105
|
+
del command
|
|
106
|
+
name = (text or "").strip()
|
|
107
|
+
if refuses_self(host, client, name):
|
|
108
|
+
return
|
|
109
|
+
if _is_admin(host, name):
|
|
110
|
+
host.send(client, f"{name} is the admin.\n")
|
|
111
|
+
return
|
|
112
|
+
found, saved = _lookup(host, client, name)
|
|
113
|
+
if not found:
|
|
114
|
+
return
|
|
115
|
+
if saved is None:
|
|
116
|
+
refusal = (
|
|
117
|
+
f"Player {name} not found.\n"
|
|
118
|
+
if player_named(host._clients.values(), name) is None
|
|
119
|
+
else f"{name} is not a resident.\n"
|
|
120
|
+
)
|
|
121
|
+
host.send(client, refusal)
|
|
122
|
+
return
|
|
123
|
+
if saved.rank == RANK_SUPER:
|
|
124
|
+
if not _is_admin(host, _name_of(client)):
|
|
125
|
+
host.send(
|
|
126
|
+
client,
|
|
127
|
+
f"Only the admin can promote {saved.name} to {rank_label(RANK_ADMIN)}.\n",
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
if _set_rank(host, client, saved, RANK_ADMIN):
|
|
131
|
+
label = rank_label(RANK_ADMIN)
|
|
132
|
+
broadcast_admin(
|
|
133
|
+
host,
|
|
134
|
+
f"[ Admin ] {_name_of(client)} promotes {saved.name} to {label}.\n",
|
|
135
|
+
)
|
|
136
|
+
return
|
|
137
|
+
if saved.rank == RANK_ADMIN:
|
|
138
|
+
host.send(client, f"{saved.name} is already an admin.\n")
|
|
139
|
+
return
|
|
140
|
+
if saved.rank != RANK_RESIDENT:
|
|
141
|
+
host.send(client, f"{name} is not a resident.\n")
|
|
142
|
+
return
|
|
143
|
+
if _set_rank(host, client, saved, RANK_SUPER):
|
|
144
|
+
broadcast_staff(
|
|
145
|
+
host,
|
|
146
|
+
f"< SU > {_name_of(client)} promotes {saved.name} to {rank_label(RANK_SUPER)}.\n",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def command_demote(host: Host, client: Client, command: Command, text: str | None) -> None:
|
|
151
|
+
"""Move an admin or super user down one rank. ``force`` drops them to resident.
|
|
152
|
+
|
|
153
|
+
Any admin may demote a super user. Only the config admin may demote an admin.
|
|
154
|
+
"""
|
|
155
|
+
parsed = _demote_args(text)
|
|
156
|
+
if parsed is None:
|
|
157
|
+
host.send(client, f"Syntax: {command.syntax}\n")
|
|
158
|
+
return
|
|
159
|
+
name, force = parsed
|
|
160
|
+
if refuses_self(host, client, name):
|
|
161
|
+
return
|
|
162
|
+
if _is_admin(host, name):
|
|
163
|
+
host.send(client, "The admin cannot be demoted.\n")
|
|
164
|
+
return
|
|
165
|
+
found, saved = _lookup(host, client, name)
|
|
166
|
+
if not found:
|
|
167
|
+
return
|
|
168
|
+
connected = player_named(host._clients.values(), name)
|
|
169
|
+
if saved is None and connected is None:
|
|
170
|
+
host.send(client, f"Player {name} not found.\n")
|
|
171
|
+
return
|
|
172
|
+
if saved is None or saved.rank not in (RANK_SUPER, RANK_ADMIN):
|
|
173
|
+
if saved is not None and saved.rank == RANK_RESIDENT:
|
|
174
|
+
host.send(client, f"{saved.name} is a resident.\n")
|
|
175
|
+
return
|
|
176
|
+
shown = connected.player.name if connected is not None and connected.player else name
|
|
177
|
+
host.send(client, f"{shown} cannot be demoted.\n")
|
|
178
|
+
return
|
|
179
|
+
if saved.rank == RANK_ADMIN and not _is_admin(host, _name_of(client)):
|
|
180
|
+
host.send(
|
|
181
|
+
client,
|
|
182
|
+
f"Only the admin can demote {saved.name} from {rank_label(RANK_ADMIN)}.\n",
|
|
183
|
+
)
|
|
184
|
+
return
|
|
185
|
+
if saved.rank == RANK_ADMIN and not force:
|
|
186
|
+
if _set_rank(host, client, saved, RANK_SUPER):
|
|
187
|
+
label = rank_label(RANK_ADMIN)
|
|
188
|
+
broadcast_admin(
|
|
189
|
+
host,
|
|
190
|
+
f"[ Admin ] {_name_of(client)} demotes {saved.name} from {label}.\n",
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
if not _set_rank(host, client, saved, RANK_RESIDENT):
|
|
194
|
+
return
|
|
195
|
+
if saved.rank == RANK_ADMIN:
|
|
196
|
+
line = f"{_name_of(client)} demotes {saved.name} from {rank_label(RANK_ADMIN)}.\n"
|
|
197
|
+
broadcast_admin(host, f"[ Admin ] {line}")
|
|
198
|
+
broadcast_staff(host, f"< SU > {line}")
|
|
199
|
+
return
|
|
200
|
+
broadcast_staff(
|
|
201
|
+
host,
|
|
202
|
+
f"< SU > {_name_of(client)} demotes {saved.name} from {rank_label(RANK_SUPER)}.\n",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _demote_args(text: str | None) -> tuple[str, bool] | None:
|
|
207
|
+
"""Return the name and whether ``force`` was set, or ``None`` when the words are wrong."""
|
|
208
|
+
parts = (text or "").strip().split()
|
|
209
|
+
if len(parts) == 1:
|
|
210
|
+
return parts[0], False
|
|
211
|
+
if len(parts) == 2 and parts[1].lower() == "force":
|
|
212
|
+
return parts[0], True
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def command_shutdown(host: Host, client: Client, command: Command, text: str | None) -> None:
|
|
217
|
+
"""Start, replace, cancel, or finish the one server countdown."""
|
|
218
|
+
parts = (text or "").strip().split(None, 1)
|
|
219
|
+
if len(parts) < 2 or parts[1].strip() == "":
|
|
220
|
+
host.send(client, f"Syntax: {command.syntax}\n")
|
|
221
|
+
return
|
|
222
|
+
word, reason = parts[0], parts[1].strip()
|
|
223
|
+
if word == "now":
|
|
224
|
+
host._shutdown_seconds = None
|
|
225
|
+
host._shutdown_reason = ""
|
|
226
|
+
broadcast_all(host, f"Shutting down now for '{reason}'.\n", skip=None)
|
|
227
|
+
host.shutdown()
|
|
228
|
+
return
|
|
229
|
+
if word == "cancel":
|
|
230
|
+
if host._shutdown_seconds is None:
|
|
231
|
+
host.send(client, "No shutdown is running.\n")
|
|
232
|
+
return
|
|
233
|
+
host._shutdown_seconds = None
|
|
234
|
+
host._shutdown_reason = ""
|
|
235
|
+
broadcast_all(host, f"Shutdown cancelled for '{reason}'.\n", skip=None)
|
|
236
|
+
return
|
|
237
|
+
if not word.isascii() or not word.isdigit() or int(word) < 1:
|
|
238
|
+
host.send(client, f"Syntax: {command.syntax}\n")
|
|
239
|
+
return
|
|
240
|
+
seconds = int(word)
|
|
241
|
+
host._shutdown_seconds = seconds
|
|
242
|
+
host._shutdown_reason = reason
|
|
243
|
+
broadcast_all(host, shutdown_phrase(seconds, reason), skip=None)
|
|
244
|
+
if seconds == 1:
|
|
245
|
+
broadcast_all(host, "Shutting down now!\n", skip=None)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _set_rank(host: Host, client: Client, saved: SavedPlayer, rank: str) -> bool:
|
|
249
|
+
"""Write ``rank`` to the file of ``saved`` and to the live user, when connected."""
|
|
250
|
+
try:
|
|
251
|
+
save_player(host._roster_dir, replace(saved, rank=rank))
|
|
252
|
+
except (ConfigError, OSError) as exc:
|
|
253
|
+
print(exc)
|
|
254
|
+
host.send(client, CANNOT_NOW)
|
|
255
|
+
return False
|
|
256
|
+
target = player_named(host._clients.values(), saved.name)
|
|
257
|
+
if target is not None and target.player is not None:
|
|
258
|
+
target.player.rank = rank
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
COMMANDS.extend(
|
|
263
|
+
(
|
|
264
|
+
Command(
|
|
265
|
+
"allow",
|
|
266
|
+
command_allow,
|
|
267
|
+
False,
|
|
268
|
+
"allow newbies|residents on|off",
|
|
269
|
+
rank=RANK_ADMIN,
|
|
270
|
+
),
|
|
271
|
+
Command("broadcast", command_broadcast, False, "broadcast <message>", rank=RANK_ADMIN),
|
|
272
|
+
Command("promote", command_promote, False, "promote <name>", rank=RANK_ADMIN),
|
|
273
|
+
Command(
|
|
274
|
+
"demote",
|
|
275
|
+
command_demote,
|
|
276
|
+
False,
|
|
277
|
+
"demote <name>|demote <name> force",
|
|
278
|
+
rank=RANK_ADMIN,
|
|
279
|
+
),
|
|
280
|
+
Command("ae", command_ae, False, "ae <message>", rank=RANK_ADMIN),
|
|
281
|
+
Command("at", command_at, False, "at <message>", rank=RANK_ADMIN),
|
|
282
|
+
Command("au", command_au, False, "au <message>", rank=RANK_ADMIN),
|
|
283
|
+
Command(
|
|
284
|
+
"shutdown",
|
|
285
|
+
command_shutdown,
|
|
286
|
+
False,
|
|
287
|
+
"shutdown <seconds> <reason>",
|
|
288
|
+
rank=RANK_ADMIN,
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
)
|