overhaul vote implementation, add emoji-adding

pull/1/head
michael 2019-08-31 20:28:21 -07:00
parent fd65f34770
commit e604ebc487
6 changed files with 130 additions and 166 deletions

3
.gitignore vendored
View File

@ -103,5 +103,8 @@ venv.bak/
# mypy # mypy
.mypy_cache/ .mypy_cache/
# vscode config
.vscode/
# muh token is stored as plain text ehehehehe # muh token is stored as plain text ehehehehe
token.txt token.txt

View File

@ -1,24 +1,8 @@
Karl Marx 2 Karl Marx 2
=========== ===========
Karl Marx 2 takes a somewhat different spin on moderation for smaller servers (~10-35 users), handing the controls back to the users themselves. Instead of a single moderator issuing a command to mute, kick or even *ban* a given user, the choice is left up to anyone and everyone present in the channel. Is the user at hand being annoying or spammy enough to be muted? Are they toxic enough to be kicked/banned? Let's take a vote!
<center><img src="mute_demo.png" width=400/></center> Usage Philosophy
----------------
Karl Marx 2 hands the controls back to the users themselves. Instead of a few moderators or an administrator choosing what the server needs, these types of choices are now left up to the whole of the server. Do you want a custom emoji? Is the someone being toxic enough to be kicked/banned? Let's take a vote!
I didn't have as much time as I would have liked to work on this, but even after Hack Week, I intend to progressively expand its abilities/scalability and even flesh out its functionality beyond just moderation. <!--<center><img src="mute_demo.png" width=400/></center>-->
Made with love. [Enjoy!](https://discordapp.com/oauth2/authorize?client_id=592852914553487370&permissions=8&scope=bot) :)
Installation
------------
1. Clone the repo wherever you want and make sure you have python3 installed with the discord.py package. (If you don't then just `pip3 install discord.py` and you should be good to go!)
2. Grab the token of your frontend bot application and paste into `<path/to/bot>/token.txt`.
3. `python3 bot.py`
Commands
--------
- `>>manual` - Get ye some help
- `>>mute` - Hold a 30-second vote to mute a user for 10 minutes (minimum voters: 4, over 50% majority required). You can set different requirements in `config.json`.
- `>>kick` - Kick a user. The vote is up for 5 minutes, and requires that a minimum of 6 users and >50% approve.
- `>>exile` - Euphamism for banning a user, I guess. By default, the vote lasts 20 minutes, and requires that there be at least 8 votes and a 50% majority. Like the `>>mute` command, you can also tweak settings in `config.json`.
- `>>anthem` - First verse of the Soviet National Anthem. Might need that at some point to show your communist pride.
- `>>ping` - Fun leftovers from when I was first setting up the bot. :P

207
bot.py
View File

@ -7,6 +7,7 @@ import asyncio
import json import json
import discord import discord
from discord.ext import commands from discord.ext import commands
from bot_utils import *
# I know, config parsing is ugly and bad, I'll get around to refactoring later TwT # I know, config parsing is ugly and bad, I'll get around to refactoring later TwT
@ -23,7 +24,11 @@ MIN_KICK_VOTERS = config["MIN_KICK_VOTERS"]
BAN_VOTE_TIME = config["BAN_VOTE_TIME"] BAN_VOTE_TIME = config["BAN_VOTE_TIME"]
MIN_BAN_VOTERS = config["MIN_BAN_VOTERS"] MIN_BAN_VOTERS = config["MIN_BAN_VOTERS"]
EMOTE_VOTE_TIME = config["EMOTE_VOTE_TIME"]
MIN_EMOTE_VOTERS = config["MIN_EMOTE_VOTERS"]
bot = commands.Bot(command_prefix='>>') bot = commands.Bot(command_prefix='>>')
bot.remove_command('help')
# To store users who are currently being voted on # To store users who are currently being voted on
muted_users = [] muted_users = []
@ -33,85 +38,72 @@ banning_users = []
@bot.event @bot.event
async def on_ready(): async def on_ready():
await bot.change_presence(activity=discord.Game(name='>>help'))
print("Bot started.") print("Bot started.")
print("--------------------------") print("--------------------------")
@bot.command(aliases=['manual', 'commands'])
@bot.command() async def help(ctx):
async def manual(ctx):
embed = discord.Embed(title="How 2 Comrade") embed = discord.Embed(title="How 2 Comrade")
embed.add_field( embed.add_field(
name=">>mute", name=">>addEmote <emoji name>",
value="hold a {0}-second vote on whether or not to add a given emote (provided as a message attachment.".format(EMOTE_VOTE_TIME)
)
embed.add_field(
name=">>mute <user>",
value="Hold a {0}-second vote to mute a user for {1} minutes (minimum voters: {2}, over 50% majority required). You can set different requirements in `config.json`.".format(MUTE_VOTE_TIME, int(MUTE_TIME/60), MIN_MUTE_VOTERS) value="Hold a {0}-second vote to mute a user for {1} minutes (minimum voters: {2}, over 50% majority required). You can set different requirements in `config.json`.".format(MUTE_VOTE_TIME, int(MUTE_TIME/60), MIN_MUTE_VOTERS)
) )
embed.add_field( embed.add_field(
name=">>kick", name=">>kick <user>",
value="Kick user. The vote is up for {0} minutes, and requires that a minimum of {1} users and >50% approve.".format(int(KICK_VOTE_TIME/60), MIN_KICK_VOTERS) value="Kick user. The vote is up for {0} minutes, and requires that a minimum of {1} users and >50% approve.".format(int(KICK_VOTE_TIME/60), MIN_KICK_VOTERS)
) )
embed.add_field( embed.add_field(
name=">>exile", name=">>ban <user>",
value="Ban user. By default, the vote lasts {0} minutes, and requires that there be at least {1} votes and a 50% majority. Like the `>>mute`/`>>kick` commands, you can also tweak settings in `config.json`.".format(int(BAN_VOTE_TIME/60), MIN_BAN_VOTERS) value="Ban user. By default, the vote lasts {0} minutes, and requires that there be at least {1} votes and a 50% majority. Like the `>>mute`/`>>kick` commands, you can also tweak settings in `config.json`.".format(int(BAN_VOTE_TIME/60), MIN_BAN_VOTERS)
) )
embed.add_field(
name=">>ping",
value="Get bot latency. Still being implemented."
)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@bot.command()
@bot.command(aliases=['addEmoji'])
async def addEmote(ctx, emote_name: str):
"""
command: addEmote
Hold a vote to add an emoji.
"""
filename = str(ctx.message.attachments[0].filename)
valid_exts = [".jpg", ".jpeg", ".png", ".gif"]
valid = False
for ext in valid_exts:
# print(filename.endswith(ext))
if filename.endswith(ext):
valid = True
break
if not valid:
await ctx.send("Invalid filetype!")
return
vote_passed = await take_vote(ctx, "Add emoji `{}`?".format(emote_name), EMOTE_VOTE_TIME, MIN_EMOTE_VOTERS)
# if all_in_favor > not_in_favor and all_in_favor > MIN_EMOTE_VOTERS:
if vote_passed:
file_bytes = await ctx.message.attachments[0].read()
await ctx.guild.create_custom_emoji(name=emote_name, image=file_bytes)
@bot.command(aliases=['latency'])
async def ping(ctx): async def ping(ctx):
""" await ctx.send("Currently under construction")
command: ping
Use with caution, you might stir up a revolution.
"""
await ctx.send("What do you think I am, some sort of toy? I refuse to bend to the will of the bourgeoisie!")
@bot.command()
async def anthem(ctx):
"""
command: anthem
some people just need a reference, ya know?
"""
await ctx.send("Soyuz nyerushimyiy ryespublik svobodnyikh\n"
"Splotila navyeki Vyelikaya Rus.\n"
"Da zdravstvuyet sozdannyiy volyey narodov\n"
"Yedinyiy, moguchiy Sovyetskiy Soyuz!\n")
# not commands, just some functionality that's used across commands
async def take_vote(ctx, question:str, wait_time):
"""
take_vote(ctx, question:str) - Collects votes
ctx: pass from command function
question: what to ask
returns [<all who want>, <all who don't want>].
It's up to the context/use case to decide how these should be used.
"""
votey_message = await ctx.send(
embed=discord.Embed(
type="rich",
title="NEW VOTE",
description="{}\n\n✅ - Yes\n\n❌ - No".format(question)
)
)
await votey_message.add_reaction('')
await votey_message.add_reaction('')
await asyncio.sleep(wait_time)
finished_votey = await votey_message.channel.fetch_message(votey_message.id)
all_in_favor = 0
not_in_favor = 0
for reaction in finished_votey.reactions:
if str(reaction.emoji) == '':
all_in_favor += reaction.count-1 # don't include bot's reaction
if str(reaction.emoji) == '':
not_in_favor += reaction.count-1
await ctx.send(embed=discord.Embed(type='rich', title="VOTE RESULTS", description="✅ - {0}\n\n❌ - {1}\n".format(all_in_favor, not_in_favor)))
return [all_in_favor, not_in_favor]
@bot.command() @bot.command()
async def mute(ctx, target_user:discord.User): async def mute(ctx, target_user:discord.User):
@ -124,23 +116,16 @@ async def mute(ctx, target_user:discord.User):
return return
muting_users.append(target_user) muting_users.append(target_user)
vote_passed = await take_vote(ctx, "Mute `{}`?".format(target_user), MUTE_VOTE_TIME, MIN_MUTE_VOTERS)
results = await take_vote(ctx, "Mute `{}`?".format(target_user), MUTE_VOTE_TIME)
all_in_favor = results[0]
not_in_favor = results[1]
muting_users.remove(target_user) muting_users.remove(target_user)
if all_in_favor >= MIN_MUTE_VOTERS and all_in_favor - not_in_favor > 0: if vote_passed:
# Add to muted_users # Add to muted_users
muted_users.append(target_user) muted_users.append(target_user)
# add temp. role for mute # add temp. role for mute
muted_role = await ctx.guild.create_role(name="Muted") muted_role = await ctx.guild.create_role(name="Muted")
# edit role position to take precedence over other roles # edit role position to take precedence over other roles
await muted_role.edit(position=ctx.guild.get_member(target_user.id).top_role.position+1) await muted_role.edit(position=ctx.guild.get_member(target_user.id).top_role.position+1)
# change channel permissions for new role # change channel permissions for new role
for channel in ctx.guild.channels: for channel in ctx.guild.channels:
if channel is discord.TextChannel and target_user in channel.members: if channel is discord.TextChannel and target_user in channel.members:
@ -151,43 +136,13 @@ async def mute(ctx, target_user:discord.User):
# Give role to member # Give role to member
await ctx.guild.get_member(target_user.id).add_roles(muted_role) await ctx.guild.get_member(target_user.id).add_roles(muted_role)
await ctx.send("**{0}, the majority has ruled that you should be muted.** See ya in {1} minutes!".format(target_user, int(MUTE_TIME/60)))
endmessage = "**{0}, the majority has ruled that you should be muted.** See ya in {1} minutes!".format(target_user, int(MUTE_TIME/60))
await ctx.send(
embed=discord.Embed(
type='rich',
title="MUTE VOTE VERDICT",
description=endmessage
)
)
await asyncio.sleep(MUTE_TIME) await asyncio.sleep(MUTE_TIME)
await muted_role.delete() await muted_role.delete()
# Remove from muted_users # Remove from muted_users
muted_users.remove(target_user) muted_users.remove(target_user)
return
elif all_in_favor <= not_in_favor:
endmessage = "A >50% vote was not reached."
elif all_in_favor < MIN_MUTE_VOTERS:
endmessage = "Not enough users voted to mute `{0}` (min: {1})".format(target_user, MIN_MUTE_VOTERS)
else:
endmessage = "**`{}` has not been muted.**".format(target_user)
await ctx.send(
embed=discord.Embed(
type='rich',
title="MUTE VOTE VERDICT",
description=endmessage
)
)
@bot.command() @bot.command()
async def kick(ctx, target_user:discord.User): async def kick(ctx, target_user:discord.User):
@ -198,36 +153,16 @@ async def kick(ctx, target_user:discord.User):
# add to kicking_users # add to kicking_users
kicking_users.append(target_user) kicking_users.append(target_user)
results = await take_vote(ctx, "Kick `{}`?".format(target_user), KICK_VOTE_TIME) vote_passed = await take_vote(ctx, "Kick `{}`?".format(target_user), KICK_VOTE_TIME, MIN_KICK_VOTERS)
all_in_favor = results[0]
not_in_favor = results[1]
if all_in_favor > not_in_favor and all_in_favor >= MIN_KICK_VOTERS: # change to 10 later if vote_passed:
await ctx.guild.ban(target_user) await ctx.guild.kick(target_user)
endmessage = "`{}` was kicked.".format(target_user.name)
elif all_in_favor <= not_in_favor:
endmessage = "The majority (>50%) did not decide on kicking `{}`.".format(target_user.name)
elif all_in_favor < MIN_KICK_VOTERS:
endmessage = "Not enough users voted to kick `{0}` (min: {1}).".format(target_user.name, MIN_KICK_VOTERS)
else:
endmessage = "`{}` was not kicked.".format(target_user.name)
kicking_users.remove(target_user) kicking_users.remove(target_user)
await ctx.send(
embed=discord.Embed(
type="rich",
title="KICK VOTE VERDICT",
description=endmessage
)
)
@bot.command(aliases=['exile'])
@bot.command() async def ban(ctx, target_user:discord.User):
async def exile(ctx, target_user:discord.User):
if target_user in banning_users: if target_user in banning_users:
await ctx.send("There is already a ban vote on `{}`!".format(target_user)) await ctx.send("There is already a ban vote on `{}`!".format(target_user))
@ -236,28 +171,12 @@ async def exile(ctx, target_user:discord.User):
# add to banning_users # add to banning_users
banning_users.append(target_user) banning_users.append(target_user)
results = await take_vote(ctx, "Ban `{}`?".format(target_user), BAN_VOTE_TIME) vote_passed = await take_vote(ctx, "Ban `{}`?".format(target_user), BAN_VOTE_TIME, MIN_BAN_VOTERS)
all_in_favor = results[0]
not_in_favor = results[1]
if all_in_favor > not_in_favor and all_in_favor >= MIN_BAN_VOTERS: # change to 10 later if vote_passed:
await ctx.guild.ban(target_user) await ctx.guild.ban(target_user)
endmessage = ":crab: :crab: `{}` IS GONE :crab: :crab:".format(target_user.name) await ctx.send(":crab: :crab: `{}` IS GONE :crab: :crab:".format(target_user.name))
elif all_in_favor <= not_in_favor:
endmessage = "The majority (>50%) did not decide on banning `{}`.".format(target_user.name)
elif all_in_favor < MIN_BAN_VOTERS:
endmessage = "Not enough users voted to ban `{0}` (min: {1}.".format(target_user.name, MIN_BAN_VOTERS)
else:
endmessage = "`{}` was not banned.".format(target_user.name)
banning_users.remove(target_user) banning_users.remove(target_user)
await ctx.send(
embed=discord.Embed(
type="rich",
title="BAN VOTE VERDICT",
description=endmessage
)
)
bot.run(open("token.txt").read().strip()) bot.run(open("token.txt").read().strip())

55
bot_utils.py Normal file
View File

@ -0,0 +1,55 @@
import discord
from discord.ext import commands
import asyncio
async def take_vote(ctx, question:str, wait_time, min_voters):
"""
take_vote(ctx, question:str) - Collects votes
ctx: pass from command function
question: what to ask
returns [<all who want>, <all who don't want>].
It's up to the context/use case to decide how these should be used.
"""
embed_title="NEW VOTE"
votey_message = await ctx.send(
embed=discord.Embed(
type="rich",
title=embed_title,
# description="{}\n\n✅ - Yes\n\n❌ - No".format(question)
description="{0}\n\n✅ - Yes\n❌ - No\nMinimum {1} votes required.".format(question, min_voters)
)
)
await votey_message.add_reaction('')
await votey_message.add_reaction('')
await asyncio.sleep(wait_time)
finished_votey = await votey_message.channel.fetch_message(votey_message.id)
all_in_favor = 0
not_in_favor = 0
for reaction in finished_votey.reactions:
if str(reaction.emoji) == '':
all_in_favor += reaction.count-1 # don't include bot's reaction
if str(reaction.emoji) == '':
not_in_favor += reaction.count-1
# await ctx.send(embed=discord.Embed(type='rich', title="VOTE RESULTS", description="✅ - {0}\n\n❌ - {1}\n".format(all_in_favor, not_in_favor)))
passed = False
if all_in_favor > not_in_favor and all_in_favor >= min_voters:
question += "\nVERDICT: **Vote passed!**"
passed = True
else:
question += "\nVERDICT: **Vote failed!**"
await votey_message.edit(embed=discord.Embed(
type="rich",
title=embed_title,
description=question
))
return passed
async def improper_usage(ctx):
await ctx.send("Improper command usage! See `>>help` for more.")

0
commands.py Normal file
View File

View File

@ -7,5 +7,8 @@
"MIN_KICK_VOTERS" : 6, "MIN_KICK_VOTERS" : 6,
"BAN_VOTE_TIME" : 1200, "BAN_VOTE_TIME" : 1200,
"MIN_BAN_VOTERS" : 8 "MIN_BAN_VOTERS" : 8,
"EMOTE_VOTE_TIME" : 3600,
"MIN_EMOTE_VOTERS" : 5
} }