Skip to content

Commit dc79cfa

Browse files
committed
CHANGES:
- Some code cleanups. - DBExporter is deprecated now. - Python 3.10 is deprecated now. BUGFIXES: - Resent messages can trigger exceptions.
1 parent f4651e8 commit dc79cfa

File tree

33 files changed

+272
-85
lines changed

33 files changed

+272
-85
lines changed

core/data/server.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,7 @@ async def send_to_dcs_sync(self, message: dict, timeout: int | None = 5.0) -> di
312312
await self.send_to_dcs(message)
313313
return await asyncio.wait_for(future, timeout)
314314
finally:
315-
current = self.listeners.get(token)
316-
if current is future:
315+
if self.listeners.get(token) is future:
317316
self.listeners.pop(token, None)
318317

319318
async def sendChatMessage(self, coalition: Coalition, message: str, sender: str = None):

core/utils/helper.py

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
"format_period",
5959
"slugify",
6060
"alternate_parse_settings",
61+
"exception_to_dict",
62+
"rebuild_exception",
63+
"ReprException",
6164
"get_all_players",
6265
"is_ucid",
6366
"get_presets",
@@ -75,6 +78,7 @@
7578
"deep_merge",
7679
"hash_password",
7780
"run_parallel_nofail",
81+
"safe_set_result",
7882
"evaluate",
7983
"for_each",
8084
"YAMLError",
@@ -331,6 +335,58 @@ def parse(value: str) -> int | str | bool:
331335
return settings
332336

333337

338+
def exception_to_dict(e: BaseException) -> dict[str, Any]:
339+
"""Return a plain‑dict representation of any exception."""
340+
exc_dict = {
341+
'class': f'{e.__class__.__module__}.{e.__class__.__name__}',
342+
'message': str(e),
343+
'traceback': traceback.format_exception_only(type(e), e),
344+
}
345+
346+
# Serialise args (convert each to a string / repr)
347+
exc_dict['args'] = [repr(a) for a in e.args]
348+
349+
# Pull out useful OSError / socket attributes
350+
# (only those that are JSON‑friendly)
351+
for key in ('errno', 'strerror', 'filename', 'filename2'):
352+
if hasattr(e, key):
353+
exc_dict[key] = getattr(e, key)
354+
355+
# If the exception has a kwargs dict (rare), sanitize it
356+
kwargs = getattr(e, 'kwargs', None)
357+
if isinstance(kwargs, dict):
358+
exc_dict['kwargs'] = {k: repr(v) for k, v in kwargs.items()}
359+
360+
return exc_dict
361+
362+
363+
class ReprException(Exception):
364+
"""Wrapper that keeps the original payload if we can’t rebuild it."""
365+
def __init__(self, payload: dict[str, Any]):
366+
self.payload = payload
367+
super().__init__(f'Unable to reconstruct exception from {payload!r}')
368+
369+
370+
def rebuild_exception(payload: dict[str, Any]) -> BaseException:
371+
"""
372+
Recreate a BaseException from the serialized payload.
373+
If the payload cannot be used to instantiate the original type,
374+
we return a lightweight wrapper that stores the payload.
375+
"""
376+
cls = str_to_class(payload['class'])
377+
if not cls:
378+
return ReprException(payload)
379+
380+
args = tuple(payload.get('args', ())) # ensures a tuple
381+
kwargs = dict(payload.get('kwargs', {})) # ensures a dict
382+
383+
try:
384+
return cls(*args, **kwargs)
385+
except Exception:
386+
# Constructor raised an unexpected error – fall back.
387+
return ReprException(payload)
388+
389+
334390
def get_all_players(self, linked: bool | None = None, watchlist: bool | None = None,
335391
vip: bool | None = None) -> list[tuple[str, str]]:
336392
"""
@@ -945,15 +1001,39 @@ def tree_delete(d: dict, key: str, debug: bool | None = False):
9451001
curr_element.pop(int(keys[-1]))
9461002

9471003

948-
def deep_merge(dict1, dict2):
949-
result = dict(dict1) # Create a shallow copy of dict1
950-
for key, value in dict2.items():
951-
if key in result and isinstance(result[key], Mapping) and isinstance(value, Mapping):
952-
# Recursively merge dictionaries
1004+
def deep_merge(d1: Mapping[str, Any], d2: Mapping[str, Any]) -> Mapping[str, Any]:
1005+
"""
1006+
Merge two dictionaries recursively. Non‑mapping values are overwritten.
1007+
1008+
Parameters
1009+
----------
1010+
d1, d2 : Mapping
1011+
Input mappings to merge. They are *not* modified.
1012+
1013+
Returns
1014+
-------
1015+
dict
1016+
A new dictionary containing the deep merge of `d1` and `d2`.
1017+
"""
1018+
if not isinstance(d1, Mapping):
1019+
raise TypeError(f"d1 must be a Mapping, got {type(d1).__name__}")
1020+
if not isinstance(d2, Mapping):
1021+
raise TypeError(f"d2 must be a Mapping, got {type(d2).__name__}")
1022+
1023+
result: dict = dict(d1) # shallow copy of d1
1024+
1025+
for key, value in d2.items():
1026+
# If both sides are mappings, merge recursively
1027+
if (
1028+
key in result
1029+
and isinstance(result[key], Mapping)
1030+
and isinstance(value, Mapping)
1031+
):
9531032
result[key] = deep_merge(result[key], value)
9541033
else:
955-
# Overwrite or add the new key-value pair
1034+
# Overwrite or add the new key/value pair
9561035
result[key] = value
1036+
9571037
return result
9581038

9591039

@@ -981,6 +1061,11 @@ async def run_parallel_nofail(*tasks):
9811061
await asyncio.gather(*tasks, return_exceptions=True)
9821062

9831063

1064+
def safe_set_result(fut: asyncio.Future, payload: dict) -> None:
1065+
if not fut.done():
1066+
fut.set_result(payload)
1067+
1068+
9841069
def evaluate(value: str | int | float | bool | list | dict, **kwargs) -> str | int | float | bool | list | dict:
9851070
"""
9861071
Evaluate the given value, replacing placeholders with keyword arguments if necessary.

plugins/admin/commands.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ async def available_modules_autocomplete(interaction: discord.Interaction,
5252
return [
5353
app_commands.Choice(name=x, value=x)
5454
for x in available_modules
55-
if not current or current.casefold() in x.casefold()
55+
if 0 < len(x) <= 100 and (not current or current.casefold() in x.casefold())
5656
][:25]
5757
except Exception as ex:
5858
interaction.client.log.exception(ex)
@@ -250,8 +250,8 @@ async def extensions_autocomplete(interaction: discord.Interaction, current: str
250250

251251
class Admin(Plugin[AdminEventListener]):
252252

253-
def __init__(self, bot: DCSServerBot, listener: Type[AdminEventListener]):
254-
super().__init__(bot, listener)
253+
async def cog_load(self):
254+
await super().cog_load()
255255
self.cleanup.add_exception_type(psycopg.DatabaseError)
256256
self.cleanup.start()
257257

@@ -1378,8 +1378,12 @@ async def on_member_join(self, member: discord.Member):
13781378
if ucid and self.bot.locals.get('autoban', False):
13791379
await self.bus.unban(ucid)
13801380
if self.bot.locals.get('greeting_dm'):
1381-
channel = await member.create_dm()
1382-
await channel.send(self.bot.locals['greeting_dm'].format(name=member.name, guild=member.guild.name))
1381+
try:
1382+
channel = await member.create_dm()
1383+
await channel.send(self.bot.locals['greeting_dm'].format(name=member.name, guild=member.guild.name))
1384+
except discord.Forbidden:
1385+
self.log.debug("Could not send greeting DM to user {} due to their Discord limitations.".format(
1386+
member.display_name))
13831387
autorole = self.bot.locals.get('autorole', {}).get('on_join')
13841388
if autorole:
13851389
try:

plugins/cloud/commands.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,9 @@ def __init__(self, bot: DCSServerBot, eventlistener: Type[CloudListener] = None)
3636
self.config = self.get_config()
3737
if not self.config:
3838
raise PluginConfigurationError(plugin=self.plugin_name, option=DEFAULT_TAG)
39-
self.base_url = f"{self.config['protocol']}://{self.config['host']}:{self.config['port']}"
39+
self.base_url = None
4040
self._session = None
4141
self.client = None
42-
if self.config.get('dcs-ban', False) or self.config.get('discord-ban', False):
43-
self.cloud_bans.add_exception_type(IndexError)
44-
self.cloud_bans.add_exception_type(aiohttp.ClientError)
45-
self.cloud_bans.add_exception_type(discord.Forbidden)
46-
self.cloud_bans.add_exception_type(psycopg.DatabaseError)
47-
self.cloud_bans.add_exception_type(DiscordServerError)
48-
self.cloud_bans.start()
49-
if 'token' in self.config:
50-
self.cloud_sync.add_exception_type(IndexError)
51-
self.cloud_sync.add_exception_type(aiohttp.ClientError)
52-
self.cloud_sync.add_exception_type(psycopg.DatabaseError)
53-
self.cloud_sync.start()
54-
if self.config.get('register', True):
55-
self.register.start()
5642

5743
@property
5844
def session(self):
@@ -75,6 +61,23 @@ def session(self):
7561

7662
async def cog_load(self):
7763
await super().cog_load()
64+
self.base_url = f"{self.config['protocol']}://{self.config['host']}:{self.config['port']}"
65+
self._session = None
66+
self.client = None
67+
if self.config.get('dcs-ban', False) or self.config.get('discord-ban', False):
68+
self.cloud_bans.add_exception_type(IndexError)
69+
self.cloud_bans.add_exception_type(aiohttp.ClientError)
70+
self.cloud_bans.add_exception_type(discord.Forbidden)
71+
self.cloud_bans.add_exception_type(psycopg.DatabaseError)
72+
self.cloud_bans.add_exception_type(DiscordServerError)
73+
self.cloud_bans.start()
74+
if 'token' in self.config:
75+
self.cloud_sync.add_exception_type(IndexError)
76+
self.cloud_sync.add_exception_type(aiohttp.ClientError)
77+
self.cloud_sync.add_exception_type(psycopg.DatabaseError)
78+
self.cloud_sync.start()
79+
if self.config.get('register', True):
80+
self.register.start()
7881
if self.config.get('upload_errors', True):
7982
cloud_logger = CloudLoggingHandler(node=self.node, url=self.base_url + '/errors/')
8083
self.log.root.addHandler(cloud_logger)

plugins/cloud/listener.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,19 @@ async def onPlayerChangeSlot(self, server: Server, data: dict) -> None:
6262
@event(name="onPlayerStart")
6363
async def onPlayerStart(self, server: Server, data: dict) -> None:
6464
if data['id'] != 1:
65-
await server.run_on_extension(extension='Cloud', method='cloud_register')
65+
try:
66+
await server.run_on_extension(extension='Cloud', method='cloud_register')
67+
except ValueError:
68+
self.log.warning("Cloud extension is not active.")
6669
self.updates[server.name] = datetime.now(tz=timezone.utc)
6770

6871
@event(name="onPlayerStop")
6972
async def onPlayerStop(self, server: Server, data: dict) -> None:
7073
if data['id'] != 1:
71-
await server.run_on_extension(extension='Cloud', method='cloud_register')
74+
try:
75+
await server.run_on_extension(extension='Cloud', method='cloud_register')
76+
except ValueError:
77+
self.log.warning("Cloud extension is not active.")
7278
self.updates[server.name] = datetime.now(tz=timezone.utc)
7379

7480
@event(name="getMissionUpdate")

plugins/dbexporter/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Plugin DBExporter
2-
This plugin will dump the whole DCSServerBot database every hour to ./export/_tablename_.json files for further processing, if needed.
2+
This plugin will dump the whole DCSServerBot database every hour to ./export/_tablename_.json files for further
3+
processing, if needed.
4+
5+
> [!WARNING]
6+
> This plugin is deprecated and will be removed at some point.
7+
> The amount of data that will be written by this plugin can be enormous.
8+
> Please use [Backup](/service/backup/README.md) or [RestAPI](/service/restapi/README.md) instead.
39
410
## Configuration
511
As DBExporter is an optional plugin, you need to activate it in main.yaml first like so:

plugins/dbexporter/commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,5 @@ async def schedule(self):
5555

5656

5757
async def setup(bot: DCSServerBot):
58+
bot.log.warning(_("The DBExporter plugin is deprecated. Please use Backup or RestAPI instead."))
5859
await bot.add_cog(DBExporter(bot))

plugins/discord/commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010

1111
class Discord(Plugin):
12+
1213
@command(name='clear', description=_('Clear Discord messages'))
1314
@app_commands.guild_only()
1415
@utils.app_has_role('Admin')

plugins/funkman/listener.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ async def update_rangeboard(self, server: Server, what: Literal['strafe', 'bomb'
141141
except Exception as ex:
142142
self.log.exception(ex)
143143

144+
@event(name='registerDCSServer')
145+
async def registerDCSServer(self, server: Server, data: dict) -> None:
146+
config = self.get_config(server)
147+
for name in ['CHANNELID_MAIN', 'CHANNELID_RANGE', 'CHANNELID_AIRBOSS']:
148+
if name in config:
149+
self.bot.check_channel(self.config[name])
150+
144151
@event(name="moose_text")
145152
async def moose_text(self, server: Server, data: dict) -> None:
146153
config = self.plugin.get_config(server)

plugins/gamemaster/listener.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ def __init__(self, plugin: "GameMaster"):
2727
self.chat_log = dict()
2828
self.tasks: dict[str, asyncio.TimerHandle] = {}
2929

30+
async def shutdown(self) -> None:
31+
for task in self.tasks.values():
32+
task.cancel()
33+
3034
async def can_run(self, command: ChatCommand, server: Server, player: Player) -> bool:
3135
coalitions_enabled = server.locals.get('coalitions')
3236
coalition = await self.get_coalition(server, player) if coalitions_enabled else None

0 commit comments

Comments
 (0)