endstone-clans-api 1.0.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.
- endstone_clans_api-1.0.0/.github/workflows/python-publish.yml +70 -0
- endstone_clans_api-1.0.0/.gitignore +3 -0
- endstone_clans_api-1.0.0/PKG-INFO +8 -0
- endstone_clans_api-1.0.0/README.md +68 -0
- endstone_clans_api-1.0.0/pyproject.toml +23 -0
- endstone_clans_api-1.0.0/src/endstone_clans_api/__init__.py +53 -0
- endstone_clans_api-1.0.0/src/endstone_clans_api/api.py +29 -0
- endstone_clans_api-1.0.0/src/endstone_clans_api/commands.py +430 -0
- endstone_clans_api-1.0.0/src/endstone_clans_api/database.py +247 -0
- endstone_clans_api-1.0.0/src/endstone_clans_api/etc.py +22 -0
- endstone_clans_api-1.0.0/src/endstone_clans_api/events.py +136 -0
- endstone_clans_api-1.0.0/src/endstone_clans_api/main.py +194 -0
- endstone_clans_api-1.0.0/src/endstone_clans_api/py.typed +0 -0
- endstone_clans_api-1.0.0/src/endstone_clans_api/types.py +85 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# This workflow will upload a Python Package to PyPI when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
5
|
+
# They are provided by a third-party and are governed by
|
|
6
|
+
# separate terms of service, privacy policy, and support
|
|
7
|
+
# documentation.
|
|
8
|
+
|
|
9
|
+
name: Upload Python Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
release-build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.x"
|
|
28
|
+
|
|
29
|
+
- name: Build release distributions
|
|
30
|
+
run: |
|
|
31
|
+
# NOTE: put your own distribution build steps here.
|
|
32
|
+
python -m pip install build
|
|
33
|
+
python -m build
|
|
34
|
+
|
|
35
|
+
- name: Upload distributions
|
|
36
|
+
uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: release-dists
|
|
39
|
+
path: dist/
|
|
40
|
+
|
|
41
|
+
pypi-publish:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
needs:
|
|
44
|
+
- release-build
|
|
45
|
+
permissions:
|
|
46
|
+
# IMPORTANT: this permission is mandatory for trusted publishing
|
|
47
|
+
id-token: write
|
|
48
|
+
|
|
49
|
+
# Dedicated environments with protections for publishing are strongly recommended.
|
|
50
|
+
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
|
|
51
|
+
environment:
|
|
52
|
+
name: pypi
|
|
53
|
+
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
|
|
54
|
+
# url: https://pypi.org/p/YOURPROJECT
|
|
55
|
+
#
|
|
56
|
+
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
|
|
57
|
+
# ALTERNATIVE: exactly, uncomment the following line instead:
|
|
58
|
+
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
|
|
59
|
+
|
|
60
|
+
steps:
|
|
61
|
+
- name: Retrieve release distributions
|
|
62
|
+
uses: actions/download-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: release-dists
|
|
65
|
+
path: dist/
|
|
66
|
+
|
|
67
|
+
- name: Publish release distributions to PyPI
|
|
68
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
69
|
+
with:
|
|
70
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Endstone Clans API
|
|
4
|
+
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
A plugin that manages clans (or teams, if you will). This is meant to be used in conjunction with one of your own plugins, so using it on its own isn't recommended (this genuinely does not do anything without it).
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
> - Portable SQLite database
|
|
12
|
+
> You can test this plugin in staging, and bring the `clans.db` file to production. While it's not recommended to modify the database while it's still running, you can do it and most of the time you'll be fine.
|
|
13
|
+
|
|
14
|
+
> - User-facing commands that let them manage their own clans
|
|
15
|
+
> Users may create clans, rename their own clans, whatever. They must stay in only one clan, though.
|
|
16
|
+
|
|
17
|
+
> - API that _respects_ your language server
|
|
18
|
+
> No more blindly throwing methods, `getattr`s, and `setattr`s at an `Any` type like in certain other APIs. As long as you import `get_clans_api` and use that, then your language server gets to have all the information to do things like full autocomplete and typehints.
|
|
19
|
+
|
|
20
|
+
> - Event system
|
|
21
|
+
> You can cancel events, do whatever. You can even use this to do things like replace the invite UI with your own.
|
|
22
|
+
|
|
23
|
+
> - Extensive config
|
|
24
|
+
> The config lets you configure a lot.
|
|
25
|
+
>
|
|
26
|
+
> Every single piece of front-end facing text translatable through the config.
|
|
27
|
+
|
|
28
|
+
## Documentation
|
|
29
|
+
|
|
30
|
+
> [!IMPORTANT]
|
|
31
|
+
> Always use this by installing the plugin into your environment, not putting it into the server's `plugins/` directory as a `.whl`. Installing will give you the typehints that you're meant to have.
|
|
32
|
+
|
|
33
|
+
### Quick Start
|
|
34
|
+
```python
|
|
35
|
+
from endstone_clans_api import (
|
|
36
|
+
get_clans_api,
|
|
37
|
+
ClanCreateEvent,
|
|
38
|
+
clan_event_handler,
|
|
39
|
+
ClansApi
|
|
40
|
+
)
|
|
41
|
+
from endstone.plugin import Plugin
|
|
42
|
+
|
|
43
|
+
class ExamplePlugin(Plugin):
|
|
44
|
+
def on_enable(self):
|
|
45
|
+
# This will get the plugin, and then the plugin's API.
|
|
46
|
+
api: ClansApi | None = get_clans_api(self.server.plugin_manager)
|
|
47
|
+
|
|
48
|
+
# Since we're doing everything right, there is little-to-no reason that
|
|
49
|
+
# this should return None.
|
|
50
|
+
assert api, "`get_clans_api` returned `None`"
|
|
51
|
+
self.api: ClansApi = api
|
|
52
|
+
|
|
53
|
+
# Just like Endstone's register_events, you can make a sperate listener
|
|
54
|
+
# class in a different module to make everything cleaner.
|
|
55
|
+
self.api.register_events(self)
|
|
56
|
+
|
|
57
|
+
@clan_event_handler
|
|
58
|
+
def on_clan_create(self, event: ClanCreateEvent):
|
|
59
|
+
# Simple test log so you can see that the plugin's API is funcitonal.
|
|
60
|
+
self.logger.info(f"Event: ClanCreateEvent - Name: {event.clan.display_name}, Creator: {event.creator.name}")
|
|
61
|
+
|
|
62
|
+
# Let's say that we wanted to ban spaces from clan names. Just handle it
|
|
63
|
+
# here.
|
|
64
|
+
if " " in event.clan.display_name:
|
|
65
|
+
event.cancel()
|
|
66
|
+
event.creator.send_error_message("Clan names cannot have spaces in them!")
|
|
67
|
+
```
|
|
68
|
+
I think you've noticed by now that this feels really native to Endstone. Feels like we're re-implementing their design, and that's a good thing--Endstone's design is *exceptional*.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "endstone-clans-api"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = ""
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "niko-at-chalupa"},
|
|
11
|
+
]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"endstone",
|
|
14
|
+
"pydantic",
|
|
15
|
+
"ruamel.yaml",
|
|
16
|
+
"xxhash",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.entry-points."endstone"]
|
|
20
|
+
clans-api = "endstone_clans_api:ClansApiPlugin"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/endstone_clans_api"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from endstone.plugin import PluginManager
|
|
2
|
+
from .main import ClansApiPlugin
|
|
3
|
+
from .types import Clan
|
|
4
|
+
from .database import Database as ClansDatabase
|
|
5
|
+
from .api import ClansApi
|
|
6
|
+
from .events import (
|
|
7
|
+
ClanEvent,
|
|
8
|
+
ClanCancellableEvent,
|
|
9
|
+
ClanCreateEvent,
|
|
10
|
+
ClanDeleteEvent,
|
|
11
|
+
ClanJoinEvent,
|
|
12
|
+
ClanLeaveEvent,
|
|
13
|
+
ClanKickEvent,
|
|
14
|
+
ClanRenameEvent,
|
|
15
|
+
ClanInviteEvent,
|
|
16
|
+
clan_event_handler,
|
|
17
|
+
)
|
|
18
|
+
from typing import cast
|
|
19
|
+
|
|
20
|
+
def get_clans_api(plugin_manager: PluginManager) -> ClansApi | None:
|
|
21
|
+
"""
|
|
22
|
+
Get the ClansApi object the plugin uses. Will return None if an error occours.
|
|
23
|
+
|
|
24
|
+
## Note: Make sure you call this in on_enable or something similar. Internally, what this returns (ClansApiPlugin._api) is unset until the plugin's on_load() method gets called by the PluginManager
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
plugin = cast(ClansApiPlugin, plugin_manager.get_plugin("ClansApiPlugin"))
|
|
28
|
+
# I don't trust how it returns "Plugin." For all I know, this could return
|
|
29
|
+
# Plugin | None, just like get_player.
|
|
30
|
+
if not plugin:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return plugin.api
|
|
34
|
+
except Exception:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ClansApiPlugin",
|
|
39
|
+
"Clan",
|
|
40
|
+
"ClansDatabase",
|
|
41
|
+
"ClansApi",
|
|
42
|
+
"get_clans_api",
|
|
43
|
+
"ClanEvent",
|
|
44
|
+
"ClanCancellableEvent",
|
|
45
|
+
"ClanCreateEvent",
|
|
46
|
+
"ClanDeleteEvent",
|
|
47
|
+
"ClanJoinEvent",
|
|
48
|
+
"ClanLeaveEvent",
|
|
49
|
+
"ClanKickEvent",
|
|
50
|
+
"ClanRenameEvent",
|
|
51
|
+
"ClanInviteEvent",
|
|
52
|
+
"clan_event_handler",
|
|
53
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .types import Clan
|
|
2
|
+
from .database import Database
|
|
3
|
+
from .events import ClanEventManager, ClanEvent
|
|
4
|
+
from endstone.plugin import PluginManager
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .main import ClansApiPlugin
|
|
9
|
+
|
|
10
|
+
class ClansApi:
|
|
11
|
+
# I _should_ go for a service, but it's confusing, and services do
|
|
12
|
+
# not provide type hints for the end user's langage server so it's
|
|
13
|
+
# better if I provide it as a function somewhere.
|
|
14
|
+
|
|
15
|
+
def __init__(self, clans_api_plugin: 'ClansApiPlugin'):
|
|
16
|
+
self.plugin = clans_api_plugin
|
|
17
|
+
self._event_manager = ClanEventManager(clans_api_plugin)
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def db(self) -> Database:
|
|
21
|
+
return self.plugin.db
|
|
22
|
+
|
|
23
|
+
def register_events(self, listener: Any) -> None:
|
|
24
|
+
"""Registers clan event handlers."""
|
|
25
|
+
self._event_manager.register_events(listener)
|
|
26
|
+
|
|
27
|
+
def call_event(self, event: ClanEvent) -> None:
|
|
28
|
+
"""Calls clan event handlers."""
|
|
29
|
+
self._event_manager.call_event(event)
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
from endstone import Logger
|
|
2
|
+
from endstone.scheduler import Scheduler
|
|
3
|
+
from endstone.form import MessageForm, ModalForm, Toggle
|
|
4
|
+
from .database import Database
|
|
5
|
+
from abc import ABC
|
|
6
|
+
from typing import TYPE_CHECKING, Callable, TypeVar
|
|
7
|
+
from endstone.command import CommandSender, Command
|
|
8
|
+
from endstone.asyncio import submit
|
|
9
|
+
from endstone import Player
|
|
10
|
+
from typing import Awaitable
|
|
11
|
+
import concurrent.futures
|
|
12
|
+
import time
|
|
13
|
+
import json
|
|
14
|
+
from .events import (
|
|
15
|
+
ClanCreateEvent,
|
|
16
|
+
ClanDeleteEvent,
|
|
17
|
+
ClanJoinEvent,
|
|
18
|
+
ClanLeaveEvent,
|
|
19
|
+
ClanKickEvent,
|
|
20
|
+
ClanRenameEvent,
|
|
21
|
+
ClanInviteEvent,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from .main import ClansConfig, ClansApiPlugin
|
|
26
|
+
from .api import ClansApi
|
|
27
|
+
|
|
28
|
+
_T = TypeVar("_T")
|
|
29
|
+
|
|
30
|
+
class Subcommands(ABC):
|
|
31
|
+
# def a_function(self, sender: CommandSender, command: Command, args: list[str]) -> bool: ...
|
|
32
|
+
# { "a": self.a_function }
|
|
33
|
+
# ^ this is what we're asking for (the dict)
|
|
34
|
+
# This should also accept unbound methods (methods that aren't object.method, but rather method(object)), but
|
|
35
|
+
# we won't be using unbound methods anyways so it doesn't matter
|
|
36
|
+
subcommand_map: dict[str, Callable[[CommandSender, Command, list[str]], bool]]
|
|
37
|
+
plugin: 'ClansApiPlugin'
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def config(self) -> 'ClansConfig':
|
|
41
|
+
return self.plugin.config
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def messages(self) -> dict[str, str]:
|
|
45
|
+
return self.plugin.config.messages
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def db(self) -> Database:
|
|
49
|
+
return self.plugin.db
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def scheduler(self) -> Scheduler:
|
|
53
|
+
return self.plugin.server.scheduler
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def logger(self) -> Logger:
|
|
57
|
+
return self.plugin.logger
|
|
58
|
+
|
|
59
|
+
def _handle_future_result(self, future) -> None:
|
|
60
|
+
try:
|
|
61
|
+
future.result()
|
|
62
|
+
except Exception as e:
|
|
63
|
+
self.logger.error(str(e))
|
|
64
|
+
|
|
65
|
+
def _submit_and_handle_future_result(self, coro: Awaitable[_T]) -> concurrent.futures.Future[_T]:
|
|
66
|
+
future = submit(coro)
|
|
67
|
+
future.add_done_callback(self._handle_future_result)
|
|
68
|
+
return future
|
|
69
|
+
|
|
70
|
+
class ClansCommands(Subcommands):
|
|
71
|
+
def help(self, sender: CommandSender, command: Command, args: list[str]):
|
|
72
|
+
help_messages = self.plugin.config.help
|
|
73
|
+
|
|
74
|
+
sender.send_message(self.messages.get("help_header", ""))
|
|
75
|
+
for subcommand in self.subcommand_map:
|
|
76
|
+
if subcommand in help_messages:
|
|
77
|
+
description = help_messages.get(subcommand)
|
|
78
|
+
else:
|
|
79
|
+
description = "[no description]"
|
|
80
|
+
sender.send_message(f"{subcommand} - {description}")
|
|
81
|
+
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def create(self, sender: CommandSender, command: Command, args: list[str]):
|
|
85
|
+
if not isinstance(sender, Player):
|
|
86
|
+
sender.send_error_message(self.messages.get("not_a_player", "Only players can use this command."))
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
if len(args) == 0:
|
|
90
|
+
sender.send_error_message(self.messages.get("usage_create", "Usage: /clan create <name: str>"))
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
clan_name = " ".join(args)
|
|
94
|
+
|
|
95
|
+
async def create_task():
|
|
96
|
+
try:
|
|
97
|
+
xuid = int(sender.xuid)
|
|
98
|
+
member_clan = self.db.get_member_clan(xuid)
|
|
99
|
+
if member_clan:
|
|
100
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_error_message(self.messages.get("already_in_clan", "already in clan")))
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
from .types import _Clan
|
|
104
|
+
temp_clan = _Clan(self.plugin, clan_name, xuid)
|
|
105
|
+
event = ClanCreateEvent(temp_clan, sender)
|
|
106
|
+
self.api.call_event(event)
|
|
107
|
+
if event.cancelled:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
self.db.create_clan(clan_name, xuid)
|
|
112
|
+
msg = self.messages.get("clan_created", "clan created")
|
|
113
|
+
msg = msg.replace("[clan_name]", clan_name)
|
|
114
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_message(msg))
|
|
115
|
+
except RuntimeError as e:
|
|
116
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_error_message(str(e)))
|
|
117
|
+
except Exception as e:
|
|
118
|
+
# What is the point
|
|
119
|
+
raise e
|
|
120
|
+
|
|
121
|
+
self._submit_and_handle_future_result(create_task())
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
def rename(self, sender: CommandSender, command: Command, args: list[str]):
|
|
125
|
+
if not isinstance(sender, Player):
|
|
126
|
+
sender.send_error_message(self.messages.get("not_a_player", "Only players can use this command."))
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
if len(args) == 0:
|
|
130
|
+
sender.send_error_message(self.messages.get("usage_create", "Usage: /clan rename <name: str>"))
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
player = sender
|
|
134
|
+
|
|
135
|
+
async def rename_task():
|
|
136
|
+
try:
|
|
137
|
+
xuid = int(player.xuid)
|
|
138
|
+
clan = self.db.get_member_clan(xuid)
|
|
139
|
+
|
|
140
|
+
if not clan:
|
|
141
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_error_message(self.messages.get("not_in_clan", "not in clan")))
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
if clan.owner_xuid != xuid:
|
|
145
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_error_message(self.messages.get("not_the_owner", "not the owner")))
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
new_name = " ".join(args)
|
|
150
|
+
|
|
151
|
+
event = ClanRenameEvent(clan, clan.display_name, new_name)
|
|
152
|
+
self.api.call_event(event)
|
|
153
|
+
if event.cancelled:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
self.db.rename_clan(xuid, new_name)
|
|
157
|
+
msg = self.messages.get("clan_renamed", "clan renamed to [clan_name]")
|
|
158
|
+
msg = msg.replace("[clan_name]", new_name)
|
|
159
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_message(msg))
|
|
160
|
+
except RuntimeError as e:
|
|
161
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_error_message(str(e)))
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
raise e
|
|
165
|
+
|
|
166
|
+
self._submit_and_handle_future_result(rename_task())
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
def config_command(self, sender: CommandSender, command: Command, args: list[str]):
|
|
170
|
+
if not isinstance(sender, Player):
|
|
171
|
+
sender.send_error_message(self.messages.get("not_a_player", "Only players can use this command."))
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
player = sender
|
|
175
|
+
|
|
176
|
+
async def config_task():
|
|
177
|
+
xuid = int(player.xuid)
|
|
178
|
+
# Default to "true" if not set
|
|
179
|
+
allow_invites_str = self.db.get_player_preference(xuid, "allow_invites")
|
|
180
|
+
allow_invites = allow_invites_str.lower() == "true" if allow_invites_str else True
|
|
181
|
+
|
|
182
|
+
def on_form_submit(p: Player, data_json: str):
|
|
183
|
+
try:
|
|
184
|
+
data = json.loads(data_json)
|
|
185
|
+
new_allow_invites = data[0]
|
|
186
|
+
|
|
187
|
+
async def save_task():
|
|
188
|
+
self.db.set_player_preference(int(p.xuid), "allow_invites", str(new_allow_invites).lower())
|
|
189
|
+
self.scheduler.run_task(self.plugin, lambda: p.send_message(self.plugin.config.config_form.get("success", "Preferences updated!")))
|
|
190
|
+
|
|
191
|
+
submit(save_task())
|
|
192
|
+
except Exception as e:
|
|
193
|
+
self.logger.error(f"Error handling config form submission: {e}")
|
|
194
|
+
|
|
195
|
+
form = ModalForm(
|
|
196
|
+
title=self.plugin.config.config_form.get("title", "Clan Preferences"),
|
|
197
|
+
on_submit=on_form_submit
|
|
198
|
+
)
|
|
199
|
+
form.add_control(Toggle(
|
|
200
|
+
label=self.plugin.config.config_form.get("allow_invites", "Allow Clan Invitations"),
|
|
201
|
+
default_value=allow_invites
|
|
202
|
+
))
|
|
203
|
+
|
|
204
|
+
self.scheduler.run_task(self.plugin, lambda: player.send_form(form))
|
|
205
|
+
|
|
206
|
+
submit(config_task())
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
def invite(self, sender: CommandSender, command: Command, args: list[str]):
|
|
210
|
+
if not isinstance(sender, Player):
|
|
211
|
+
sender.send_error_message(self.messages.get("not_a_player", "Only players can use this command."))
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
if len(args) == 0:
|
|
215
|
+
sender.send_error_message(self.messages.get("usage_invite", "Usage: /clan invite <player: player>"))
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
target_name = args[0]
|
|
219
|
+
target = self.plugin.server.get_player(target_name)
|
|
220
|
+
if not target:
|
|
221
|
+
msg = self.messages.get("player_not_found", "Player [player_name] not found.")
|
|
222
|
+
msg = msg.replace("[player_name]", target_name)
|
|
223
|
+
sender.send_error_message(msg)
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
player = sender
|
|
227
|
+
|
|
228
|
+
async def invite_task():
|
|
229
|
+
try:
|
|
230
|
+
xuid = int(player.xuid)
|
|
231
|
+
clan = self.db.get_member_clan(xuid)
|
|
232
|
+
|
|
233
|
+
if not clan:
|
|
234
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_error_message(self.messages.get("not_in_clan", "not in clan")))
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
if clan.owner_xuid != xuid:
|
|
238
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_error_message(self.messages.get("not_the_owner", "not the owner")))
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
target_xuid = int(target.xuid)
|
|
242
|
+
target_clan = self.db.get_member_clan(target_xuid)
|
|
243
|
+
if target_clan:
|
|
244
|
+
msg = self.messages.get("player_already_in_clan", "[player_name] is already in a clan.")
|
|
245
|
+
msg = msg.replace("[player_name]", target.name)
|
|
246
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_error_message(msg))
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
allow_invites_str = self.db.get_player_preference(target_xuid, "allow_invites")
|
|
250
|
+
allow_invites = allow_invites_str.lower() == "true" if allow_invites_str else True
|
|
251
|
+
if not allow_invites:
|
|
252
|
+
msg = self.messages.get("privacy_no_invites", "[player_name] does not allow clan invitations.")
|
|
253
|
+
msg = msg.replace("[player_name]", target.name)
|
|
254
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_error_message(msg))
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
event = ClanInviteEvent(clan, player, target)
|
|
258
|
+
self.api.call_event(event)
|
|
259
|
+
if event.cancelled:
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
cooldown_key = (str(xuid), str(target_xuid))
|
|
263
|
+
now = time.time()
|
|
264
|
+
if cooldown_key in self.plugin.invite_cooldowns:
|
|
265
|
+
if now - self.plugin.invite_cooldowns[cooldown_key] < self.plugin.config.invite_cooldown:
|
|
266
|
+
msg = self.messages.get("invite_cooldown", "You must wait before inviting [player_name] again.")
|
|
267
|
+
msg = msg.replace("[player_name]", target.name)
|
|
268
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_error_message(msg))
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
def on_form_submit(p: Player, index: int):
|
|
272
|
+
if index == 0:
|
|
273
|
+
async def accept_task():
|
|
274
|
+
if self.db.get_member_clan(int(p.xuid)):
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
join_event = ClanJoinEvent(clan, p)
|
|
278
|
+
self.api.call_event(join_event)
|
|
279
|
+
if join_event.cancelled:
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
self.db.add_member(clan.owner_xuid, int(p.xuid))
|
|
283
|
+
|
|
284
|
+
def finalize_acceptance():
|
|
285
|
+
msg = self.messages.get("invite_accepted", "You have joined [clan_name]!")
|
|
286
|
+
msg = msg.replace("[clan_name]", clan.display_name)
|
|
287
|
+
p.send_message(msg)
|
|
288
|
+
|
|
289
|
+
inviter = self.plugin.server.get_player(player.name)
|
|
290
|
+
if inviter:
|
|
291
|
+
inviter.send_message(f"{p.name} joined your clan.")
|
|
292
|
+
|
|
293
|
+
self.scheduler.run_task(self.plugin, finalize_acceptance)
|
|
294
|
+
|
|
295
|
+
submit(accept_task())
|
|
296
|
+
else:
|
|
297
|
+
self.plugin.invite_cooldowns[cooldown_key] = time.time()
|
|
298
|
+
|
|
299
|
+
def notify_decline():
|
|
300
|
+
msg = self.messages.get("invite_declined", "[player_name] declined your invitation.")
|
|
301
|
+
msg = msg.replace("[player_name]", p.name)
|
|
302
|
+
inviter = self.plugin.server.get_player(player.name)
|
|
303
|
+
if inviter:
|
|
304
|
+
inviter.send_message(msg)
|
|
305
|
+
|
|
306
|
+
self.scheduler.run_task(self.plugin, notify_decline)
|
|
307
|
+
|
|
308
|
+
form = MessageForm(
|
|
309
|
+
title=self.messages.get("invite_received_title", "Clan Invitation"),
|
|
310
|
+
content=self.messages.get("invite_received_content", "[player_name] invited you to join [clan_name].")
|
|
311
|
+
.replace("[player_name]", player.name)
|
|
312
|
+
.replace("[clan_name]", clan.display_name),
|
|
313
|
+
button1=self.messages.get("invite_yes", "Yes"),
|
|
314
|
+
button2=self.messages.get("invite_no", "No"),
|
|
315
|
+
on_submit=on_form_submit
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
self.scheduler.run_task(self.plugin, lambda: target.send_form(form))
|
|
319
|
+
|
|
320
|
+
msg = self.messages.get("invite_sent", "Invitation sent to [player_name].")
|
|
321
|
+
msg = msg.replace("[player_name]", target.name)
|
|
322
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_message(msg))
|
|
323
|
+
|
|
324
|
+
except Exception as e:
|
|
325
|
+
raise e
|
|
326
|
+
|
|
327
|
+
self._submit_and_handle_future_result(invite_task())
|
|
328
|
+
return True
|
|
329
|
+
|
|
330
|
+
def kick(self, sender: CommandSender, command: Command, args: list[str]):
|
|
331
|
+
if not isinstance(sender, Player):
|
|
332
|
+
sender.send_error_message(self.messages.get("not_a_player", "Only players can use this command."))
|
|
333
|
+
return True
|
|
334
|
+
|
|
335
|
+
if len(args) == 0:
|
|
336
|
+
sender.send_error_message(self.messages.get("usage_kick", "Usage: /clan kick <player: player>"))
|
|
337
|
+
return True
|
|
338
|
+
|
|
339
|
+
target_name = args[0]
|
|
340
|
+
target = self.plugin.server.get_player(target_name)
|
|
341
|
+
if not target:
|
|
342
|
+
msg = self.messages.get("player_not_found", "Player [player_name] not found.")
|
|
343
|
+
msg = msg.replace("[player_name]", target_name)
|
|
344
|
+
sender.send_error_message(msg)
|
|
345
|
+
return True
|
|
346
|
+
|
|
347
|
+
async def kick_task():
|
|
348
|
+
xuid = int(sender.xuid)
|
|
349
|
+
clan = self.db.get_clan_by_xuid(xuid)
|
|
350
|
+
target_xuid = int(target.xuid)
|
|
351
|
+
|
|
352
|
+
if not clan or target_xuid not in clan.members_xuids or xuid == target_xuid:
|
|
353
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_error_message(self.messages.get("cannot_kick", "Cannot kick this player.")))
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
event = ClanKickEvent(clan, target, sender)
|
|
357
|
+
self.api.call_event(event)
|
|
358
|
+
if event.cancelled:
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
self.db.remove_member(xuid, target_xuid)
|
|
362
|
+
|
|
363
|
+
def notify():
|
|
364
|
+
msg = self.messages.get("player_kicked", "Kicked [player_name].").replace("[player_name]", target.name)
|
|
365
|
+
sender.send_message(msg)
|
|
366
|
+
target.send_message(self.messages.get("you_were_kicked", "You have been kicked from the clan."))
|
|
367
|
+
|
|
368
|
+
self.scheduler.run_task(self.plugin, notify)
|
|
369
|
+
|
|
370
|
+
self._submit_and_handle_future_result(kick_task())
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
def leave(self, sender: CommandSender, command: Command, args: list[str]):
|
|
374
|
+
if not isinstance(sender, Player):
|
|
375
|
+
sender.send_error_message(self.messages.get("not_a_player", "Only players can use this command."))
|
|
376
|
+
return True
|
|
377
|
+
|
|
378
|
+
async def leave_task():
|
|
379
|
+
try:
|
|
380
|
+
xuid = int(sender.xuid)
|
|
381
|
+
clan = self.db.get_member_clan(xuid)
|
|
382
|
+
|
|
383
|
+
if not clan:
|
|
384
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_error_message(self.messages.get("not_in_clan", "not in clan")))
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
if clan.owner_xuid == xuid:
|
|
388
|
+
event = ClanDeleteEvent(clan)
|
|
389
|
+
self.api.call_event(event)
|
|
390
|
+
if event.cancelled:
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
self.db.delete_clan(xuid)
|
|
394
|
+
msg = self.messages.get("clan_disbanded", "clan disbanded")
|
|
395
|
+
msg = msg.replace("[clan_name]", clan.display_name)
|
|
396
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_message(msg))
|
|
397
|
+
else:
|
|
398
|
+
event = ClanLeaveEvent(clan, sender)
|
|
399
|
+
self.api.call_event(event)
|
|
400
|
+
if event.cancelled:
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
self.db.remove_member(clan.owner_xuid, xuid)
|
|
404
|
+
msg = self.messages.get("clan_left", "clan disbanded")
|
|
405
|
+
msg = msg.replace("[clan_name]", clan.display_name)
|
|
406
|
+
self.scheduler.run_task(self.plugin, lambda: sender.send_message(msg))
|
|
407
|
+
except Exception as e:
|
|
408
|
+
raise e
|
|
409
|
+
|
|
410
|
+
self._submit_and_handle_future_result(leave_task())
|
|
411
|
+
return True
|
|
412
|
+
|
|
413
|
+
def __init__(self, plugin: 'ClansApiPlugin'):
|
|
414
|
+
self.plugin = plugin
|
|
415
|
+
|
|
416
|
+
self.subcommand_map = {
|
|
417
|
+
"help": self.help,
|
|
418
|
+
"create": self.create,
|
|
419
|
+
"rename": self.rename,
|
|
420
|
+
"config": self.config_command,
|
|
421
|
+
"invite": self.invite,
|
|
422
|
+
"kick": self.kick,
|
|
423
|
+
"leave": self.leave,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
@property
|
|
427
|
+
def api(self) -> 'ClansApi':
|
|
428
|
+
api = self.plugin.api
|
|
429
|
+
assert api
|
|
430
|
+
return api
|