Skip to content

Commit eead081

Browse files
author
karel26
committed
ENHANCEMENTS:
- Cloud: server registration added for new /serverlist command (WIP) CHANGES: - Dropped Python 3.9 support
1 parent 5153101 commit eead081

File tree

9 files changed

+116
-19
lines changed

9 files changed

+116
-19
lines changed

core/utils/discord.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,13 @@ class PlayerType(Enum):
104104
HISTORY = auto()
105105

106106

107-
async def wait_for_single_reaction(bot: DCSServerBot, interaction: discord.Interaction,
108-
message: discord.Message) -> discord.Reaction:
107+
async def wait_for_single_reaction(interaction: discord.Interaction, message: discord.Message) -> discord.Reaction:
109108
def check_press(react: discord.Reaction, user: discord.Member):
110109
return (react.message.channel == interaction.channel) & (user == member) & (react.message.id == message.id)
111110

112111
tasks = [
113-
asyncio.create_task(bot.wait_for('reaction_add', check=check_press)),
114-
asyncio.create_task(bot.wait_for('reaction_remove', check=check_press))
112+
asyncio.create_task(interaction.client.wait_for('reaction_add', check=check_press)),
113+
asyncio.create_task(interaction.client.wait_for('reaction_remove', check=check_press))
115114
]
116115
try:
117116
member = interaction.user
@@ -126,10 +125,9 @@ def check_press(react: discord.Reaction, user: discord.Member):
126125
task.cancel()
127126

128127

129-
async def selection_list(bot: DCSServerBot, interaction: discord.Interaction, data: list, embed_formatter, num: int = 5,
128+
async def selection_list(interaction: discord.Interaction, data: list, embed_formatter, num: int = 5,
130129
marker: int = -1, marker_emoji='🔄'):
131130
"""
132-
:param bot: An instance of DCSServerBot class.
133131
:param interaction: A discord.Interaction instance representing the interaction event.
134132
:param data: A list of data to display in the embeds.
135133
:param embed_formatter: A function that formats the data into an embed.
@@ -164,7 +162,7 @@ async def selection_list(bot: DCSServerBot, interaction: discord.Interaction, da
164162
await message.add_reaction('⏹️')
165163
if ((j + 1) * num) < len(data):
166164
await message.add_reaction('▶️')
167-
react = await wait_for_single_reaction(bot, interaction, message)
165+
react = await wait_for_single_reaction(interaction, message)
168166
await message.delete()
169167
if react.emoji == '◀️':
170168
j -= 1

locale/es/LC_MESSAGES/install.mo

136 Bytes
Binary file not shown.

locale/es/LC_MESSAGES/install.po

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ msgstr "Buscando configuraciones de servidores de DCS existentes..."
9898
msgid "Please enter the path to your DCS World installation"
9999
msgstr "Por favor introduzca la ruta de su instalación de DCS"
100100

101+
msgid "Do you want to continue without a DCS installation being set?"
102+
msgstr "¿Desea continuar sin configurar una instalación de DCS?"
103+
101104
msgid "Directory not found. Please try again."
102105
msgstr "Directorio no encontrado. Por favor, inténtelo de nuevo."
103106

plugins/cloud/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ maintaining the solutions that are out in the wild.
4646
| /cloud status | | Admin | Status of the connection to the cloud service. |
4747
| /cloud resync | [@member / ucid] | DCS Admin | Resync all players (or this player) with the cloud. |
4848
| /cloud statistics | [@member / ucid] | DCS | Display player cloud statistics (overall, per guild) |
49+
| /serverlist | name | DCS | Display the DCS server list. |

plugins/cloud/commands.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from datetime import timedelta
2+
13
import aiohttp
24
import asyncio
35
import certifi
@@ -11,7 +13,7 @@
1113

1214
from contextlib import suppress
1315
from core import Plugin, utils, TEventListener, PaginationReport, Group, DEFAULT_TAG, PluginConfigurationError, \
14-
get_translation, ServiceRegistry
16+
get_translation, ServiceRegistry, command
1517
from discord import app_commands, DiscordServerError
1618
from discord.ext import commands, tasks
1719
from psycopg.rows import dict_row
@@ -218,6 +220,39 @@ async def statistics(self, interaction: discord.Interaction,
218220
except aiohttp.ClientError:
219221
await interaction.followup.send(_('Cloud not connected!'), ephemeral=True)
220222

223+
@command(description=_('List registered DCS servers'))
224+
@app_commands.guild_only()
225+
@utils.app_has_role('DCS Admin') # TODO: change that to DCS
226+
async def serverlist(self, interaction: discord.Interaction, search: Optional[str] = None):
227+
228+
def format_servers(servers: list[dict], marker, marker_emoji) -> discord.Embed:
229+
embed = discord.Embed(title=_('DCS Servers'), color=discord.Color.blue())
230+
for server in servers:
231+
name = ('🔐 ' if server['password'] else '🔓 ')
232+
name += f"{server['server_name']} [{server['num_players']}/{server['max_players']}]\n"
233+
value = f"IP/Port: {server['ipaddr']}:{server['port']}\n"
234+
value += f"Map: {server['theatre']}\n"
235+
value += f"Time: {timedelta(seconds=server['time_in_mission'])}\n"
236+
if server['time_to_restart'] != -1:
237+
value += f"Restart: {timedelta(seconds=server['time_to_restart'])}\n"
238+
embed.add_field(name=name, value='```' + value + '```', inline=False)
239+
return embed
240+
241+
# noinspection PyUnresolvedReferences
242+
await interaction.response.defer()
243+
try:
244+
query = 'serverlist'
245+
if search:
246+
query += f'?wildcard={search}'
247+
response = await self.get(query)
248+
if not len(response):
249+
await interaction.followup.send(_('No server found.'), ephemeral=True)
250+
return
251+
await utils.selection_list(interaction, response, format_servers)
252+
except aiohttp.ClientError:
253+
await interaction.followup.send(_('Cloud not connected!'), ephemeral=True)
254+
255+
221256
@tasks.loop(minutes=15.0)
222257
async def cloud_bans(self):
223258
try:
@@ -263,8 +298,8 @@ async def cloud_bans(self):
263298
for user in users_to_ban - banned_users - {self.bot.owner_id}:
264299
reason = next(x['reason'] for x in bans if x['discord_id'] == user.id)
265300
await guild.ban(user, reason='DGSA: ' + reason)
266-
except Exception as ex:
267-
self.log.exception(ex)
301+
except aiohttp.ClientError:
302+
self.log.warning("Cloud service unavailable.")
268303

269304
@cloud_bans.before_loop
270305
async def before_cloud_bans(self):

plugins/cloud/listener.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
import aiohttp
22
import asyncio
33

4-
from core import EventListener, Server, Player, Side, event
4+
from core import EventListener, Server, Player, Side, event, Status
5+
from datetime import datetime, timezone
6+
from discord.ext import tasks
57
from psycopg.rows import dict_row
68

79

810
class CloudListener(EventListener):
911

12+
def __init__(self, plugin):
13+
super().__init__(plugin)
14+
if self.plugin.get_config().get('register', True):
15+
self.update_registration.start()
16+
17+
async def shutdown(self) -> None:
18+
if self.plugin.get_config().get('register', True):
19+
self.update_registration.cancel()
20+
await super().shutdown()
21+
22+
@event(name="registerDCSServer")
23+
async def registerDCSServer(self, server: Server, data: dict) -> None:
24+
if self.plugin.get_config().get('register', True) and data['channel'].startswith('sync'):
25+
await self.cloud_register(server)
26+
1027
async def update_cloud_data(self, server: Server, player: Player):
1128
if not server.current_mission:
1229
return
@@ -52,3 +69,47 @@ async def onPlayerChangeSlot(self, server: Server, data: dict) -> None:
5269
return
5370
# noinspection PyAsyncCall
5471
asyncio.create_task(self.update_cloud_data(server, player))
72+
73+
async def cloud_register(self, server: Server):
74+
try:
75+
# noinspection PyUnresolvedReferences
76+
await self.plugin.post('register_server', {
77+
"guild_id": self.node.guild_id,
78+
"server_name": server.name,
79+
"ipaddr": server.instance.dcs_host,
80+
"port": server.instance.dcs_port,
81+
"password": (server.settings['password'] != ""),
82+
"theatre": server.current_mission.map,
83+
"dcs_version": server.node.dcs_version,
84+
"num_players": len(server.get_active_players()) + 1,
85+
"max_players": int(server.settings.get('maxPlayers', 16)),
86+
"time_in_mission": int(server.current_mission.mission_time if server.current_mission else 0),
87+
"time_to_restart": (server.restart_time - datetime.now(tz=timezone.utc)).total_seconds() if server.restart_time else -1,
88+
})
89+
self.log.info(f"Server {server.name} registered with the cloud.")
90+
except aiohttp.ClientError as ex:
91+
self.log.error(f"Could not register server {server.name} with the cloud.", exc_info=ex)
92+
93+
@event(name="onSimulationStart")
94+
async def onSimulationStart(self, server: Server, _: dict) -> None:
95+
if self.plugin.get_config().get('register', True):
96+
await self.cloud_register(server)
97+
98+
@event(name="onSimulationStop")
99+
async def onSimulationStop(self, server: Server, _: dict) -> None:
100+
if self.plugin.get_config().get('register', True):
101+
try:
102+
# noinspection PyUnresolvedReferences
103+
await self.plugin.post('unregister_server', {
104+
"guild_id": self.node.guild_id,
105+
"server_name": server.name,
106+
})
107+
self.log.info(f"Server {server.name} unregistered from the cloud.")
108+
except aiohttp.ClientError as ex:
109+
self.log.error(f"Could not unregister server {server.name} from the cloud.", exc_info=ex)
110+
111+
@tasks.loop(minutes=5)
112+
async def update_registration(self):
113+
for server in self.bot.servers.values():
114+
if server.status in [Status.RUNNING, Status.PAUSED]:
115+
await self.cloud_register(server)

plugins/mission/commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,7 +1442,7 @@ async def linkcheck(self, interaction: discord.Interaction):
14421442
if len(unmatched) == 0:
14431443
await interaction.followup.send(_('No unmatched member could be matched.'), ephemeral=True)
14441444
return
1445-
n = await utils.selection_list(self.bot, interaction, unmatched, self.format_unmatched)
1445+
n = await utils.selection_list(interaction, unmatched, self.format_unmatched)
14461446
if n != -1:
14471447
async with self.apool.connection() as conn:
14481448
async with conn.transaction():
@@ -1500,7 +1500,7 @@ async def mislinks(self, interaction: discord.Interaction):
15001500
if len(suspicious) == 0:
15011501
await interaction.followup.send(_('No mislinked players found.'), ephemeral=True)
15021502
return
1503-
n = await utils.selection_list(self.bot, interaction, suspicious, self.format_suspicious)
1503+
n = await utils.selection_list(interaction, suspicious, self.format_suspicious)
15041504
if n != -1:
15051505
ephemeral = utils.get_ephemeral(interaction)
15061506
async with self.apool.connection() as conn:

run.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,9 @@ async def run_node(name, config_dir=None, no_autoupdate=False) -> int:
212212
exit(-2)
213213

214214
# Check versions
215-
if int(platform.python_version_tuple()[0]) < 3 or int(platform.python_version_tuple()[1]) < 9:
216-
log.error("You need Python 3.9 or higher to run DCSServerBot (3.11 recommended)!")
215+
if int(platform.python_version_tuple()[0]) < 3 or int(platform.python_version_tuple()[1]) < 10:
216+
log.error("You need Python 3.10 or higher to run DCSServerBot!")
217217
exit(-2)
218-
elif int(platform.python_version_tuple()[1]) == 9:
219-
log.warning("Python 3.9 is outdated, you should consider upgrading it to 3.10 or higher.")
220218

221219
# Add certificates
222220
os.environ["SSL_CERT_FILE"] = certifi.where()

services/bot/dcsserverbot.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ async def on_command_error(self, ctx: commands.Context, err: Exception):
268268
await ctx.send("An unknown exception occurred.")
269269

270270
async def on_app_command_error(self, interaction: discord.Interaction, error: discord.app_commands.AppCommandError):
271-
if isinstance(error, discord.app_commands.CommandNotFound) or isinstance(error, discord.app_commands.CommandInvokeError):
271+
if isinstance(error, discord.app_commands.CommandNotFound):
272272
pass
273273
# noinspection PyUnresolvedReferences
274274
if not interaction.response.is_done():
@@ -279,11 +279,12 @@ async def on_app_command_error(self, interaction: discord.Interaction, error: di
279279
elif isinstance(error, discord.app_commands.CheckFailure):
280280
await interaction.followup.send(f"You don't have the permission to use {interaction.command.name}!",
281281
ephemeral=True)
282-
elif isinstance(error, TimeoutError) or isinstance(error, asyncio.TimeoutError):
282+
elif isinstance(error, (TimeoutError, asyncio.TimeoutError)):
283283
await interaction.followup.send('A timeout occurred. Is the DCS server running?', ephemeral=True)
284284
elif isinstance(error, discord.app_commands.TransformerError):
285285
await interaction.followup.send(error, ephemeral=True)
286286
elif isinstance(error, discord.app_commands.AppCommandError):
287+
self.log.exception(error)
287288
await interaction.followup.send(str(error))
288289
else:
289290
self.log.exception(error)

0 commit comments

Comments
 (0)