Skip to content

Commit 366d764

Browse files
author
karel26
committed
Created a cloud plugin to support server registration on remote servers.
1 parent b664c95 commit 366d764

File tree

6 files changed

+154
-71
lines changed

6 files changed

+154
-71
lines changed

core/data/impl/serverimpl.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
from services.bot import DCSServerBot
4444

4545
DEFAULT_EXTENSIONS = {
46-
"LogAnalyser": {}
46+
"LogAnalyser": {},
47+
"Cloud": {}
4748
}
4849

4950
__all__ = ["ServerImpl"]

extensions/cloud/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Extension "Cloud"
2+
Used by the [Cloud-plugin](../../plugins/cloud/README.md) for updating the cloud server list.
3+
4+
## Configuration
5+
All configuration is done in the cloud plugin.

extensions/cloud/__init__.py

Whitespace-only changes.

extensions/cloud/extension.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import aiohttp
2+
import certifi
3+
import os
4+
import ssl
5+
6+
from aiohttp import BasicAuth
7+
from core import Extension, Server, utils, DEFAULT_TAG
8+
from datetime import datetime, timezone
9+
from pathlib import Path
10+
from typing import Optional, Any
11+
12+
# ruamel YAML support
13+
from ruamel.yaml import YAML
14+
yaml = YAML()
15+
16+
17+
class Cloud(Extension):
18+
19+
def __init__(self, server: Server, config: dict):
20+
super().__init__(server, config)
21+
self.config = self.read_config()[DEFAULT_TAG]
22+
self._session = None
23+
self.client = None
24+
self.base_url = f"{self.config['protocol']}://{self.config['host']}:{self.config['port']}"
25+
26+
def load_config(self) -> Optional[dict]:
27+
return yaml.load(Path(os.path.join(self.node.config_dir, 'services', 'bot.yaml')).read_text(encoding='utf-8'))
28+
29+
def read_config(self):
30+
return yaml.load(Path(os.path.join(self.node.config_dir, 'plugins', 'cloud.yaml')).read_text(encoding='utf-8'))
31+
32+
@property
33+
def proxy(self) -> Optional[str]:
34+
return self.locals.get('proxy', {}).get('url')
35+
36+
@property
37+
def proxy_auth(self) -> Optional[BasicAuth]:
38+
username = self.locals.get('proxy', {}).get('username')
39+
try:
40+
password = utils.get_password('proxy', self.node.config_dir)
41+
except ValueError:
42+
return None
43+
if username and password:
44+
return BasicAuth(username, password)
45+
46+
@property
47+
def session(self):
48+
if not self._session:
49+
headers = {
50+
"Content-type": "application/json"
51+
}
52+
if 'token' in self.config:
53+
headers['Authorization'] = f"Bearer {self.config['token']}"
54+
self._session = aiohttp.ClientSession(
55+
connector=aiohttp.TCPConnector(ssl=ssl.create_default_context(cafile=certifi.where())),
56+
raise_for_status=True, headers=headers
57+
)
58+
return self._session
59+
60+
async def post(self, request: str, data: Any) -> Any:
61+
async def send(element: dict):
62+
url = f"{self.base_url}/{request}/"
63+
async with self.session.post(url, json=element, proxy=self.proxy, proxy_auth=self.proxy_auth) as response:
64+
return await response.json()
65+
66+
if isinstance(data, list):
67+
for line in data:
68+
await send(line)
69+
else:
70+
await send(data)
71+
72+
async def cloud_register(self):
73+
# we do not send cloud updates if we are not allowed and for non-public servers
74+
if not self.config.get('register', True) or not self.server.settings['isPublic']:
75+
return
76+
try:
77+
# noinspection PyUnresolvedReferences
78+
await self.post('register_server', {
79+
"guild_id": self.node.guild_id,
80+
"server_name": self.server.name,
81+
"ipaddr": self.server.instance.dcs_host,
82+
"port": self.server.instance.dcs_port,
83+
"password": (self.server.settings['password'] != ""),
84+
"theatre": self.server.current_mission.map,
85+
"dcs_version": self.node.dcs_version,
86+
"num_players": len(self.server.get_active_players()) + 1,
87+
"max_players": int(self.server.settings.get('maxPlayers', 16)),
88+
"mission": self.server.current_mission.name,
89+
"date": self.server.current_mission.date.strftime("%Y-%m-%d") if isinstance(self.server.current_mission.date, datetime) else self.server.current_mission.date,
90+
"start_time": self.server.current_mission.start_time,
91+
"time_in_mission": self.server.current_mission.mission_time,
92+
"time_to_restart": (self.server.restart_time - datetime.now(tz=timezone.utc)).total_seconds() if self.server.restart_time else -1,
93+
})
94+
self.log.info(f"Server {self.server.name} registered with the cloud.")
95+
except aiohttp.ClientError as ex:
96+
self.log.error(f"Could not register server {self.server.name} with the cloud.", exc_info=ex)
97+
98+
async def cloud_unregister(self):
99+
try:
100+
# noinspection PyUnresolvedReferences
101+
await self.plugin.post('unregister_server', {
102+
"guild_id": self.node.guild_id,
103+
"server_name": self.server.name,
104+
})
105+
self.log.info(f"Server {self.server.name} unregistered from the cloud.")
106+
except aiohttp.ClientError as ex:
107+
self.log.error(f"Could not unregister server {self.server.name} from the cloud.", exc_info=ex)
108+
109+
async def startup(self) -> bool:
110+
await self.cloud_register()
111+
return await super().startup()
112+
113+
def shutdown(self) -> bool:
114+
self.loop.create_task(self.cloud_unregister())
115+
return super().shutdown()

plugins/cloud/commands.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,9 @@ async def serverlist(self, interaction: discord.Interaction, search: Optional[st
228228

229229
def format_servers(servers: list[dict], marker, marker_emoji) -> discord.Embed:
230230
embed = discord.Embed(title=_('DCS Servers'), color=discord.Color.blue())
231-
for server in servers:
232-
name = ('🔐 ' if server['password'] else '🔓 ')
233-
name += f"{server['server_name']} [{server['num_players']}/{server['max_players']}]\n"
231+
for idx, server in enumerate(servers):
232+
name = chr(0x31 + idx) + '\u20E3' + f" {server['server_name']} [{server['num_players']}/{server['max_players']}]"
233+
name += (' 🔐' if server['password'] else ' 🔓') + '\n'
234234
value = f"IP/Port: {server['ipaddr']}:{server['port']}\n"
235235
value += f"Map: {server['theatre']}\n"
236236
value += f"Time: {timedelta(seconds=server['time_in_mission'])}\n"
@@ -239,19 +239,36 @@ def format_servers(servers: list[dict], marker, marker_emoji) -> discord.Embed:
239239
embed.add_field(name=name, value='```' + value + '```', inline=False)
240240
return embed
241241

242+
async def display_server(server: dict):
243+
embed = discord.Embed(color=discord.Color.blue())
244+
embed.title = f"{server['server_name']} [{server['num_players']}/{server['max_players']}]"
245+
embed.add_field(name=_("Address"), value=f"{server['ipaddr']}:{server['port']}", inline=False)
246+
embed.add_field(name=_("Map"), value=f"{server['theatre']}", inline=False)
247+
embed.add_field(name=_("Mission"), value=f"{server['mission']}", inline=False)
248+
embed.add_field(name=_("Time"), value=f"{timedelta(seconds=server['time_in_mission'])}", inline=False)
249+
if server['time_to_restart'] != -1:
250+
embed.add_field(name=_("Restart in"), value=f"{timedelta(seconds=server['time_to_restart'])}", inline=False)
251+
await interaction.followup.send(embed=embed)
252+
242253
# noinspection PyUnresolvedReferences
243254
await interaction.response.defer()
244255
try:
245256
query = f'serverlist?dcs_version={self.node.dcs_version}'
246257
if search:
247258
query += f'&wildcard={quote(search)}'
248259
else:
249-
query += f'guild_id={self.node.guild_id}'
260+
query += f'&guild_id={self.node.guild_id}'
250261
response = await self.get(query)
251262
if not len(response):
252-
await interaction.followup.send(_('No server found.'), ephemeral=True)
263+
if not search:
264+
await interaction.followup.send(_('No servers of this group are active.'), ephemeral=True)
265+
else:
266+
await interaction.followup.send(
267+
_('No server found with the name "*{search}*".').format(search=search), ephemeral=True)
253268
return
254-
await utils.selection_list(interaction, response, format_servers)
269+
n = await utils.selection_list(interaction, response, format_servers)
270+
if n >= 0:
271+
await display_server(response[n])
255272
except aiohttp.ClientError:
256273
await interaction.followup.send(_('Cloud not connected!'), ephemeral=True)
257274

plugins/cloud/listener.py

Lines changed: 9 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,12 @@
1-
from contextlib import suppress
2-
31
import aiohttp
42
import asyncio
53

6-
from core import EventListener, Server, Player, Side, event, Status
7-
from datetime import datetime, timezone
8-
from discord.ext import tasks
4+
from core import EventListener, Server, Player, Side, event
95
from psycopg.rows import dict_row
106

117

128
class CloudListener(EventListener):
139

14-
def __init__(self, plugin):
15-
super().__init__(plugin)
16-
if self.plugin.get_config().get('register', True):
17-
self.update_registration.start()
18-
19-
async def shutdown(self) -> None:
20-
if self.plugin.get_config().get('register', True):
21-
self.update_registration.cancel()
22-
await super().shutdown()
23-
24-
@event(name="registerDCSServer")
25-
async def registerDCSServer(self, server: Server, data: dict) -> None:
26-
if self.plugin.get_config().get('register', True) and data['channel'].startswith('sync'):
27-
await self.cloud_register(server)
28-
2910
async def update_cloud_data(self, server: Server, player: Player):
3011
if not server.current_mission:
3112
return
@@ -72,48 +53,12 @@ async def onPlayerChangeSlot(self, server: Server, data: dict) -> None:
7253
# noinspection PyAsyncCall
7354
asyncio.create_task(self.update_cloud_data(server, player))
7455

75-
async def cloud_register(self, server: Server):
76-
try:
77-
# noinspection PyUnresolvedReferences
78-
await self.plugin.post('register_server', {
79-
"guild_id": self.node.guild_id,
80-
"server_name": server.name,
81-
"ipaddr": server.instance.dcs_host,
82-
"port": server.instance.dcs_port,
83-
"password": (server.settings['password'] != ""),
84-
"theatre": server.current_mission.map,
85-
"dcs_version": server.node.dcs_version,
86-
"num_players": len(server.get_active_players()) + 1,
87-
"max_players": int(server.settings.get('maxPlayers', 16)),
88-
"mission": server.current_mission.name if server.current_mission else "",
89-
"time_in_mission": int(server.current_mission.mission_time if server.current_mission else 0),
90-
"time_to_restart": (server.restart_time - datetime.now(tz=timezone.utc)).total_seconds() if server.restart_time else -1,
91-
})
92-
self.log.info(f"Server {server.name} registered with the cloud.")
93-
except aiohttp.ClientError as ex:
94-
self.log.error(f"Could not register server {server.name} with the cloud.", exc_info=ex)
95-
96-
@event(name="onSimulationStart")
97-
async def onSimulationStart(self, server: Server, _: dict) -> None:
98-
if self.plugin.get_config().get('register', True):
99-
await self.cloud_register(server)
100-
101-
@event(name="onSimulationStop")
102-
async def onSimulationStop(self, server: Server, _: dict) -> None:
103-
if self.plugin.get_config().get('register', True):
104-
try:
105-
# noinspection PyUnresolvedReferences
106-
await self.plugin.post('unregister_server', {
107-
"guild_id": self.node.guild_id,
108-
"server_name": server.name,
109-
})
110-
self.log.info(f"Server {server.name} unregistered from the cloud.")
111-
except aiohttp.ClientError as ex:
112-
self.log.error(f"Could not unregister server {server.name} from the cloud.", exc_info=ex)
56+
@event(name="onPlayerStart")
57+
async def onPlayerStart(self, server: Server, data: dict) -> None:
58+
if data['id'] != 1:
59+
await server.run_on_extension(extension='Cloud', method='cloud_register')
11360

114-
@tasks.loop(minutes=5)
115-
async def update_registration(self):
116-
for server in self.bot.servers.values():
117-
if server.status in [Status.RUNNING, Status.PAUSED]:
118-
with suppress(Exception):
119-
await self.cloud_register(server)
61+
@event(name="onPlayerStop")
62+
async def onPlayerStop(self, server: Server, data: dict) -> None:
63+
if data['id'] != 1:
64+
await server.run_on_extension(extension='Cloud', method='cloud_register')

0 commit comments

Comments
 (0)