Skip to content

Commit bf60c5b

Browse files
committed
ENHANCEMENTS:
- Voting: new discord commands.
1 parent a70aafb commit bf60c5b

File tree

10 files changed

+207
-36
lines changed

10 files changed

+207
-36
lines changed

plugins/creditsystem/commands.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,10 @@ async def info(self, interaction: discord.Interaction,
8181
member = interaction.user
8282
ucid = await self.bot.get_ucid_by_member(member)
8383
if not ucid:
84+
_mission = self.bot.cogs['Mission']
8485
# noinspection PyUnresolvedReferences
8586
await interaction.response.send_message(_("Use {} to link your account.").format(
86-
(await utils.get_command(self.bot, name='linkme')).mention
87+
(await utils.get_command(self.bot, name=_mission.linkme.name)).mention
8788
), ephemeral=True)
8889
return
8990
data = await self.get_credits(ucid)

plugins/mission/listener.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ async def onBanReject(self, server: Server, data: dict) -> None:
674674
admin_channel = self.bot.get_admin_channel(server)
675675
if not admin_channel:
676676
return
677-
message = _('Banned user {name} (ucid={ucid}, ipaddr={ipaddr}) rejected. Reason: {reason}').format(
677+
message = _('Banned user {name} (ucid={ucid}, ipaddr={ipaddr}) rejected.\nReason: {reason}').format(
678678
name=data.get('name', 'n/a'), ucid=data['ucid'], ipaddr=data['ipaddr'], reason=data['reason'])
679679
await admin_channel.send(f"```{message}```")
680680

plugins/voting/README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,21 @@ DEFAULT:
5757
mission: {} # Select all available presets of your serverSettings.lua
5858
```
5959
60+
## Discord Commands
61+
| Command | Parameter | Channel | Role | Description |
62+
|:-------------|:--------------|:-------:|:----------|:-------------------------------------------------------------------------|
63+
| /vote list | | all | DCS | Lists all active votes on servers. |
64+
| /vote create | server choice | all | DCS | Create a new vote. Permission specifics might apply (e.g. creator role). |
65+
| /vote cancel | server | all | DCS Admin | Cancel the running vote on this server. |
66+
67+
> [!IMPORTANT]
68+
> You need to define the `creator` role in your voting.yaml to use `/vote create`.
69+
> This is to avoid that random people can create votes in your Discord server.
70+
6071
## In-Game Commands
6172
| Command | Parameter | Description |
62-
|---------|--------------------|--------------------------------------------------|
63-
| -vote | \<what\> \[param\] | Start a voting . |
73+
|:--------|:-------------------|:-------------------------------------------------|
74+
| -vote | \<what\> \[param\] | Start a voting. |
6475
| -vote | cancel | Cancel a voting (only DCS Admin can do that). |
6576
| -vote | <num> | Vote for one of the options. |
6677
| -vote | | Display the current voting and the leading vote. |

plugins/voting/commands.py

Lines changed: 153 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
from typing import Type
1+
import discord
22

3-
from core import Plugin, PluginInstallationError
4-
from plugins.voting.listener import VotingListener
3+
from core import Plugin, PluginInstallationError, Group, utils, get_translation, Server, Status
4+
from discord import app_commands, SelectOption
5+
from plugins.creditsystem.commands import CreditSystem
56
from services.bot import DCSServerBot
7+
from typing import Type, Literal, cast
8+
9+
from .base import VotableItem
10+
from .listener import VotingListener, VotingHandler
11+
12+
_ = get_translation(__name__.split('.')[1])
613

714

815
class Voting(Plugin[VotingListener]):
@@ -11,6 +18,149 @@ def __init__(self, bot: DCSServerBot, eventlistener: Type[VotingListener] = None
1118
if not self.locals:
1219
raise PluginInstallationError(reason=f"No {self.plugin_name}.yaml file found!", plugin=self.plugin_name)
1320

21+
# New command group "/vote"
22+
vote = Group(name="vote", description=_("Commands to manage votes"))
23+
24+
@vote.command(name='list', description=_('Lists the current votes'))
25+
@app_commands.guild_only()
26+
@utils.app_has_role('DCS')
27+
async def _list(self, interaction: discord.Interaction):
28+
servers = []
29+
votes = []
30+
voters = []
31+
for server_name, handler in self.eventlistener._all_votes.items():
32+
servers.append(server_name)
33+
votes.append(repr(handler.item))
34+
voters.append(str(len(handler.votes)))
35+
if len(servers):
36+
embed = discord.Embed(color=discord.Color.blue())
37+
embed.add_field(name=_("Server"), value='\n'.join(servers))
38+
embed.add_field(name=_("Running Vote"), value='\n'.join(votes))
39+
embed.add_field(name=_("Voters"), value='\n'.join(voters))
40+
# noinspection PyUnresolvedReferences
41+
await interaction.response.send_message(embed=embed, ephemeral=True)
42+
else:
43+
# noinspection PyUnresolvedReferences
44+
await interaction.response.send_message(_("No running votes found."), ephemeral=True)
45+
46+
@vote.command(description=_('Create a vote'))
47+
@app_commands.guild_only()
48+
@utils.app_has_role('DCS')
49+
async def create(self, interaction: discord.Interaction,
50+
server: app_commands.Transform[Server, utils.ServerTransformer(status=[Status.RUNNING])],
51+
what: Literal['Restart', 'Mission Change', 'Weather Change']):
52+
config = self.get_config(server)
53+
# Users with either the "creator" role or "DCS Admin" can use this command
54+
roles = set(config.get('creator', []) + self.bot.roles['DCS Admin'])
55+
if not utils.check_roles(roles, interaction.user):
56+
# noinspection PyUnresolvedReferences
57+
await interaction.response.send_message(_("You are not authorized to create a vote."))
58+
return
59+
is_admin = utils.check_roles(self.bot.roles['DCS Admin'], interaction.user)
60+
61+
points = config.get('credits')
62+
credits = campaign_id = ucid = None
63+
if not is_admin and points:
64+
ucid = await self.bot.get_ucid_by_member(interaction.user)
65+
if not ucid:
66+
_mission = self.bot.cogs['Mission']
67+
# noinspection PyUnresolvedReferences
68+
await interaction.response.send_message(_("Use {} to link your account.").format(
69+
(await utils.get_command(self.bot, name=_mission.linkme.name)).mention
70+
), ephemeral=True)
71+
return
72+
_creditssystem = cast(CreditSystem, self.bot.cogs['CreditSystem'])
73+
data = await _creditssystem.get_credits(ucid)
74+
campaign_id, campaign_name = utils.get_running_campaign(self.node, server)
75+
credits = next((x['credits'] for x in data if x['id'] == campaign_id), 0)
76+
if credits < points:
77+
# noinspection PyUnresolvedReferences
78+
await interaction.response.send_message(
79+
_("You don't have enough credits to create a vote!"), ephemeral=True)
80+
return
81+
82+
if self.eventlistener._all_votes.get(server.name):
83+
# noinspection PyUnresolvedReferences
84+
await interaction.response.send_message(_('There is already a voting running on this server.'),
85+
ephemeral=True)
86+
return
87+
88+
if what == 'Mission Change' and config['options'].get('mission') is not None:
89+
message = _("Vote for a mission change on server {}").format(server.name)
90+
element = 'mission'
91+
elif what == 'Restart' and config['options'].get('restart') is not None:
92+
message = _("Vote for a restart of server {}").format(server.name)
93+
element = 'restart'
94+
elif what == 'Weather Change' and config['options'].get('preset') is not None:
95+
message = _("Vote for a weather change on server {}").format(server.name)
96+
element = 'preset'
97+
else:
98+
# noinspection PyUnresolvedReferences
99+
await interaction.response.send_message(_("Unknown vote type or vote type not configured."), ephemeral=True)
100+
return
101+
102+
if not is_admin and points:
103+
message += "\n_" + _("This vote will cost you {} credits.").format(points) + "_"
104+
105+
if not await utils.yn_question(interaction, question=_("Do you want to create a vote?"), message=message):
106+
await interaction.followup.send(_('Aborted.'))
107+
return
108+
109+
class_name = f"plugins.voting.options.{element}.{element.title()}"
110+
item: VotableItem = utils.str_to_class(class_name)(
111+
server, config['options'].get(element)
112+
)
113+
choices = await item.get_choices()
114+
if len(choices) > 2:
115+
rc = await utils.selection(interaction,
116+
title=_("Active players can vote for any of these options.\n"
117+
"Make your vote now!"),
118+
options=[
119+
SelectOption(label=x, value=str(idx + 2))
120+
for idx, x in enumerate(choices[1:])
121+
if idx < 25
122+
])
123+
if rc is None:
124+
await interaction.followup.send(_("Aborted."))
125+
return
126+
vote = int(rc)
127+
else:
128+
vote = 2
129+
130+
if not item.can_vote():
131+
await interaction.followup.send(_('This option is not available at the moment.'), ephemeral=True)
132+
133+
handler = VotingHandler(listener=self.eventlistener, item=item, server=server, config=config)
134+
self.eventlistener._all_votes[server.name] = handler
135+
handler.votes[vote] = 1
136+
137+
await interaction.followup.send(_('{} created. It is open for {}').format(
138+
repr(item), utils.format_time(config.get('time', 300))))
139+
140+
if not is_admin and points:
141+
async with self.apool.connection() as conn:
142+
await conn.execute("""
143+
UPDATE credits SET points = %s WHERE campaign_id = %s AND player_ucid = %s
144+
""", (credits - points, campaign_id, ucid))
145+
await conn.execute("""
146+
INSERT INTO credits_log (campaign_id, event, player_ucid, old_points, new_points, remark)
147+
VALUES (%s, %s, %s, %s, %s, %s)
148+
""", (campaign_id, 'vote', ucid, credits, credits - points, _("Paid for a vote")))
149+
150+
@vote.command(description=_('Cancel a vote'))
151+
@app_commands.guild_only()
152+
@utils.app_has_role('DCS Admin')
153+
async def cancel(self, interaction: discord.Interaction,
154+
server: app_commands.Transform[Server, utils.ServerTransformer(status=[Status.RUNNING])]):
155+
handler = self.eventlistener._all_votes.get(server.name)
156+
if not handler:
157+
# noinspection PyUnresolvedReferences
158+
await interaction.response.send_message(_('There is no voting running on this server.'), ephemeral=True)
159+
return
160+
handler.cancel()
161+
# noinspection PyUnresolvedReferences
162+
await interaction.response.send_message(_('Voting cancelled.'), ephemeral=utils.get_ephemeral(interaction))
163+
14164

15165
async def setup(bot: DCSServerBot):
16166
await bot.add_cog(Voting(bot, VotingListener))

plugins/voting/listener.py

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717

1818
_ = get_translation(__name__.split('.')[1])
1919

20-
all_votes: dict[str, 'VotingHandler'] = dict()
21-
22-
2320
class VotingHandler:
2421
def __init__(self, listener: 'VotingListener', item: VotableItem, server: Server, config: dict):
2522
self.loop = asyncio.get_event_loop()
@@ -50,7 +47,7 @@ async def print(self, player: Player | None = None):
5047
message += f'{idx + 1}. {element}\n'
5148
message += await self.get_leading_vote()
5249
message += "\n" + _("Use {prefix}{command} <number> to vote for the change.").format(
53-
prefix=self.config['prefix'], command=self.listener.vote.name) + "\n"
50+
prefix=self.listener.prefix, command=self.listener.vote.name) + "\n"
5451
if player:
5552
await player.sendUserMessage(message)
5653
else:
@@ -66,8 +63,9 @@ async def start(self):
6663
for reminder in sorted(self.config.get('reminder', []), reverse=True):
6764
if reminder >= voting_time:
6865
continue
69-
self.tasks.append(self.loop.call_later(voting_time - reminder,
70-
partial(asyncio.create_task, self.remind(reminder))))
66+
self.tasks.append(
67+
self.loop.call_later(voting_time - reminder, lambda r=reminder: asyncio.create_task(self.remind(r)))
68+
)
7169
self.tasks.append(self.loop.call_later(voting_time, lambda: asyncio.create_task(self.end_vote())))
7270

7371
async def vote(self, player: Player, num: int):
@@ -97,8 +95,8 @@ def _get_possible_voters(self) -> int:
9795
async def check_vote(self) -> int:
9896
message = _("Voting finished")
9997
voting_rule = self.config.get('voting_rule', 'majority')
100-
possible_voters = self._get_possible_voters()
101-
if not self.votes or not possible_voters:
98+
possible_voters = self._get_possible_voters() or 1
99+
if not self.votes: # or not possible_voters:
102100
message += _(" without any (active) participant.")
103101
elif (self.config.get('voting_threshold') and
104102
(sum(self.votes.values()) / possible_voters) < self.config.get('voting_threshold')):
@@ -126,19 +124,18 @@ async def check_vote(self) -> int:
126124
return -1
127125

128126
async def end_vote(self):
129-
global all_votes
130-
131127
win_id = await self.check_vote()
132128
if win_id > -1:
133129
winner = next(islice(await self.item.get_choices(), win_id, None))
134130
message = f"\"{winner}\" won with {self.votes[win_id + 1]} votes!"
135131
await self.server.sendChatMessage(Coalition.ALL, message)
136132
await self.server.sendPopupMessage(Coalition.ALL, message)
137133
await self.item.execute(winner)
138-
all_votes.pop(self.server.name, None)
134+
self.listener._all_votes.pop(self.server.name, None)
139135

140136

141137
class VotingListener(EventListener["Voting"]):
138+
_all_votes: dict[str, 'VotingHandler'] = dict()
142139

143140
async def can_run(self, command: ChatCommand, server: Server, player: Player) -> bool:
144141
config = self.get_config(server=server)
@@ -157,9 +154,7 @@ def check_role(self, player: Player, roles: list[str] | None = None) -> bool:
157154
return True
158155

159156
async def do_vote(self, server: Server, player: Player, params: list[str]):
160-
global all_votes
161-
162-
vote = all_votes.get(server.name)
157+
vote = type(self)._all_votes.get(server.name)
163158
if len(params) != 1:
164159
await vote.print(player)
165160
return
@@ -169,8 +164,6 @@ async def do_vote(self, server: Server, player: Player, params: list[str]):
169164
await vote.vote(player, int(params[0]))
170165

171166
async def create_vote(self, server: Server, player: Player, config: dict, params: list[str]):
172-
global all_votes
173-
174167
points = config.get("credits")
175168
# if credits are specified, check that the player has enough
176169
if points and isinstance(player, CreditPlayer) and player.points < points:
@@ -193,7 +186,6 @@ async def create_vote(self, server: Server, player: Player, config: dict, params
193186
await player.sendChatMessage('Usage: {prefix}{command} <{params}>'.format(
194187
prefix=self.prefix, command=self.vote.name, params='|'.join(choices)))
195188
return
196-
config['prefix'] = self.prefix
197189
try:
198190
class_name = f"plugins.voting.options.{what}.{what.title()}"
199191
item: VotableItem = utils.str_to_class(class_name)(
@@ -207,12 +199,14 @@ async def create_vote(self, server: Server, player: Player, config: dict, params
207199
await player.sendChatMessage("This voting option is not available at the moment.")
208200
return
209201
if points and isinstance(player, CreditPlayer):
202+
old_points = player.points
210203
player.points -= points
204+
player.audit('vote', old_points, _("Paid for a vote"))
211205
await player.sendChatMessage(f"Your voting has been created for the cost of {points} credit points.")
212206
except (TypeError, ValueError) as ex:
213207
await player.sendChatMessage(str(ex))
214208
return
215-
all_votes[server.name] = VotingHandler(listener=self, item=item, server=server, config=config)
209+
type(self)._all_votes[server.name] = VotingHandler(listener=self, item=item, server=server, config=config)
216210
await self.bot.audit(f"{player.display_name} called a vote for {what}",
217211
user=player.member or player.ucid, server=server)
218212

@@ -230,13 +224,11 @@ async def onPlayerStart(self, server: Server, data: dict) -> None:
230224

231225
@chat_command(name="vote", help="start a voting or vote for a change")
232226
async def vote(self, server: Server, player: Player, params: list[str]):
233-
global all_votes
234-
235227
config = self.get_config(server=server)
236-
if server.name in all_votes:
228+
if server.name in type(self)._all_votes:
237229
if len(params) == 1 and params[0] == 'cancel':
238230
if utils.check_roles(self.bot.roles['DCS Admin'], player.member):
239-
all_votes.pop(server.name).cancel()
231+
type(self)._all_votes.pop(server.name).cancel()
240232
message = "The voting has been cancelled by an Admin."
241233
await server.sendChatMessage(Coalition.ALL, message)
242234
await server.sendPopupMessage(Coalition.ALL, message)

plugins/voting/options/kick.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ def __init__(self, server: Server, config: dict, params: list[str] | None = None
1313
if not self.player:
1414
raise ValueError('Player "{}" not found.'.format(' '.join(params)))
1515

16+
def __repr__(self) -> str:
17+
return f"Vote to kick player {self.player.name}"
18+
1619
async def print(self) -> str:
1720
return f"You can now vote to kick player {self.player.name} because of misbehaviour."
1821

1922
async def get_choices(self) -> list[str]:
20-
return [f"Kick {self.player.name}", f"Don't kick {self.player.name}"]
23+
return [f"Don't kick {self.player.name}", f"Kick {self.player.name}"]
2124

2225
async def execute(self, winner: str):
2326
if winner.startswith("Don't"):

plugins/voting/options/mission.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class Mission(VotableItem):
1010
def __init__(self, server: Server, config: dict, params: list[str] | None = None):
1111
super().__init__('mission', server, config, params)
1212

13+
def __repr__(self) -> str:
14+
return f"Vote to change mission"
15+
1316
async def print(self) -> str:
1417
return ("You can now vote to change the mission of this server.\n"
1518
"If you vote for the current mission, the mission will be restarted!\n"
@@ -23,10 +26,12 @@ async def get_choices(self) -> list[str]:
2326
async def execute(self, winner: str):
2427
if winner == 'No Change':
2528
return
26-
message = f"The mission will change in 60s."
27-
await self.server.sendChatMessage(Coalition.ALL, message)
28-
await self.server.sendPopupMessage(Coalition.ALL, message)
29-
await asyncio.sleep(60)
29+
if self.server.is_populated():
30+
message = f"The mission will change in 60s."
31+
await self.server.sendChatMessage(Coalition.ALL, message)
32+
await self.server.sendPopupMessage(Coalition.ALL, message)
33+
await asyncio.sleep(60)
34+
3035
for idx, mission in enumerate(await self.server.getMissionList()):
3136
if winner in mission:
3237
asyncio.create_task(self.server.loadMission(mission=idx + 1, modify_mission=False))

plugins/voting/options/preset.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class Preset(VotableItem):
99
def __init__(self, server: Server, config: dict, params: list[str] | None = None):
1010
super().__init__('preset', server, config, params)
1111

12+
def __repr__(self) -> str:
13+
return f"Vote to change preset"
14+
1215
async def print(self) -> str:
1316
return "You can now vote to change the preset of this server."
1417

0 commit comments

Comments
 (0)