diff --git a/.pylintrc b/.pylintrc index 95985b0..44f856b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -572,5 +572,5 @@ min-public-methods=2 # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception +overgeneral-exceptions=builtins.BaseException, + builtins.Exception diff --git a/src/bot.py b/src/bot.py index a8b0a9b..80d2c03 100644 --- a/src/bot.py +++ b/src/bot.py @@ -32,6 +32,7 @@ import help_command import regrade import utils +import spam os.environ['TF_CPP_MIN_LOG_LEVEL'] = '1' @@ -47,6 +48,7 @@ BOT_VERSION=os.getenv('VERSION') print(BOT_VERSION) Test_bot_application_ID = int(os.getenv('TEST_BOT_APP_ID')) +guild_id = int(os.getenv('TEST_GUILD_ID')) ## needed for spam detection TESTING_MODE = None @@ -63,7 +65,6 @@ async def on_ready(): ''' run on bot start-up ''' global TESTING_MODE TESTING_MODE = False - #DiscordComponents(bot) db.connect() db.mutation_query(''' @@ -121,15 +122,25 @@ async def on_ready(): is_active BOOLEAN NOT NULL CHECK (is_active IN (0, 1)) ) ''') - + db.mutation_query(''' + CREATE TABLE IF NOT EXISTS spam_settings ( + warning_num INT, + timeout_num INT, + timeout_min INT, + timeout_hour INT, + timeout_day INT, + time_between_clears INT + ) + ''') event_creation.init(bot) office_hours.init(bot) - await cal.init(bot) + spam.init(bot) #initialize the spam function of the bot so spam.py has + # access to the bot and clearing starts print('Logged in as') print(bot.user.name) print(bot.user.id) print('------') - + await cal.init(bot) ##this needed to be moved below bc otherwise the stuff above is never called ########################### # Function: on_guild_join # Description: run when a the bot joins a guild @@ -222,32 +233,23 @@ async def on_member_remove(member): @bot.event async def on_message(message): ''' run on message sent to a channel ''' - #spam detection - url_data=[] message_links = [] temp=[] ctx = await bot.get_context(message) - print(message.content) - print(message.author.id) - count = 0 - with open("spam.txt", "a",encoding='utf-8') as f: - f.writelines(f"{str(message.author.id)}\n") - - with open("spam.txt","r+",encoding='utf-8') as f: - for line in f: - if line.strip("\n") == str(message.author.id): - count = count+1 - - if count>5: - #await ctx.send("spam;too many messages") --> this feature is commented out right now because it - #litterly tells you that you've sent too many messages every 5 messages you send, which is rarely even spam - f.truncate(0) + member = message.guild.get_member(message.author.id) + instructor = False + for role in member.roles: + if role.name == 'Instructor': + instructor = True + if not instructor: + # Only spam detect on non instructors + await spam.handle_spam(message, ctx, guild_id) # handles spam # allow messages from test bot - print(message.author.bot) - print(message.author.id) - print(Test_bot_application_ID) + #print(message.author.bot) + #print(message.author.id) + #print(Test_bot_application_ID) if message.author.bot and message.author.id == Test_bot_application_ID: ctx = await bot.get_context(message) await bot.invoke(ctx) @@ -395,6 +397,21 @@ async def create_event(ctx): ''' run event creation interface ''' await event_creation.create_event(ctx, TESTING_MODE) +########################### +# Function: create_event +# Description: command to create event and send to event_creation module +# Ensures command author is Instructor +# Inputs: +# - ctx: context of the command +# Outputs: +# - Options to create event +########################### +@bot.command(name='set_spam_settings', help='Allows instructor to set spam settings') +@commands.has_role('Instructor') +async def set_spam_settings(ctx): + ''' run spam setting prompts ''' + await spam.set(ctx) + ########################### # Function: oh # Description: command related office hour and send to office_hours module @@ -688,7 +705,7 @@ async def update_chart(storage, name, link): async def show_stats(ctx): embed = Embed(title="Bot stats", colour=ctx.author.colour, - thumbnail=bot.user.avatar_url, + #thumbnail=bot.user.avatar_url, timestamp=datetime.utcnow()) proc = Process() @@ -954,9 +971,7 @@ async def begin_tests(ctx): if 'office-hour-test' in ch.name), None) if test_oh_chan: await office_hours.close_oh(ctx.guild, 'test') - await office_hours.open_oh(ctx.guild, 'test') - ########################### # Function: end_tests # Description: Finalize automated testing @@ -968,9 +983,7 @@ async def end_tests(ctx): ''' end tests command ''' if ctx.author.id != Test_bot_application_ID: return - await office_hours.close_oh(ctx.guild, 'test') - # TODO maybe use ctx.bot.logout() await ctx.bot.close() # quit(0) diff --git a/src/cal.py b/src/cal.py index f3275b9..31b85ec 100644 --- a/src/cal.py +++ b/src/cal.py @@ -115,7 +115,6 @@ def update_calendar(ctx): async def init(b): ''' initialize the calendar ''' global BOT - BOT = b for guild in BOT.guilds: for channel in guild.text_channels: @@ -126,7 +125,7 @@ async def init(b): await display_events(channel) #close calls on assignments and exams await closecalls.start(channel) - + print('I dont get here') @@ -211,9 +210,9 @@ async def closecalls(ctx): # pragma: no cover if not MSG_CC_NONE: MSG_CC_NONE = await ctx.send(embed=CLOSE_CALL_EMBED_NONE) else: + # otherwise, edit the saved message from earlier await MSG_CC_NONE.edit(embed=CLOSE_CALL_EMBED_NONE) - @closecalls.before_loop async def before(): # pragma: no cover await BOT.wait_until_ready() diff --git a/src/event_creation.py b/src/event_creation.py index 2e77055..6cb0538 100644 --- a/src/event_creation.py +++ b/src/event_creation.py @@ -4,12 +4,15 @@ import datetime from datetime import timedelta from discord.ui import Button, Select, View -from discord import SelectOption, ButtonStyle, Interaction -import validators +from discord import SelectOption, ButtonStyle from discord.utils import get -from discord.ext import tasks, commands +from discord.ext import tasks + +import validators + from utils import EmailUtility + import office_hours import cal import db @@ -50,11 +53,13 @@ def check(m): button_clicked = (await BOT.wait_for('button_click')).custom_id """ async def button_callback(interaction): - await interaction.response.send_message('What would you like the assignment to be called') + await interaction.response.send_message('What would you like the assignment to ' + 'be called') msg = await BOT.wait_for('message', timeout = 60.0, check = check) title = msg.content.strip() - await ctx.send('What is the due date of this assignment?\nEnter in format `MM-DD-YYYY`') + await ctx.send('What is the due date of this assignment?\nEnter in format ' + '`MM-DD-YYYY`') msg = await BOT.wait_for('message', timeout = 60.0, check = check) date = msg.content.strip() try: @@ -130,7 +135,8 @@ async def project_button_callback(interaction): ) await ctx.send('Project successfully created!') await cal.display_events(ctx) ## needed so the calander updates - button3.callback = project_button_callback #assigns the project button to the function directly above + button3.callback = project_button_callback #assigns the project button to the function + # directly above async def exam_button_callback(interaction): await interaction.response.send_message('What is the title of this exam?') @@ -240,7 +246,8 @@ async def office_hour_button_callback(interaction): day_view.add_item(day_select) #function to respond to instructor selection async def instructor_select_callback(interaction): - instructor = instructor_select.values[0] ## assigns instructor with the selected instructor + instructor = instructor_select.values[0] ## assigns instructor with the selected + # instructor print(instructor) await interaction.response.send_message( 'Which day would you like the office hour to be on?', @@ -257,7 +264,8 @@ async def instructor_select_callback(interaction): async def day_select_callback(interaction): day = day_select.values[0] print(day) - await interaction.response.send_message('What is the start time of the office hour?\nEnter in 24-hour format' + + await interaction.response.send_message('What is the start time of the office ' + 'hour?\nEnter in 24-hour format' + ' e.g. an starting at 1:59pm can be inputted as 13:59') msg = await BOT.wait_for('message', timeout = 60.0, check = check) t_start = msg.content.strip() @@ -291,8 +299,10 @@ async def day_select_callback(interaction): ) await ctx.send('Office hour successfully created!') - await cal.display_events(ctx) ## updates the calender channel on discord with new office hours - day_select.callback = day_select_callback ## sets the day selection button to call to the correct function + await cal.display_events(ctx) ## updates the calender channel on discord with + # new office hours + day_select.callback = day_select_callback ## sets the day selection button to call to + # the correct function button4.callback = office_hour_button_callback else: await ctx.author.send('`!create` can only be used in the `instructor-commands` channel') diff --git a/src/qna.py b/src/qna.py index f72dda4..bfa423d 100644 --- a/src/qna.py +++ b/src/qna.py @@ -74,7 +74,7 @@ async def question(ctx, qs): ########################### async def answer(ctx, num, ans): ''' answer the specific question ''' - if int(num) not in QNA.keys(): + if int(num) not in QNA: await ctx.author.send('Invalid question number: ' + str(num)) # delete user msg await ctx.message.delete() diff --git a/src/spam.py b/src/spam.py new file mode 100644 index 0000000..2733a9d --- /dev/null +++ b/src/spam.py @@ -0,0 +1,161 @@ +import asyncio +from datetime import timedelta +import discord +import db + + + +BOT = None +########################### +# Function: set +# Description: prompts the instructor to set the spam settings +# Inputs: +# - ctx: the context of the channel we are on +########################### +async def set(ctx): + if ctx.channel.name == 'instructor-commands': + # only run in the instructor command channel + await ctx.send('How many messages should a user be able to send before they are warned ' + 'of spamming?') + try: + print("lord...") + msg = (await BOT.wait_for('message', timeout=60.0, check=lambda x: x.channel.name == + 'instructor-commands' and x.author.id != BOT.user.id)).content + print(f'this is the fucked msg {msg}') + msg = int(msg) + print(msg) + msg_warn_num = msg + except: + await ctx.send('Invalid entry. Try again...') + return + + await ctx.send('How many messages should a user be able to send before they are put in ' + 'timeout?') + try: + msg_timeout_num = int((await BOT.wait_for('message', timeout=60.0, check=lambda x: + x.channel.name == + 'instructor-commands' and x.author.id != BOT.user.id)).content) + except: + await ctx.send('Invalid entry. Try again...') + return + + await ctx.send( + f'How much time should users have to send {msg_warn_num} messages before they will be ' + f'warned ' + f'and then eventually put in timeout?') + try: + msg_time_clears = int((await BOT.wait_for('message', timeout=60.0, check=lambda x: + x.channel.name == + 'instructor-commands' and x.author.id != BOT.user.id)).content) + except: + await ctx.send('Invalid entry. Try again...') + return + + await ctx.send('How long should a user be placed in timeout when caught spamming? ' + 'Format: min,hours,days Example: 5,0,0 is 5 minutes') + try: + msg_timeout_time = (await BOT.wait_for('message', timeout=60.0, check=lambda x: + x.channel.name == + 'instructor-commands' and x.author.id != BOT.user.id)).content + timeout_time = msg_timeout_time.split(',') + minutes = int(timeout_time[0]) + hours = int(timeout_time[1]) + days = int(timeout_time[2]) + timedelta(seconds=0, minutes=minutes, hours=hours, days=days) + except: + await ctx.send('Invalid entry. Try again...') + return + db.mutation_query( + 'UPDATE spam_settings SET warning_num = ?, timeout_num = ?, timeout_min = ?, ' + 'timeout_hour = ?, timeout_day = ?, time_between_clears = ?', + [msg_warn_num, msg_timeout_num, int(timeout_time[0]), int(timeout_time[1]), + int(timeout_time[2]), msg_time_clears] + ) + await ctx.send('Spam settings successfully updated!') + else: + await ctx.author.send('`!set_spam_settings` can only be used in the `instructor-commands` ' + 'channel') + await ctx.message.delete() +########################### +# Function: clear_spam +# Description: run as a constant task that clears the spam.txt file +# Inputs: +# - None +########################### +async def clear_spam(): + while True: + rows = db.select_query('SELECT * FROM spam_settings') + rows_tuple = rows.fetchall()[0] + time_between_clears = rows_tuple[5] + # print("We cleared") ## for testing purposes + await asyncio.sleep(time_between_clears) # uses a set seconds betweeen spam clearing + with open("spam.txt", "r+", encoding='utf-8') as f: + f.truncate(0) # delete the user_id of the last message sent +########################### +# Function: init +# Description: initializes this module, giving it access to discord bot. Also inits the clear +# spam function and +# puts default values in for the spam settings. +# Inputs: +# - bot: discord bot +# Outputs: None +########################### +def init(bot): + global BOT + BOT = bot + bot.loop.create_task(clear_spam()) # set up a task for the bot to clear out spam from the + # txt file + row = db.select_query('SELECT * FROM spam_settings').fetchall() + if len(row) == 0: + # there is nothing in the database then put defaults in it + warning_num = 4 # number of messages before warning of spam + timeout_num = 5 # number of messages before timeout + timeout_min = 5 # number of minutes in timeout + timeout_hour = 0 # hours in timeout + timeout_day = 0 # days in timout + time_between_clears = 15 + db.mutation_query( + 'INSERT INTO spam_settings VALUES (?, ?, ?, ?, ?, ?)', + [warning_num, timeout_num, timeout_min, timeout_hour, timeout_day, time_between_clears] + ) + +########################### +# Function: handle_spam +# Description: takes a message and determines whether the author who sent that message is spamming +# or not +# Inputs: +# - message: the message sent in the channel +# - ctx: context of the message +# - guild_id: the id of the guild we are in +# Outputs: None +########################### +async def handle_spam(message, ctx, guild_id): + print(message.content) + print(message.author.id) + count = 0 + with open("spam.txt", "a", encoding='utf-8') as f: + f.writelines(f"{str(message.author.id)}\n") + + with open("spam.txt", "r+", encoding='utf-8') as f: + for line in f: + if line.strip("\n") == str(message.author.id): + count = count + 1 + rows = db.select_query('SELECT * FROM spam_settings') + rows_tuple = rows.fetchall()[0] + warning_num = rows_tuple[0] + timeout_num = rows_tuple[1] + if count > timeout_num: + guild = BOT.get_guild(guild_id) + member = guild.get_member(message.author.id) + muted = discord.utils.get(guild.roles, name="Mute") + # await member.add_roles(muted) + seconds = 0 + minutes = rows_tuple[2] + hours = rows_tuple[3] + days = rows_tuple[4] + await member.timeout(timedelta(seconds=seconds, minutes=minutes, hours=hours, + days=days)) + await ctx.send(f"{message.author.name} has been muted") # lets the everyone know who + # was timed out + elif count > warning_num: + await ctx.send(f"Warning, {message.author.name} will be muted if they continue to spam") diff --git a/src/spam.txt b/src/spam.txt index 11c0aa1..e69de29 100644 --- a/src/spam.txt +++ b/src/spam.txt @@ -1 +0,0 @@ -1150159208969408532 diff --git a/test/test_spam.py b/test/test_spam.py index 77e9bae..fb5547c 100644 --- a/test/test_spam.py +++ b/test/test_spam.py @@ -3,6 +3,8 @@ ########################### from time import sleep import discord +from utils import wait_for_msg + ########################### # Function: test # Description: runs each test @@ -12,8 +14,110 @@ # Outputs: None ########################### async def test(testing_bot, guild_id): - await test_spam(testing_bot) + commands_channel = next( + ch for ch in testing_bot.get_guild(guild_id).text_channels if ch.name == 'instructor-commands') + # Add instructor role to bot + guild = testing_bot.get_guild(guild_id) + role = discord.utils.get(guild.roles, name="Instructor") + member = guild.get_member(testing_bot.user.id) + await member.add_roles(role) # so we can test the !set_spam_settings command + + await start_invalid_settings(testing_bot, commands_channel) + await test_valid_settings(testing_bot, commands_channel) + #await test_invalid_timeout_time(testing_bot, commands_channel) + #await test_spam(testing_bot) + +async def start_invalid_settings(testing_bot, commands_channel): + await test_invalid_warning_num(testing_bot, commands_channel) + #await test_invalid_timeout_num(testing_bot, commands_channel) + #await test_invalid_clear_time(testing_bot, commands_channel) + #await test_invalid_timeout_time(testing_bot, commands_channel) + + +async def test_invalid_warning_num(testing_bot, commands_channel): + await commands_channel.send('!set_spam_settings') + await wait_for_msg(testing_bot, commands_channel, 'How many messages should a user ' + 'be able to send before they are warned of spamming?') + # above wait for the expected response, else it throws an exception in the function wait_for_msg + + await commands_channel.send('90hoq03') + await wait_for_msg(testing_bot, commands_channel, 'Invalid entry. Try again...') + + await test_invalid_timeout_num(testing_bot, commands_channel) + + + +async def test_invalid_timeout_num(testing_bot, commands_channel): + await commands_channel.send('!set_spam_settings') + await wait_for_msg(testing_bot, commands_channel, 'How many messages should a user ' + 'be able to send before they are warned of spamming?') + # above wait for the expected response, else it throws an exception in the function wait_for_msg + + await commands_channel.send('4') + await wait_for_msg(testing_bot, commands_channel, 'How many messages should a user ' + 'be able to send before they are put in timeout?') + await commands_channel.send('+') + await wait_for_msg(testing_bot, commands_channel, 'Invalid entry. Try again...') + + await test_invalid_clear_time(testing_bot, commands_channel) + + +async def test_invalid_clear_time(testing_bot, commands_channel): + await commands_channel.send('!set_spam_settings') + await wait_for_msg(testing_bot, commands_channel, 'How many messages should a user ' + 'be able to send before they are warned of spamming?') + # above wait for the expected response, else it throws an exception in the function wait_for_msg + + await commands_channel.send('4') + await wait_for_msg(testing_bot, commands_channel, 'How many messages should a user ' + 'be able to send before they are put in timeout?') + await commands_channel.send('5') + await wait_for_msg(testing_bot, commands_channel, 'How much time should users have to send 4 messages ' + 'before they will be warned and then eventually put in timeout?') + await commands_channel.send('beezleopop') + await wait_for_msg(testing_bot, commands_channel, 'Invalid entry. Try again...') + + await test_invalid_timeout_time(testing_bot, commands_channel) + +async def test_invalid_timeout_time(testing_bot, commands_channel): + await commands_channel.send('!set_spam_settings') + await wait_for_msg(testing_bot, commands_channel, 'How many messages should a user ' + 'be able to send before they are warned of spamming?') + # above wait for the expected response, else it throws an exception in the function wait_for_msg + + await commands_channel.send('4') + await wait_for_msg(testing_bot, commands_channel, 'How many messages should a user ' + 'be able to send before they are put in timeout?') + await commands_channel.send('5') + await wait_for_msg(testing_bot, commands_channel, 'How much time should users have to send 4 messages ' + 'before they will be warned and then eventually put in timeout?') + await commands_channel.send('15') + await wait_for_msg(testing_bot, commands_channel, + 'How long should a user be placed in timeout when caught spamming? ' + 'Format: min,hours,days Example: 5,0,0 is 5 minutes') + await commands_channel.send('1') + await wait_for_msg(testing_bot, commands_channel, 'Invalid entry. Try again...') + + +async def test_valid_settings(testing_bot, commands_channel): + await commands_channel.send('!set_spam_settings') + await wait_for_msg(testing_bot, commands_channel, 'How many messages should a user ' + 'be able to send before they are warned of spamming?') + # above wait for the expected response, else it throws an exception in the function wait_for_msg + + await commands_channel.send('4') + await wait_for_msg(testing_bot, commands_channel, 'How many messages should a user ' + 'be able to send before they are put in timeout?') + await commands_channel.send('5') + await wait_for_msg(testing_bot, commands_channel, 'How much time should users have to send 4 messages ' + 'before they will be warned and then eventually put in timeout?') + await commands_channel.send('15') + await wait_for_msg(testing_bot, commands_channel, 'How long should a user be placed in timeout when caught spamming? ' + 'Format: min,hours,days Example: 5,0,0 is 5 minutes') + await commands_channel.send('5,0,0') + await wait_for_msg(testing_bot, commands_channel, 'Spam settings successfully updated!') +""" async def test_spam(testing_bot): qna_channel = discord.utils.get(testing_bot.get_all_channels(), name='q-and-a') await qna_channel.send('shit') @@ -25,7 +129,7 @@ async def test_spam(testing_bot): await ctx.send("message 6") #messages = await qna_channel.history(limit=1).flatten() messages = [message async for message in qna_channel.history(limit=1)] - +""" diff --git a/test/tests.py b/test/tests.py index 14023c3..acc6e95 100644 --- a/test/tests.py +++ b/test/tests.py @@ -15,6 +15,7 @@ import test_help import test_regrade import test_email_address +import test_spam if platform.system() == 'Windows': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -30,7 +31,6 @@ async def run_tests(): exit_status = 0 await begin_tests() try: - print('testing QNA\n----------') await test_qna.test(testing_bot, TEST_GUILD_ID) print('testing office hours\n----------') @@ -46,15 +46,15 @@ async def run_tests(): print('testing help\n----------') await test_help.test(testing_bot, TEST_GUILD_ID) print('testing regrade\n----------') - #await test_regrade.test(testing_bot, TEST_GUILD_ID) - + await test_regrade.test(testing_bot, TEST_GUILD_ID) print('testing email address configuration\n----------') await test_email_address.test(testing_bot, TEST_GUILD_ID) print('testing chart\n-----------') await test_chart.test(testing_bot, TEST_GUILD_ID) - print('testing email utility\n-----------') #await test_email_utility.test() + print('testing spam\n----------') + await test_spam.test(testing_bot, TEST_GUILD_ID) except AssertionError as ex: print('exception: ', type(ex).__name__ + ':', ex) print('--')