Skip to content

Commit 83d9987

Browse files
committed
ENHANCEMENT:
- Punishment: informative events can be sent to a different channel now (or disabled) CHANGES: - Permission check for channels improved.
1 parent 4bafb04 commit 83d9987

File tree

7 files changed

+123
-50
lines changed

7 files changed

+123
-50
lines changed

core/const.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
"MONTH",
1616
"TRAFFIC_LIGHTS",
1717
"SAVED_GAMES",
18-
"DEFAULT_TAG"
18+
"DEFAULT_TAG",
19+
"SEND_ONLY_CHANNEL_PERMISSIONS",
20+
"DEFAULT_CHANNEL_PERMISSIONS",
21+
"FULL_MANAGE_CHANNEL_PERMISSIONS",
1922
]
2023

2124
METER_IN_FEET = 3.28084
@@ -67,3 +70,21 @@
6770
)[0]
6871

6972
DEFAULT_TAG = 'DEFAULT'
73+
74+
SEND_ONLY_CHANNEL_PERMISSIONS = {
75+
"view_channel",
76+
"send_messages",
77+
"read_messages",
78+
"read_message_history",
79+
"add_reactions",
80+
}
81+
82+
DEFAULT_CHANNEL_PERMISSIONS = SEND_ONLY_CHANNEL_PERMISSIONS | {
83+
"attach_files",
84+
"embed_links",
85+
"manage_messages",
86+
}
87+
88+
FULL_MANAGE_CHANNEL_PERMISSIONS = DEFAULT_CHANNEL_PERMISSIONS | {
89+
"manage_channel",
90+
}

plugins/greenieboard/const.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
GRADES = {
22
"_OK_": {"rating": 5, "grade": "Perfect pass", "legend": "PERFECT"},
33
"OK": {"rating": 4, "grade": "Minimum deviation with good correction", "color": "#29C248", "legend": "OK"},
4-
"(OK)": {"rating": 3, "grade": "Reasonable deviation with average correction", "color": "#F2C038", "legend": "FAIR"},
5-
"B": {"rating": 2.5, "grade": "Tailhook did not catch a wire, aircraft went around for another pass", "color": '#088199', "legend": "BOLTER"},
4+
"(OK)": {"rating": 3, "grade": "Reasonable deviation with avg. correction", "color": "#F2C038", "legend": "FAIR"},
5+
"B": {"rating": 2.5, "grade": "No wire-catch, go-around needed", "color": '#088199', "legend": "BOLTER"},
66
"--": {"rating": 2, "grade": "No grade. Below average corrections but safe pass", "color": "#73481d", "legend": "NO GRADE"},
77
"WO": {"rating": 1, "grade": "Wave-off", "color": "#000000", "legend": "WAVE OFF"},
88
"C": {"rating": 0, "grade": "Cut. Unsafe, gross deviations inside the wave-off window", "color": "#CC0000", "legend": "CUT"},
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
ALTER TABLE greenieboard ADD COLUMN IF NOT EXISTS mission_id INTEGER NOT NULL DEFAULT -1;
22
ALTER TABLE greenieboard ADD COLUMN IF NOT EXISTS trapsheet TEXT;
33
DELETE FROM greenieboard;
4-
INSERT INTO greenieboard (mission_id, player_ucid, unit_type, grade, comment, place, night, points, time) SELECT mission_id, init_id, init_type, grade, comment, place, FALSE, CASE WHEN grade = '_OK_' THEN 5 WHEN grade = 'OK' THEN 4 WHEN grade = '(OK)' THEN 3 WHEN grade = 'B' THEN 2.5 WHEN grade IN('---', 'OWO', 'WOP') THEN 2 WHEN grade IN ('WO', 'LIG') THEN 1 WHEN grade = 'C' THEN 0 END AS points, time FROM (SELECT mission_id, init_id, init_type, SUBSTRING(comment, 'LSO: GRADE:([_\(\)-BCKOW]{1,4})') AS grade, comment, place, time FROM missionstats WHERE event LIKE '%QUALITY%' AND init_type IS NOT NULL) AS landings;
4+
INSERT INTO greenieboard (mission_id, player_ucid, unit_type, grade, comment, place, night, points, time) SELECT mission_id, init_id, init_type, grade, comment, place, FALSE, CASE WHEN grade = '_OK_' THEN 5 WHEN grade = 'OK' THEN 4 WHEN grade = '(OK)' THEN 3 WHEN grade = 'B' THEN 2.5 WHEN grade IN('---', 'OWO', 'WOP') THEN 2 WHEN grade IN ('WO', 'LIG') THEN 1 WHEN grade = 'C' THEN 0 END AS points, time FROM (SELECT mission_id, init_id, init_type, SUBSTRING(comment, 'LSO: GRADE:([_\(\)-BCKOW]{1,4})') AS grade, comment, place, time FROM missionstats WHERE event LIKE '%QUALITY%' AND init_id IS NOT NULL) AS landings;

plugins/punishment/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ opt_plugins:
1313
The plugin itself is configured with a file named config/plugins/punishment.yaml. You'll find a sample in ./samples:
1414
```yaml
1515
DEFAULT:
16+
channel: 1122334455667788 # Optional: Channel where to post who was punished for what (default: admin channel, disable: -1).
1617
penalties: # These are the penalty points to use.
1718
- event: kill # If you team-kill a human player, you get 30 points, 18 in the case of an AI.
1819
human: 30

plugins/punishment/commands.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from contextlib import suppress
66
from core import (Plugin, PluginRequiredError, utils, Player, Server, PluginInstallationError, command, DEFAULT_TAG,
7-
Report, get_translation)
7+
Report, get_translation, SEND_ONLY_CHANNEL_PERMISSIONS)
88
from discord import app_commands
99
from discord.app_commands import Range
1010
from discord.ext import tasks
@@ -42,7 +42,13 @@ async def cog_unload(self):
4242
async def punish(self, server: Server, ucid: str, punishment: dict, reason: str, points: float | None = None):
4343
player: Player = server.get_player(ucid=ucid)
4444
member = self.bot.get_member_by_ucid(ucid)
45-
admin_channel = self.bot.get_admin_channel(server)
45+
channel_id = self.get_config(server).get('channel')
46+
if channel_id:
47+
self.bot.check_channel(channel_id, SEND_ONLY_CHANNEL_PERMISSIONS)
48+
channel = self.bot.get_channel(channel_id)
49+
else:
50+
channel = self.bot.get_admin_channel(server)
51+
4652
if punishment['action'] == 'ban':
4753
# we must not punish for reslots here
4854
self.eventlistener.pending_kill.pop(ucid, None)
@@ -66,8 +72,8 @@ async def punish(self, server: Server, ucid: str, punishment: dict, reason: str,
6672
message = _("Player with ucid {ucid} banned by {banned_by} for {reason}.").format(
6773
ucid=ucid, banned_by=self.bot.member.name, reason=reason)
6874
# audit
69-
if admin_channel:
70-
await admin_channel.send("```" + message + "```")
75+
if channel:
76+
await channel.send("```" + message + "```")
7177
await self.bot.audit(message)
7278

7379
# everything after that point can only be executed if players are active
@@ -114,8 +120,8 @@ async def punish(self, server: Server, ucid: str, punishment: dict, reason: str,
114120
points=points))
115121
if message:
116122
# audit
117-
if admin_channel:
118-
await admin_channel.send("```" + message + "```")
123+
if channel:
124+
await channel.send("```" + message + "```")
119125
await self.bot.audit(message)
120126

121127
# TODO: change to pubsub

plugins/punishment/schemas/punishment_schema.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ schema;element_schema:
1212
nullable: false
1313
mapping:
1414
enabled: {type: bool, nullable: false}
15+
channel: {type: int, nullable: false}
1516
penalties:
1617
type: seq
1718
nullable: false

services/bot/dcsserverbot.py

Lines changed: 84 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import discord
33

44
from aiohttp import ClientError
5-
from core import Channel, utils, Status, PluginError, Group, Node
5+
from core import Channel, utils, Status, PluginError, Group, Node, DEFAULT_CHANNEL_PERMISSIONS, \
6+
SEND_ONLY_CHANNEL_PERMISSIONS
67
from core.data.node import FatalException
78
from core.listener import EventListener
89
from core.services.registry import ServiceRegistry
@@ -126,42 +127,79 @@ def check_roles(self, roles: Iterable[str | int]):
126127
if not self.get_role(role):
127128
self.log.error(f" => Role {role} not found in your Discord!")
128129

129-
def check_channel(self, channel_id: int) -> bool:
130+
@staticmethod
131+
def _channel_path(channel: discord.abc.GuildChannel) -> str:
132+
"""
133+
Helper: return 'category/channel' if the channel has a category,
134+
otherwise just 'channel'.
135+
"""
136+
# Use ASCII‑safe names in the log (just like the original code)
137+
channel_name = channel.name.encode("utf-8", "replace").decode()
138+
139+
if channel.category: # `channel.category` is a CategoryChannel or None
140+
cat_name = channel.category.name.encode("ascii", "replace").decode()
141+
return f"{cat_name}/{channel_name}"
142+
return channel_name
143+
144+
def check_channel(
145+
self,
146+
channel_id: int,
147+
permissions: Iterable[str] | None = None,
148+
) -> bool:
149+
"""
150+
Verify that the bot has the *required* permissions on the given channel.
151+
152+
Parameters
153+
----------
154+
channel_id : int
155+
Discord channel ID. `-1` is treated as a “no‑check” marker.
156+
permissions : Iterable[str] | None
157+
Permission names to check for (e.g. ``'view_channel'``).
158+
If omitted, the default set in ``const.DEFAULT_CHANNEL_PERMISSIONS`` is used.
159+
160+
Returns
161+
-------
162+
bool
163+
``True`` if *all* requested permissions are present; otherwise ``False``.
164+
"""
130165
if channel_id == -1:
166+
# A sentinel value – we purposely skip the check.
131167
return True
168+
132169
channel = self.get_channel(channel_id)
133170
if not channel:
134-
self.log.error(f'No channel with ID {channel_id} found!')
171+
self.log.error(f"No channel with ID {channel_id} found!")
135172
return False
136-
channel_name = channel.name.encode(encoding='ASCII', errors='replace').decode()
137-
# name changes of the status channel will only happen with the correct permission
138-
ret = True
139-
permissions = channel.permissions_for(self.member)
140-
if not permissions.view_channel:
141-
self.log.error(f' => Permission "View Channel" missing for channel {channel_name}')
142-
ret = False
143-
if not permissions.send_messages:
144-
self.log.error(f' => Permission "Send Messages" missing for channel {channel_name}')
145-
ret = False
146-
if not permissions.read_messages:
147-
self.log.error(f' => Permission "Read Messages" missing for channel {channel_name}')
148-
ret = False
149-
if not permissions.read_message_history:
150-
self.log.error(f' => Permission "Read Message History" missing for channel {channel_name}')
151-
ret = False
152-
if not permissions.add_reactions:
153-
self.log.error(f' => Permission "Add Reactions" missing for channel {channel_name}')
154-
ret = False
155-
if not permissions.attach_files:
156-
self.log.error(f' => Permission "Attach Files" missing for channel {channel_name}')
157-
ret = False
158-
if not permissions.embed_links:
159-
self.log.error(f' => Permission "Embed Links" missing for channel {channel_name}')
160-
ret = False
161-
if not permissions.manage_messages:
162-
self.log.error(f' => Permission "Manage Messages" missing for channel {channel_name}')
163-
ret = False
164-
return ret
173+
174+
# Make a *copy* so that the caller can pass in a mutable list without
175+
# accidentally mutating the defaults.
176+
required_perms: set[str] = set(permissions or DEFAULT_CHANNEL_PERMISSIONS)
177+
178+
channel_name = self._channel_path(channel)
179+
channel_perms = channel.permissions_for(self.member)
180+
181+
# ------------------------------------------------------------------
182+
# Iterate over the permission names and flag missing ones.
183+
# ------------------------------------------------------------------
184+
has_all = True
185+
for perm_name in required_perms:
186+
# If the attribute does not exist on the Permission object we
187+
# raise a clear error – this is a programming mistake, not a
188+
# runtime Discord issue.
189+
if not hasattr(channel_perms, perm_name):
190+
raise AttributeError(
191+
f"Permission object has no attribute '{perm_name}'. "
192+
"Check the spelling against the discord.py docs."
193+
)
194+
195+
if not getattr(channel_perms, perm_name):
196+
self.log.error(
197+
f" => Permission '{perm_name.replace('_', ' ').title()}' "
198+
f"missing for channel '{channel_name}'"
199+
)
200+
has_all = False
201+
202+
return has_all
165203

166204
def get_channel(self, channel_id: int, /) -> Any:
167205
if channel_id == -1:
@@ -179,16 +217,22 @@ def get_role(self, role: str | int) -> discord.Role | None:
179217
else:
180218
return None
181219

182-
def check_channels(self, server: "Server"):
183-
channels = ['status', 'chat']
220+
def _check_server_channels(self, server: "Server"):
221+
channels = {
222+
'status': DEFAULT_CHANNEL_PERMISSIONS,
223+
'chat': SEND_ONLY_CHANNEL_PERMISSIONS
224+
}
184225
if not self.locals.get('channels', {}).get('admin'):
185-
channels.append('admin')
226+
channels['admin'] = DEFAULT_CHANNEL_PERMISSIONS
186227
if server.locals.get('coalitions'):
187-
channels.extend(['red', 'blue'])
188-
for c in channels:
228+
channels |= {
229+
'red': SEND_ONLY_CHANNEL_PERMISSIONS,
230+
'blue': SEND_ONLY_CHANNEL_PERMISSIONS
231+
}
232+
for c, perms in channels.items():
189233
channel_id = int(server.channels[Channel(c)])
190234
if channel_id != -1:
191-
self.check_channel(channel_id)
235+
self.check_channel(channel_id, perms)
192236

193237
async def on_ready(self):
194238
async def register_guild_name():
@@ -225,7 +269,7 @@ async def register_guild_name():
225269
self.check_roles(roles)
226270
# check channels in bot.yaml
227271
for name, channel in self.locals.get('channels', {}).items():
228-
self.check_channel(int(channel))
272+
self.check_channel(int(channel), DEFAULT_CHANNEL_PERMISSIONS)
229273
# check channels in servers.yaml
230274
for server in self.servers.values():
231275
if server.locals.get('coalitions'):
@@ -234,7 +278,7 @@ async def register_guild_name():
234278
roles.add(server.locals['coalitions']['red_role'])
235279
self.check_roles(roles)
236280
try:
237-
self.check_channels(server)
281+
self._check_server_channels(server)
238282
except KeyError:
239283
self.log.error(f" => Mandatory channel(s) missing for server {server.name} in servers.yaml!")
240284

0 commit comments

Comments
 (0)