From e604ebc487151a9d2c50e440a2eda36529c4006c Mon Sep 17 00:00:00 2001 From: turtlebasket Date: Sat, 31 Aug 2019 20:28:21 -0700 Subject: [PATCH] overhaul vote implementation, add emoji-adding --- .gitignore | 5 +- README.md | 24 +----- bot.py | 207 ++++++++++++++++----------------------------------- bot_utils.py | 55 ++++++++++++++ commands.py | 0 config.json | 5 +- 6 files changed, 130 insertions(+), 166 deletions(-) create mode 100644 bot_utils.py create mode 100644 commands.py diff --git a/.gitignore b/.gitignore index a1071e7..24f1150 100644 --- a/.gitignore +++ b/.gitignore @@ -103,5 +103,8 @@ venv.bak/ # mypy .mypy_cache/ +# vscode config +.vscode/ + # muh token is stored as plain text ehehehehe -token.txt +token.txt \ No newline at end of file diff --git a/README.md b/README.md index 6bc6428..47b4a5e 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,8 @@ 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! -
+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. - -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 `/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 + \ No newline at end of file diff --git a/bot.py b/bot.py index 2cbcd6f..132a8f1 100644 --- a/bot.py +++ b/bot.py @@ -7,6 +7,7 @@ import asyncio import json import discord 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 @@ -23,7 +24,11 @@ MIN_KICK_VOTERS = config["MIN_KICK_VOTERS"] BAN_VOTE_TIME = config["BAN_VOTE_TIME"] 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.remove_command('help') # To store users who are currently being voted on muted_users = [] @@ -33,85 +38,72 @@ banning_users = [] @bot.event async def on_ready(): + await bot.change_presence(activity=discord.Game(name='>>help')) print("Bot started.") print("--------------------------") - -@bot.command() -async def manual(ctx): +@bot.command(aliases=['manual', 'commands']) +async def help(ctx): embed = discord.Embed(title="How 2 Comrade") embed.add_field( - name=">>mute", + name=">>addEmote ", + 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 ", 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( - name=">>kick", + name=">>kick ", 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( - name=">>exile", + name=">>ban ", 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) -@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): - """ - 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 [, ]. - 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] + await ctx.send("Currently under construction") @bot.command() async def mute(ctx, target_user:discord.User): @@ -124,23 +116,16 @@ async def mute(ctx, target_user:discord.User): return muting_users.append(target_user) - - results = await take_vote(ctx, "Mute `{}`?".format(target_user), MUTE_VOTE_TIME) - all_in_favor = results[0] - not_in_favor = results[1] - + vote_passed = await take_vote(ctx, "Mute `{}`?".format(target_user), MUTE_VOTE_TIME, MIN_MUTE_VOTERS) 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 muted_users.append(target_user) - # add temp. role for mute muted_role = await ctx.guild.create_role(name="Muted") - # 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) - # change channel permissions for new role for channel in ctx.guild.channels: 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 await ctx.guild.get_member(target_user.id).add_roles(muted_role) - - 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 ctx.send("**{0}, the majority has ruled that you should be muted.** See ya in {1} minutes!".format(target_user, int(MUTE_TIME/60))) await asyncio.sleep(MUTE_TIME) - await muted_role.delete() # Remove from muted_users 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() async def kick(ctx, target_user:discord.User): @@ -198,36 +153,16 @@ async def kick(ctx, target_user:discord.User): # add to kicking_users kicking_users.append(target_user) - results = await take_vote(ctx, "Kick `{}`?".format(target_user), KICK_VOTE_TIME) - all_in_favor = results[0] - not_in_favor = results[1] + vote_passed = await take_vote(ctx, "Kick `{}`?".format(target_user), KICK_VOTE_TIME, MIN_KICK_VOTERS) - if all_in_favor > not_in_favor and all_in_favor >= MIN_KICK_VOTERS: # change to 10 later - await ctx.guild.ban(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) + if vote_passed: + await ctx.guild.kick(target_user) kicking_users.remove(target_user) - await ctx.send( - embed=discord.Embed( - type="rich", - title="KICK VOTE VERDICT", - description=endmessage - ) - ) - -@bot.command() -async def exile(ctx, target_user:discord.User): +@bot.command(aliases=['exile']) +async def ban(ctx, target_user:discord.User): if target_user in banning_users: 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 banning_users.append(target_user) - results = await take_vote(ctx, "Ban `{}`?".format(target_user), BAN_VOTE_TIME) - all_in_favor = results[0] - not_in_favor = results[1] + vote_passed = await take_vote(ctx, "Ban `{}`?".format(target_user), BAN_VOTE_TIME, MIN_BAN_VOTERS) - 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) - endmessage = ":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) + await ctx.send(":crab: :crab: `{}` IS GONE :crab: :crab:".format(target_user.name)) 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()) diff --git a/bot_utils.py b/bot_utils.py new file mode 100644 index 0000000..e481bf1 --- /dev/null +++ b/bot_utils.py @@ -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 [, ]. + 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.") \ No newline at end of file diff --git a/commands.py b/commands.py new file mode 100644 index 0000000..e69de29 diff --git a/config.json b/config.json index 8207534..d6af584 100644 --- a/config.json +++ b/config.json @@ -7,5 +7,8 @@ "MIN_KICK_VOTERS" : 6, "BAN_VOTE_TIME" : 1200, - "MIN_BAN_VOTERS" : 8 + "MIN_BAN_VOTERS" : 8, + + "EMOTE_VOTE_TIME" : 3600, + "MIN_EMOTE_VOTERS" : 5 }