Code: Select all# This is based on the original votekick.py script as I don't have the
# luxury of a large server to test on, so I'll keep the same license.
# Oh, and since pyspades/pysnip is under the GPL, this is required to be
# under the same license, because it's cancerous like that. Anyway,
# original license below:
# Copyright (c) James Hofmann 2012.
# pyspades is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# pyspades is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with pyspades. If not, see <http://www.gnu.org/licenses/>.
# Modifications copyright (c) Sean Gordon 2013.
# Also, 80-char line length limits are stupid - are we living in the 80s?
from twisted.internet.reactor import seconds
from scheduler import Scheduler
from commands import name, add, get_player, InvalidPlayer
######################
# OPTIONS & MESSAGES #
######################
# Note: You should change these options in the config if possible. These are defaults!
O_REQUIRE_REASON = True
# If a player starts a bad votekick and the threshold (as defined below) of negative votes is met, kick that player
O_KICK_INSTIGATOR = True
# Broadcast votes in chat?
O_SHOW_VOTES = True
O_VOTE_DURATION = 2 * 60 # In seconds
O_VOTE_COOLDOWN = 3 * 60 # In seconds
O_BAN_TIME = 15 # In minutes, set to 0 to kick without banning
O_CHAT_UPDATE_TIME = 30 # In seconds
# Formula for calculating required votes: ROUND(MULTIPLIER * TOTAL_PLAYERS + CONSTANT) where ROUND() rounds to nearest whole number
O_FORMULA_MULTIPLIER = 1/4
O_FORMULA_CONSTANT = 3
# The count can be limited at TOTAL_PLAYERS - 1 to prevent impossible votekick scenarios (e.g. 3 players online, 4 votes required)
O_CAP_REQUIRED_VOTES = True
# Disallow voting if there are less than a certain number of players
O_MINIMUM_PLAYERS = 3
# The only messages you're likely to want to change, if any, are the first two
S_DEFAULT_REASON = "NO REASON GIVEN, BECAUSE I'M LAZY"
S_BAD_VOTEKICK_REASON = "INSTIGATED A BAD VOTEKICK"
S_NO_VOTEKICK = "No votekick in progress"
S_IN_PROGRESS = "Votekick already in progress"
S_SELF_VOTEKICK = "You can't votekick yourself"
S_IMMUNE = "This player can't be votekicked"
S_COOLING_DOWN = "You can't start another votekick yet"
S_REASON_REQUIRED = "You must provide a reason for the votekick"
S_NOT_ENOUGH_PLAYERS = "There aren't enough players to vote"
S_DID_NOT_START = "You didn't start this votekick"
S_VOTE_YES = "{player} voted YES"
S_VOTE_NO = "{player} voted NO"
S_ENDED = "Votekick for {target} has ended. {result}"
S_RESULT_TIMED_OUT = 'Votekick timed out'
S_RESULT_CANCELLED = 'Votekick cancelled'
S_RESULT_BANNED = "Banned by admin"
S_RESULT_KICKED = "Kicked by admin"
S_RESULT_INSTIGATOR_KICKED = "Instigator kicked by admin"
S_RESULT_TARGET_LEFT = "Target {target} left during votekick"
S_RESULT_INSTIGATOR_LEFT = "Instigator {instigator} left during votekick"
S_RESULT_INSTIGATOR_BANNED = "Instigator {instigator} kicked"
S_RESULT_TARGET_BANNED = 'Player {target} kicked'
S_ANNOUNCE = '{instigator} started a VOTEKICK against {target} - Vote by using /Y or /N'
S_ANNOUNCE_SELF = "You started a votekick against {target} - Say /CANCEL to stop it"
S_ANNOUNCE_IRC = "* {instigator} started a votekick against player {target} - Reason: {reason}"
S_UPDATE = '{instigator} is votekicking {target} - Vote by using /Y or /N..'
S_REASON = 'Reason: {reason}'
class VotekickException(Exception):
pass
############
# COMMANDS #
############
@name('votekick')
def start_votekick(connection, *args):
protocol = connection.protocol
if connection not in protocol.players:
raise KeyError()
player = connection
if not args:
if protocol.votekick:
# player requested votekick info
protocol.votekick.send_chat_update(player)
return
else:
raise ValueError()
value = args[0]
try:
target = get_player(protocol, '#' + value)
except InvalidPlayer:
target = get_player(protocol, value)
reason = " ".join(args[1:])
try:
votekick = Votekick.start(player, target, reason)
protocol.votekick = votekick
except VotekickException as err:
return str(err)
@name('cancel')
def cancel_votekick(connection):
protocol = connection.protocol
votekick = protocol.votekick
if not votekick:
return S_DID_NOT_START
if connection in protocol.players:
player = connection
if player is votekick.instigator or player.admin or player.rights.cancel:
#TODO: Check if instigator is losing vote, so they don't cancel it just to stop themselves being kicked
votekick.end(S_RESULT_CANCELLED)
else:
return S_CANT_CANCEL
@name('y')
def vote_yes(connection):
protocol = connection.protocol
if connection not in protocol.players:
raise KeyError()
player = connection
votekick = protocol.votekick
if not votekick:
return S_NO_VOTEKICK
else:
votekick.vote(player, True)
@name('n')
def vote_no(connection):
protocol = connection.protocol
if connection not in protocol.players:
raise KeyError()
player = connection
votekick = protocol.votekick
if not votekick:
return S_NO_VOTEKICK
else:
votekick.vote(player, False)
add(start_votekick)
add(cancel_votekick)
add(vote_yes)
add(vote_no)
class Votekick(object):
schedule = None
def _get_votes_remaining(self):
return self.protocol.get_required_votes() - len([a for a,b in d.iteritems() if b]) + 1
def _get_votes_remaining_no(self):
return self.protocol.get_required_votes() - len([a for a,b in d.iteritems() if not b) + 1
votes_remaining = property(_get_votes_remaining)
votes_remaining_no = property(_get_votes_remaining_no)
@classmethod
def start(cls, instigator, target, reason = None):
protocol = instigator.protocol
last_votekick = instigator.last_votekick
if reason then:
reason = reason.strip()
if protocol.votekick:
raise VotekickException(S_IN_PROGRESS)
elif instigator is target:
raise VotekickException(S_SELF_VOTEKICK)
elif not protocol.is_enough_players():
raise VotekickException(S_NOT_ENOUGH_PLAYERS)
elif target.admin or target.rights.cancel:
raise VotekickException(S_IMMUNE)
elif not instigator.admin and (last_votekick is not None and seconds() - last_votekick < cls.interval):
raise VotekickException(S_COOLING_DOWN)
elif self.require_reason and not reason:
raise VotekickException(S_REASON_REQUIRED)
result = protocol.on_votekick_start(instigator, target, reason)
if result is not None:
raise VotekickException(result)
reason = reason or S_DEFAULT_REASON
return cls(instigator, target, reason)
def __init__(self, instigator, target, reason):
self.protocol = protocol = instigator.protocol
self.instigator = instigator
self.target = target
self.reason = reason
self.votes = {instigator : True}
self.ended = False
protocol.irc_say(S_ANNOUNCE_IRC.format(instigator = instigator.name,
target = target.name, reason = self.reason))
protocol.send_chat(S_ANNOUNCE.format(instigator = instigator.name,
target = target.name), sender = instigator)
protocol.send_chat(S_REASON.format(reason = self.reason),
sender = instigator)
instigator.send_chat(S_ANNOUNCE_SELF.format(target = target.name))
schedule = Scheduler(protocol)
schedule.call_later(self.duration, self.end, S_RESULT_TIMED_OUT)
schedule.loop_call(self.chat_update_time, self.send_chat_update)
self.schedule = schedule
def vote(self, player, vote):
if self.target is player:
return
if self.public_votes:
self.protocol.send_chat((S_VOTE_YES if vote else S_VOTE_NO).format(player = player.name))
self.votes[player] = vote
if self.votes_remaining <= 0:
# vote passed, ban or kick accordingly
target = self.target
self.end(S_RESULT_TARGET_BANNED)
print '%s votekicked' % target.name
if self.ban_duration > 0.0:
target.ban(self.reason, self.ban_duration)
else:
target.kick(silent = True)
elif self.votes_remaining_no <= 0:
# vote failed badly, ban or kick accordingly
target = self.instigator
self.end(S_RESULT_INSTIGATOR_BANNED
print '%s votekicked' % target.name
if self.ban_duration > 0.0:
target.ban(S_BAD_VOTEKICK_REASON, self.ban_duration)
else:
target.kick(silent = True)
def release(self):
self.instigator = None
self.target = None
self.votes = None
if self.schedule:
self.schedule.reset()
self.schedule = None
self.protocol.votekick = None
def end(self, result):
self.ended = True
message = S_ENDED.format(target = self.target.name, result = result)
self.protocol.send_chat(message, irc = True)
if not self.instigator.admin:
self.instigator.last_votekick = seconds()
self.protocol.on_votekick_end()
self.release()
def send_chat_update(self, target = None):
# send only to target player if provided, otherwise broadcast to server
target = target or self.protocol
target.send_chat(S_UPDATE.format(instigator = self.instigator.name, target = self.target.name, needed = self.votes_remaining))
target.send_chat(S_REASON.format(reason = self.reason))
def apply_script(protocol, connection, config):
# Using the same config keys as the other version where possible
Votekick.require_reason = config.get('votekick_require_reason', O_REQUIRE_REASON)
Votekick.kick_instigator = config.get('votekick_kick_instigator', O_KICK_INSTIGATOR)
Votekick.show_votes = config.get('votekick_public_votes', O_SHOW_VOTES)
Votekick.vote_duration = config.get('votekick_vote_duration', O_VOTE_DURATION)
Votekick.cooldown = config.get('votekick_cooldown', O_VOTE_COOLDOWN)
Votekick.ban_duration = config.get('votekick_ban_duration', O_BAN_TIME)
Votekick.chat_update_time = config.get('votekick_chat_update_time', O_CHAT_UPDATE_TIME)
formula_multiplier = config.get('votekick_formula_multiplier', O_FORMULA_MULTIPLIER)
formula_constant = config.get('votekick_formula_constant', O_FORMULA_CONSTANT)
cap_required_votes = config.get('votekick_cap_required_votes', O_CAP_REQUIRED_VOTES)
minimum_players = config.get('votekick_minimum_players', O_MINIMUM_PLAYERS)
class VotekickProtocol(protocol):
votekick = None
def get_player_count(self):
return sum(not player.disconnected for player in self.players.itervalues())
def is_enough_players(self):
return self.get_player_count() >= self.minimum_players
def get_required_votes(self):
player_count = self.get_player_count
threshold = self.formula_multiplier * player_count + self.formula_constant
if self.cap_required_votes and threshold > player_count - 1:
threshold = player_count - 1
def on_map_leave(self):
if self.votekick:
self.votekick.release()
protocol.on_map_leave(self)
def on_ban(self, banee, reason, duration):
votekick = self.votekick
if votekick and votekick.target is self:
votekick.end(S_RESULT_BANNED)
protocol.on_ban(self, connection, reason, duration)
def on_votekick_start(self, instigator, target, reason):
pass
def on_votekick_end(self):
pass
class VotekickConnection(connection):
last_votekick = None
def on_disconnect(self):
votekick = self.protocol.votekick
if votekick:
if votekick.target is self:
# target leaves, gets votekick ban
reason = votekick.reason
votekick.end(S_RESULT_TARGET_LEFT.format(target = self.name))
self.ban(reason, Votekick.ban_duration)
elif votekick.instigator is self:
# instigator leaves, votekick is called off
s = S_RESULT_INSTIGATOR_LEFT.format(instigator = self.name)
votekick.end(s)
else:
# make sure we still have enough players
votekick.votes.pop(self, None)
if votekick.votes_remaining <= 0:
votekick.end(S_NOT_ENOUGH_PLAYERS)
connection.on_disconnect(self)
def kick(self, reason = None, silent = False):
votekick = self.protocol.votekick
if votekick:
if votekick.target is self:
votekick.end(S_RESULT_KICKED)
elif votekick.instigator is self:
votekick.end(S_RESULT_INSTIGATOR_KICKED)
connection.kick(self, reason, silent)
return VotekickProtocol, VotekickConnection
Edit: It's also possible that I've missed something crucial and not realised.