├── .gitignore ├── libs ├── db │ ├── __init__.py │ ├── base.py │ └── signups.py ├── genInvite.py ├── sendMail.py ├── colours.py ├── loadconf.py ├── saferproxyfix.py └── verifyEmail.py ├── inviteSite ├── views │ ├── error.html │ ├── verify.html │ ├── home.html │ ├── email.template │ └── display.html ├── assets │ ├── css │ │ └── style.css │ └── js │ │ └── main.js └── app.py ├── config ├── env.json ├── env.json.dev ├── contributors.json ├── secrets.json.template ├── init.sql ├── config.json └── strings.json ├── requirements.txt ├── hacksoc-bot.service ├── cogs ├── bannedwords.py ├── misc.py ├── site.py ├── crypto.py ├── contributors.py ├── roles.py ├── invites.py └── admin.py ├── README.md ├── prewrittenText └── welcome.txt └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | config/secrets.json 2 | **/__pycache__/ 3 | env/* 4 | **/*.swp 5 | -------------------------------------------------------------------------------- /libs/db/__init__.py: -------------------------------------------------------------------------------- 1 | from libs.db.base import Conn 2 | from libs.db.signups import SignupConn 3 | -------------------------------------------------------------------------------- /inviteSite/views/error.html: -------------------------------------------------------------------------------- 1 |

{{title}}

2 |

{{desc}}

3 | -------------------------------------------------------------------------------- /config/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "guild":722774915605987390, 3 | "channel":{ 4 | "welcome":722780467644072042, 5 | "rules":722780450476785735 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /config/env.json.dev: -------------------------------------------------------------------------------- 1 | { 2 | "guild":838894621051846726, 3 | "channel":{ 4 | "welcome":838894621308354597, 5 | "rules":838894621308354595 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | enhanced-dpy 2 | requests 3 | py3-validate-email 4 | pymysql 5 | flask 6 | flask-limiter 7 | waitress 8 | qrcode 9 | pillow 10 | -------------------------------------------------------------------------------- /hacksoc-bot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Hacksoc Bot 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | User=bot 8 | Group=bot 9 | WorkingDirectory=/opt/hacksoc-bot 10 | ExecStart=/usr/bin/python3 /opt/hacksoc-bot/main.py 11 | Restart=on-failure 12 | RestartSec=5 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /config/contributors.json: -------------------------------------------------------------------------------- 1 | { 2 | "MuirlandOracle":{ 3 | "Blog":"https://muir.land", 4 | "Github":"https://github.com/muirlandoracle", 5 | "Twitter":"https://twitter.com/MuirlandOracle", 6 | "LinkedIn":"https://www.linkedin.com/in/agcyber/" 7 | }, 8 | "Waves":{ 9 | "Blog":"https://wavess.xyz/", 10 | "Github":"https://github.com/waveyyyy", 11 | "Twitter":"https://twitter.com/ItsWavey_", 12 | "LinkedIn":"https://www.linkedin.com/in/callumshanks/" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cogs/bannedwords.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import re 3 | from discord.ext import commands 4 | from libs.loadconf import config, formatHelp 5 | 6 | 7 | 8 | class BannedWords(commands.Cog): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | @commands.Cog.listener() 13 | async def on_message(self, message): 14 | if re.match(".*quidd?it?ch.*", message.content, re.IGNORECASE | re.DOTALL): 15 | await message.delete() 16 | 17 | def setup(bot): 18 | bot.add_cog(BannedWords(bot)) 19 | -------------------------------------------------------------------------------- /cogs/misc.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from libs.loadconf import config, formatHelp 4 | 5 | 6 | 7 | class Misc(commands.Cog): 8 | def __init__(self, bot): 9 | self.bot = bot 10 | 11 | @commands.command( 12 | name="version", 13 | description=formatHelp("version", "desc"), 14 | usage=formatHelp("version", "usage"), 15 | ) 16 | async def contributors(self, ctx): 17 | await ctx.channel.send(config["version"]) 18 | 19 | 20 | 21 | 22 | def setup(bot): 23 | bot.add_cog(Misc(bot)) 24 | -------------------------------------------------------------------------------- /libs/genInvite.py: -------------------------------------------------------------------------------- 1 | import requests, json 2 | from libs.loadconf import getEnv, secrets, config 3 | 4 | def genInvite(): 5 | headers = { 6 | "Content-Type" : "application/json", 7 | "Authorization" : f"Bot {secrets['token']}" 8 | } 9 | params = { 10 | "max_age":config["inviteExpireTime"], 11 | "unique":True 12 | } 13 | channel = getEnv("channel", "welcome") 14 | r = (requests.post(f"https://discordapp.com/api/v6/channels/{channel}/invites", json=params, headers=headers).text) 15 | try: 16 | r = json.loads(r) 17 | except: 18 | return False 19 | return r["code"] 20 | -------------------------------------------------------------------------------- /libs/db/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import pymysql, sys 3 | from libs.loadconf import secrets 4 | 5 | class Conn(): 6 | def __init__(self): 7 | try: 8 | self.dbh = pymysql.connect( 9 | host=secrets["mysqlLoc"], 10 | user=secrets["mysqlUsername"], 11 | password=secrets["mysqlPass"], 12 | db=secrets["mysqlDB"] 13 | ) 14 | except: 15 | print ("Failed to connect to the database") 16 | sys.exit() 17 | self.dictcurs = self.dbh.cursor(pymysql.cursors.DictCursor) 18 | self.curs = self.dbh.cursor() 19 | 20 | def __del__(self): 21 | self.dbh.close() 22 | -------------------------------------------------------------------------------- /inviteSite/views/verify.html: -------------------------------------------------------------------------------- 1 |

Verify

2 |

You should have received an email to your university email address containing a verification code

3 |

Please enter your verification code here:

4 |
5 | 6 | 7 |
8 |

If you did not receive a verification code, click here to try resending the message

9 | -------------------------------------------------------------------------------- /cogs/site.py: -------------------------------------------------------------------------------- 1 | import discord, threading, waitress, os 2 | from discord.ext import commands 3 | from libs.loadconf import config 4 | from inviteSite.app import app 5 | 6 | 7 | class Site(commands.Cog): 8 | def __init__(self, bot): 9 | if os.environ.get("DEV"): 10 | thread = threading.Thread(target=lambda: app.run(host=config['site']['host'], port=config['site']['port'])) 11 | else: 12 | thread = threading.Thread(target=lambda: waitress.serve(app, host=config['site']['host'], port=config['site']['port'])) 13 | thread.setDaemon(True) 14 | thread.start() 15 | 16 | def setup(bot): 17 | bot.add_cog(Site(bot)) 18 | -------------------------------------------------------------------------------- /config/secrets.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "token":"BOT TOKEN GOES HERE", 3 | "flaskSecret":"MAKE SOMETHING UP", 4 | "sendgridAPI":"SENDGRID_API_TOKEN", 5 | "mysqlUsername":"discord", 6 | "mysqlLoc":"localhost", 7 | "mysqlPass":"MYSQL PASSWORD GOES HERE", 8 | "mysqlDB":"discord", 9 | "verifaliaOneID":"VERIFALIA USERNAME", 10 | "verifaliaOneKey":"VERIFALIA PASSWORD", 11 | "verifaliaTwoID":"VERIFALIA USERNAME", 12 | "verifaliaTwoKey":"VERIFALIA PASSWORD", 13 | "verifaliaThreeID":"VERIFALIA USERNAME", 14 | "verifaliaThreeKey":"VERIFALIA PASSWORD", 15 | "verifaliaFourID":"VERIFALIA USERNAME", 16 | "verifaliaFourKey":"VERIFALIA PASSWORD" 17 | } 18 | 19 | -------------------------------------------------------------------------------- /cogs/crypto.py: -------------------------------------------------------------------------------- 1 | import discord, re, time as t 2 | from discord.ext import commands 3 | from libs.loadconf import config, formatHelp 4 | 5 | 6 | time = lambda: int(t.time()) 7 | 8 | class Crypto(commands.Cog): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | self.cryptoLastRun = 0 12 | 13 | @commands.Cog.listener() 14 | async def on_message(self, msg): 15 | if not msg.author.bot and time() - self.cryptoLastRun > config["cryptoCooldown"] and re.search("crypto(?!( +)?(?:graphy|currency))", msg.content, re.IGNORECASE): 16 | self.cryptoLastRun = time() 17 | await msg.reply("Crypto means cryptography!") 18 | 19 | 20 | def setup(bot): 21 | bot.add_cog(Crypto(bot)) 22 | -------------------------------------------------------------------------------- /libs/sendMail.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | from libs.loadconf import secrets 3 | 4 | class SendMail(): 5 | def __enter__(self): 6 | self.handle = smtplib.SMTP_SSL("smtp.sendgrid.net", 465) 7 | self.handle.login("apikey", secrets["sendgridAPI"]) 8 | return self 9 | 10 | def __exit__(self, type, value, tb): 11 | self.handle.quit() 12 | 13 | def sendInviteVerification(self, address, code): 14 | with open("inviteSite/views/email.template") as msgHandle: 15 | msg = msgHandle.read().format(address, code) 16 | self.handle.sendmail("no-reply@hacksoc.co.uk", [address], msg) 17 | 18 | def sendWarning(self, msg): 19 | self.handle.sendmail("no-reply@hacksoc.co.uk", ["secretary@hacksoc.co.uk"], msg) 20 | -------------------------------------------------------------------------------- /inviteSite/views/home.html: -------------------------------------------------------------------------------- 1 |

Welcome to HackSoc!

2 |

Welcome to the Discord portal for the Abertay Ethical Hacking Society.

3 |

Please enter your student ID:

4 |
5 | 6 | 7 |
8 |

We will send an email to your student email address with a verification code to access the server.

9 |

If you don't have a student email, please reach out to us at team@hacksoc.co.uk!

10 | -------------------------------------------------------------------------------- /config/init.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE discord; 2 | CREATE DATABASE discord; 3 | CREATE TABLE discord.signups ( 4 | id INT AUTO_INCREMENT PRIMARY KEY, 5 | discordID VARCHAR(25), 6 | studentID VARCHAR(10), 7 | perms VARCHAR(20), 8 | inviteCode VARCHAR(20), 9 | joinTime DATETIME, 10 | verificationCode VARCHAR(35) UNIQUE, 11 | verificationExpiry INT, 12 | verificationUsed BOOL DEFAULT 0, 13 | genType VARCHAR(10) DEFAULT "AUTO", 14 | inviteUsed BOOL DEFAULT 0 15 | ); 16 | CREATE TABLE discord.permaInvites ( 17 | id INT AUTO_INCREMENT PRIMARY KEY, 18 | inviteCode VARCHAR(20) UNIQUE, 19 | perms VARCHAR(20), 20 | uses INT DEFAULT 0 21 | ); 22 | 23 | CREATE USER 'discord'@'localhost' IDENTIFIED BY 'PUT_A_SECURE_PASSWORD_HERE'; 24 | GRANT ALL ON discord.* TO 'discord'@'localhost'; 25 | -------------------------------------------------------------------------------- /libs/colours.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ 3 | Title: Colours 4 | Type: Helper Module 5 | Purpose: Prettify the Terminal Output 6 | Last Edited By: Sam 7 | Last Updated: 17/05/21 8 | """ 9 | 10 | import sys 11 | 12 | 13 | class Colours: 14 | red = "\033[91m" 15 | green = "\033[92m" 16 | blue = "\033[34m" 17 | orange = "\033[33m" 18 | purple = "\033[35m" 19 | end = "\033[0m" 20 | 21 | @classmethod 22 | def success(self, message): 23 | print(f"{self.green}[+] {message}{self.end}") 24 | 25 | @classmethod 26 | def warn(self, message): 27 | print(f"{self.orange}[*] {message}{self.end}") 28 | 29 | @classmethod 30 | def info(self, message): 31 | print(f"{self.blue}[*] {message}{self.end}") 32 | 33 | @classmethod 34 | def fail(self, message, die=True): 35 | print(f"{self.red}[-] {message}{self.end}") 36 | 37 | if die: 38 | sys.exit(0) 39 | -------------------------------------------------------------------------------- /inviteSite/views/email.template: -------------------------------------------------------------------------------- 1 | From: no-reply@hacksoc.co.uk 2 | To: {} 3 | MIME-Version: 1.0 4 | Content-Type: text/html 5 | Subject: Hacksoc Discord Verification Code 6 | 7 | 8 | 9 | 15 | 16 | 17 | 20 | 21 |
10 | 11 |

Abertay Hackers

12 |

Please use this code to verify yourself in the Discord Signup Portal:

13 | {} 14 |
18 |

If you did not request this email, please feel free to ignore it, or forward it to emailabuse@hacksoc.co.uk.

19 |
22 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix":"!", 3 | "status":"Hack the planet!", 4 | "version":"v1.6", 5 | "cogs":[ 6 | "contributors", 7 | "admin", 8 | "misc", 9 | "invites", 10 | "site", 11 | "roles", 12 | "crypto", 13 | "bannedwords" 14 | ], 15 | "disabledCogs":[], 16 | "commands":[ 17 | "contributors" 18 | ], 19 | "hiddenCommands":[ 20 | "help" 21 | ], 22 | "adminCommands":[ 23 | "rules", 24 | "welcome", 25 | "invite" 26 | ], 27 | "perms":{ 28 | "member":[ 29 | "Verified", 30 | "Member" 31 | ], 32 | "grad":[ 33 | "Verified", 34 | "Grad" 35 | ], 36 | "staff":[ 37 | "Abertay Staff" 38 | ], 39 | "sa":[ 40 | "Student Association" 41 | ], 42 | "fresher":[ 43 | "Verified", 44 | "Member", 45 | "New Student (Fresher)" 46 | ], 47 | "honourarymember":[ 48 | "Verified", 49 | "Grad", 50 | "Honourary Member" 51 | ], 52 | "guest":[ 53 | "Guest" 54 | ] 55 | }, 56 | "selfAssignables":[ 57 | "CTF" 58 | ], 59 | "inviteExpireTime":1800, 60 | "site":{ 61 | "host":"127.0.0.1", 62 | "port":9000 63 | }, 64 | "cryptoCooldown":180 65 | } 66 | -------------------------------------------------------------------------------- /cogs/contributors.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from libs.loadconf import config, strings, contributors as contributorList, formatHelp 4 | 5 | 6 | class Contributors(commands.Cog): 7 | def __init__(self, bot): 8 | self.bot = bot 9 | 10 | # Send the contributors list 11 | @commands.command( 12 | name="contributors", 13 | description=formatHelp("contributors", "desc"), 14 | usage=formatHelp("contributors", "usage"), 15 | ) 16 | async def contributors(self, ctx): 17 | embed = discord.Embed(colour=discord.Colour.green()) 18 | embed.title = "Bot Contributor List" 19 | for i in contributorList.keys(): 20 | text = "" 21 | for j in contributorList[i].keys(): 22 | text += f"__*{j}:*__ {contributorList[i][j]}\n" 23 | if len(text) == 0: 24 | text = "No socials available" 25 | embed.add_field(name=i, value=text, inline=False) 26 | await ctx.channel.send(embed=embed) 27 | 28 | @commands.command(name="github", description=formatHelp("github", "desc"), usage=formatHelp("github", "usage")) 29 | async def github(self, ctx): 30 | embed = discord.Embed(colour=discord.Color.blue()) 31 | embed.set_thumbnail(url="https://github.com/fluidicon.png") 32 | embed.title = "Contribute to the Bot:" 33 | embed.description = strings["links"]["botGithub"] 34 | await ctx.channel.send(embed=embed) 35 | 36 | 37 | def setup(bot): 38 | bot.add_cog(Contributors(bot)) 39 | -------------------------------------------------------------------------------- /inviteSite/views/display.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HackSoc Discord Invite 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 | 24 | {% include page %}{% with messages = get_flashed_messages() %}{% if messages %} 25 |

{{ messages[0] }}

{% endif %}{% endwith %} 26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hacksoc Bot 2 | ## Discord bot for the Abertay Hacksoc [Discord Server](https://discord.hacksoc.co.uk) 3 | 4 | ## Contributions Policy 5 | Contributions from Hacksoc members are welcome! 6 | 7 | Please feel free to fork the public repo to create your own development repo. If you wish to add this into the Hacksoc bot dev server, please speak to the secretary. 8 | 9 | When deploying: 10 | * Go to the [Discord Developer portal](https://discord.com/developers/) and create a bot. Take note of the token! 11 | * Clone your local copy of the repository to a development machine (a linux VM is ideal for this, as you can also create a MySQL Database) 12 | * Create a `config/secrets.json` file (there is a template provided at `config/secrets.json.template`). You will need to fill in all of the fields in this template as directed. 13 | * Update channel/guild IDs in the "env" section of the `config/config.json` file. A pre-populated version of this already exists for the official dev server (speak to the secretary about getting access) 14 | * For dev, please export an environment variable DEV=/path/to/config/file, rather than overwriting the original 15 | * Initialise your MySQL database locally by running `mysql < config/init.sql` -- make sure to update the password!. 16 | * Run the `main.py` script. 17 | * Your bot should now be deployed! 18 | 19 | ## Contribution Thanks 20 | A big thank you to [@CyberSophi](https://twitter.com/CyberSophi) for the Discord Portal design, and to [@Samiser](https://twitter.com/Sam1ser) for taking the time to implement the design for web. 21 | 22 | ## To Do: 23 | - [ ] Add Multi-language support 24 | - [ ] Set up decent logging 25 | - [ ] Add AGM Commands 26 | -------------------------------------------------------------------------------- /prewrittenText/welcome.txt: -------------------------------------------------------------------------------- 1 | Hello there! :wave: 2 | 3 | Welcome to the Abertay Ethical Hacking Society Discord! :woman_technologist::man_technologist: 4 | 5 | You should have already been assigned an appropriate role by the bot when you joined; however, if this is not the case then you must first be assigned an appropriate role before gaining access to associated channels. If the bot hasn't assigned you roles (or has made a mistake) please reach out to one of our {committeeRole} members via private message :slight_smile: 6 | 7 | Each role corresponds to a unique colour, so make sure to request the one appropriate for you! 8 | 9 | Role - Colour 10 | :red_circle: Committee - Pink 11 | :blue_circle: Society Member - Blue 12 | :orange_circle: Graduate - Orange 13 | :green_circle: Previous Committee - Green 14 | :purple_circle: Abertay Staff - Purple 15 | 16 | To ensure all members of the Discord are part of the Abertay Ethical Hacking Society community, we request you use an easily identifiable alias (i.e. first name, initials, handle). Hopefully, this will help provide a more welcoming atmosphere, allowing community members to get to know you better. 17 | 18 | Before you join the server, make sure you have reviewed the rules outlined in <#{rulesChannel}> :pencil:. By joining you are agreeing to follow these rules and guidelines. Anyone found to be in violation of these rules will face the consequences set forth in the Abertay Ethical Hacking Society constitution, which can be found here: {constitution}. 19 | 20 | Here are some other ways you can keep up to date with the Abertay Ethical Hacking Society: 21 | :bird: Twitter: @AbertayHackers :bird: 22 | :desktop: Website: <{site}> :desktop: 23 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Title: Main 4 | Type: Main Program 5 | Purpose: Start the whole thing! 6 | Author: AG | MuirlandOracle 7 | Last Updated: 17/05/2021 (Samiser) 8 | """ 9 | import discord, asyncio 10 | from discord.ext import commands 11 | from discord.ext.commands import CommandNotFound, MissingRequiredArgument 12 | from discord.ext.commands.errors import CheckFailure 13 | from libs.loadconf import config, secrets 14 | from libs.colours import Colours 15 | 16 | #Set Member intent 17 | intents = discord.Intents.default() 18 | intents.members = True 19 | 20 | 21 | 22 | bot = commands.Bot(command_prefix=config["prefix"], intents=intents) 23 | bot.remove_command("help") 24 | 25 | 26 | # Load the cogs 27 | for cog in config["cogs"]: 28 | if cog not in config["disabledCogs"]: 29 | try: 30 | bot.load_extension(f"cogs.{cog}") 31 | Colours.success(f"{cog} loaded successfully") 32 | except Exception as e: 33 | Colours.warn(f"{cog} failed to load: {e}") 34 | else: 35 | Colours.info(f"Skipping {cog}") 36 | 37 | 38 | # On Ready confirmation 39 | @bot.event 40 | async def on_ready(): 41 | if config["status"] != "": 42 | await bot.change_presence(activity=discord.Game(config["status"])) 43 | Colours.success("Bot Started!") 44 | 45 | 46 | # Ignore the annoying errors 47 | @bot.event 48 | async def on_command_error(ctx, error): 49 | error_to_skip = [CommandNotFound, MissingRequiredArgument, CheckFailure] 50 | for error_type in error_to_skip: 51 | if isinstance(error, error_type): 52 | return 53 | raise error 54 | 55 | 56 | 57 | 58 | try: 59 | bot.run(secrets["token"]) 60 | except RuntimeError: 61 | exit() 62 | -------------------------------------------------------------------------------- /cogs/roles.py: -------------------------------------------------------------------------------- 1 | import discord, asyncio 2 | from discord.ext import commands 3 | from libs.loadconf import config, getResponse, formatHelp 4 | 5 | 6 | class Roles(commands.Cog): 7 | def __init__(self, bot): 8 | self.bot = bot 9 | 10 | @commands.command(name="role", description=formatHelp("role", "desc"), usage=formatHelp("role", "usage")) 11 | async def role(self, ctx, reqRole=None): 12 | if not reqRole: 13 | await ctx.send(getResponse("error", "invalidArgumentOptions").format("\n".join(config["selfAssignables"]))) 14 | return 15 | 16 | try: 17 | pos = [i.lower() for i in config["selfAssignables"]].index(reqRole.lower()) 18 | except ValueError: 19 | await ctx.send(getResponse("error", "roleAssignNotAllowed").format(reqRole, "\n".join(config["selfAssignables"]))) 20 | try: 21 | await asyncio.sleep(5) 22 | await msg.delete() 23 | await ctx.message.delete() 24 | return 25 | except: 26 | return 27 | 28 | 29 | reqRole = discord.utils.get(ctx.guild.roles, name=config["selfAssignables"][pos]) 30 | if reqRole in ctx.author.roles: 31 | try: 32 | await ctx.author.remove_roles(reqRole) 33 | msg = await ctx.send(getResponse("success", "roleRemoved")) 34 | except: 35 | msg = await ctx.send(getResponse("error", "roleOpFailed")) 36 | else: 37 | try: 38 | await ctx.author.add_roles(reqRole) 39 | msg = await ctx.send(getResponse("success", "roleAdded")) 40 | except: 41 | msg = await ctx.send(getResponse("error", "roleOpFailed")) 42 | 43 | try: 44 | await asyncio.sleep(5) 45 | await msg.delete() 46 | await ctx.message.delete() 47 | except: 48 | return 49 | 50 | def setup(bot): 51 | bot.add_cog(Roles(bot)) 52 | -------------------------------------------------------------------------------- /libs/loadconf.py: -------------------------------------------------------------------------------- 1 | import json, discord, os 2 | 3 | envLoc = os.environ.get("DEV") 4 | if not envLoc or not os.path.exists(envLoc): 5 | envLoc = "config/env.json" 6 | 7 | # Open all the config files 8 | with open("config/secrets.json") as secretData,\ 9 | open("config/config.json") as configData,\ 10 | open("config/contributors.json") as contribData,\ 11 | open(envLoc) as envData,\ 12 | open("config/strings.json") as stringData: 13 | secrets = json.load(secretData) 14 | config = json.load(configData) 15 | config["env"] = json.load(envData) 16 | strings = json.load(stringData) 17 | contributors = json.load(contribData) 18 | 19 | 20 | def formatHelp(cmd, arg, lang="en"): 21 | return strings[lang]["commands"][cmd][arg].format(config["prefix"]) 22 | 23 | 24 | def getResponse(resType, res, lang="en"): 25 | return strings[lang]["responses"][resType][res] 26 | 27 | 28 | def getEnv(envType, envVal): 29 | return config["env"][envType][envVal] 30 | 31 | 32 | def getGuild(bot): 33 | return bot.get_guild(config["env"]["guild"]) 34 | 35 | 36 | def getRole(bot, roleName): 37 | return discord.utils.get(getGuild(bot).roles, name=roleName) 38 | 39 | 40 | class LoadRules: 41 | def __init__(self, lang="en"): 42 | self.lang = lang 43 | self.rules = strings[self.lang]["rules"]["rules"] 44 | self.title = strings[self.lang]["rules"]["title"] 45 | self.footer = strings[self.lang]["rules"]["footer"].format( 46 | strings["links"]["constitution"] 47 | ) 48 | self.ruleWord = strings[self.lang]["rules"]["ruleWord"] 49 | 50 | def getRule(self, num): 51 | return f"**{self.ruleWord} {num}:** {self.rules[num-1]}" 52 | 53 | def getNumRules(self): 54 | return len(self.rules) 55 | 56 | def getAvailableRoles(): 57 | #Get a list of perms 58 | roles = list(config["perms"].keys()) 59 | roleList = "```\n" 60 | for i in roles[:-1]: 61 | roleList += f"{i}\n" 62 | roleList += f"{roles[-1]}```" 63 | return roles, roleList 64 | -------------------------------------------------------------------------------- /cogs/invites.py: -------------------------------------------------------------------------------- 1 | import discord, os, sys 2 | from discord.ext import commands 3 | from libs.loadconf import config, getGuild, getRole 4 | from libs.db import SignupConn 5 | from libs.colours import Colours 6 | from libs.sendMail import SendMail 7 | 8 | 9 | class Invites(commands.Cog): 10 | def __init__(self, bot): 11 | self.bot = bot 12 | 13 | @commands.Cog.listener() 14 | async def on_member_join(self, member): 15 | conn = SignupConn() 16 | permaInvites = conn.getPermaInvites() 17 | for invite in await getGuild(self.bot).invites(): 18 | if invite.inviter.id == self.bot.user.id and invite.uses > 0: 19 | code = invite.code 20 | if code in permaInvites: 21 | if invite.uses != conn.getPermaUses(code) + 1: 22 | continue 23 | conn.incrementPermaUses(code) 24 | role = conn.getPermaInviteRole(code) 25 | conn.insertPermaJoin(member.id, role, code) 26 | for i in config["perms"][role]: 27 | await member.add_roles(getRole(self.bot, i)) 28 | return 29 | 30 | conn.setUsed(code) 31 | conn.setDiscordID(member.id, code) 32 | role = conn.checkRoleFromInvite(code) 33 | if not role: 34 | Colours.warn("Invalid Invite used") 35 | if not os.environ.get("DEV"): 36 | try: 37 | with SendMail() as s: 38 | s.sendWarning(f"Invalid Invite Used by {member}.\nCode was {invite.code}") 39 | except: 40 | print("Error, could not open email handle", file=sys.stderr) 41 | return 42 | for i in config["perms"][role]: 43 | await member.add_roles(getRole(self.bot, i)) 44 | 45 | await invite.delete() 46 | break 47 | 48 | 49 | def setup(bot): 50 | bot.add_cog(Invites(bot)) 51 | -------------------------------------------------------------------------------- /inviteSite/assets/css/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | * { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | body { 12 | background-color: #222; 13 | color: white; 14 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; 15 | font-size: 110%; 16 | } 17 | 18 | canvas { 19 | z-index: -2; 20 | padding: 0; 21 | margin: 0 auto; 22 | display: block; 23 | } 24 | 25 | a { 26 | text-decoration: none; 27 | color: white; 28 | } 29 | 30 | p { 31 | margin: 20px auto; 32 | width: 500px; 33 | } 34 | 35 | h1 { 36 | text-align: center; 37 | margin: 20px 0; 38 | } 39 | 40 | .centered { 41 | text-align: center; 42 | } 43 | 44 | #logo { 45 | display: block; 46 | margin: 0 auto; 47 | width: 100px; 48 | } 49 | 50 | #outer { 51 | display: table; 52 | position: absolute; 53 | top: 0; 54 | left: 0; 55 | height: 100%; 56 | width: 100%; 57 | } 58 | 59 | #middle { 60 | display: table-cell; 61 | vertical-align: middle; 62 | height: 100%; 63 | } 64 | 65 | #inner-back { 66 | margin: 0; 67 | width: 50%; 68 | height: 100%; 69 | background-color: rgba(0, 0, 38, 0.9); 70 | box-shadow: 0 0 300px 300px rgba(0, 0, 38, 0.9); 71 | border-radius: 0px; 72 | vertical-align: middle; 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | } 77 | 78 | #inner { 79 | padding: 40px; 80 | text-align: justify; 81 | text-justify: inter-word; 82 | } 83 | 84 | input { 85 | float: left; 86 | font-size: 90%; 87 | display: block; 88 | margin: 8px auto; 89 | width: 25%; 90 | padding: 12px 5px; 91 | border: 1px solid #bbb; 92 | border-radius: 5px; 93 | -webkit-appearance:none; 94 | } 95 | 96 | input[type=text] { 97 | padding: 12px 20px; 98 | width: 75%; 99 | box-sizing: border-box; 100 | outline: none; 101 | } 102 | 103 | input[type=text]:focus { 104 | border: 1px solid #555; 105 | transition: border 0.2s; 106 | } 107 | 108 | @media screen and (max-width: 1600px) { 109 | #inner-back { 110 | width: 800px; 111 | } 112 | } 113 | 114 | @media screen and (max-width: 800px) { 115 | #inner-back { 116 | width: 100%; 117 | background-color: rgba(0, 0, 38, 0.85); 118 | box-shadow: 0 0 300px 300px rgba(0, 0, 38, 0.85); 119 | } 120 | } 121 | 122 | @media screen and (max-width: 580px) { 123 | p { 124 | width: 100%; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /libs/saferproxyfix.py: -------------------------------------------------------------------------------- 1 | class SaferProxyFix(object): 2 | """This middleware can be applied to add HTTP proxy support to an 3 | application that was not designed with HTTP proxies in mind. It 4 | sets `REMOTE_ADDR`, `HTTP_HOST` from `X-Forwarded` headers. 5 | If you have more than one proxy server in front of your app, set 6 | num_proxy_servers accordingly 7 | Do not use this middleware in non-proxy setups for security reasons. 8 | get_remote_addr will raise an exception if it sees a request that 9 | does not seem to have enough proxy servers behind it so long as 10 | detect_misconfiguration is True. 11 | The original values of `REMOTE_ADDR` and `HTTP_HOST` are stored in 12 | the WSGI environment as `werkzeug.proxy_fix.orig_remote_addr` and 13 | `werkzeug.proxy_fix.orig_http_host`. 14 | :param app: the WSGI application 15 | """ 16 | 17 | def __init__(self, app, num_proxy_servers=1, detect_misconfiguration=False): 18 | self.app = app 19 | self.num_proxy_servers = num_proxy_servers 20 | self.detect_misconfiguration = detect_misconfiguration 21 | 22 | def get_remote_addr(self, forwarded_for): 23 | """Selects the new remote addr from the given list of ips in 24 | X-Forwarded-For. By default the last one is picked. Specify 25 | num_proxy_servers=2 to pick the second to last one, and so on. 26 | """ 27 | if self.detect_misconfiguration and not forwarded_for: 28 | raise Exception("SaferProxyFix did not detect a proxy server. Do not use this fixer if you are not behind a proxy.") 29 | if self.detect_misconfiguration and len(forwarded_for) < self.num_proxy_servers: 30 | raise Exception("SaferProxyFix did not detect enough proxy servers. Check your num_proxy_servers setting.") 31 | 32 | if forwarded_for and len(forwarded_for) >= self.num_proxy_servers: 33 | return forwarded_for[-1 * self.num_proxy_servers] 34 | 35 | def __call__(self, environ, start_response): 36 | getter = environ.get 37 | forwarded_proto = getter('HTTP_X_FORWARDED_PROTO', '') 38 | forwarded_for = getter('HTTP_X_FORWARDED_FOR', '').split(',') 39 | forwarded_host = getter('HTTP_X_FORWARDED_HOST', '') 40 | environ.update({ 41 | 'werkzeug.proxy_fix.orig_wsgi_url_scheme': getter('wsgi.url_scheme'), 42 | 'werkzeug.proxy_fix.orig_remote_addr': getter('REMOTE_ADDR'), 43 | 'werkzeug.proxy_fix.orig_http_host': getter('HTTP_HOST') 44 | }) 45 | forwarded_for = [x for x in [x.strip() for x in forwarded_for] if x] 46 | remote_addr = self.get_remote_addr(forwarded_for) 47 | if remote_addr is not None: 48 | environ['REMOTE_ADDR'] = remote_addr 49 | if forwarded_host: 50 | environ['HTTP_HOST'] = forwarded_host 51 | if forwarded_proto: 52 | environ['wsgi.url_scheme'] = forwarded_proto 53 | return self.app(environ, start_response) 54 | 55 | -------------------------------------------------------------------------------- /inviteSite/assets/js/main.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('resize', resize, false); 2 | 3 | var cellSize = 30; 4 | var wireLength = 20; 5 | var cutOffLength = 2; 6 | var straightness = 5; 7 | var grid = []; 8 | var gridWidth, gridHeight; 9 | var available = []; 10 | var wires = []; 11 | 12 | var dirs = [ 13 | [-1, -1], [0, -1], [1, -1], 14 | [1, 0], 15 | [1, 1], [0, 1], [-1 , 1], 16 | [-1, 0] 17 | ]; 18 | 19 | function Cell(x, y){ 20 | this.x = x; 21 | this.y = y; 22 | this.available = true; 23 | } 24 | 25 | function Wire(start){ 26 | this.cells = []; 27 | this.cells.push(start); 28 | this.last = findOpenDir(start.x, start.y); 29 | } 30 | 31 | Wire.prototype.render = function(){ 32 | noFill(); 33 | strokeWeight(cellSize/4); 34 | stroke(0, 0, 50); 35 | beginShape(); 36 | for (var i = 0; i < this.cells.length; i++){ 37 | var cell = this.cells[i]; 38 | vertex((cell.x + .5)*cellSize, (cell.y + .5)*cellSize); 39 | } 40 | endShape(); 41 | fill(0); 42 | strokeWeight(cellSize/6); 43 | var end = this.cells.length - 1; 44 | ellipse((this.cells[0].x + .5)*cellSize, (this.cells[0].y + .5)*cellSize, cellSize*.7); 45 | ellipse((this.cells[end].x + .5)*cellSize, (this.cells[end].y + .5)*cellSize, cellSize*.7); 46 | } 47 | 48 | Wire.prototype.generate = function(){ 49 | var hasSpace = true; 50 | while(this.cells.length < wireLength && hasSpace){ 51 | var prevCell = this.cells[this.cells.length-1]; 52 | var tries = [0, 1, -1]; 53 | if (Math.random() > .5) tries = [0, -1, 1]; 54 | var found = false; 55 | hasSpace = false; 56 | 57 | while(tries.length > 0 && !found){ 58 | var mod = tries.splice(Math.floor(Math.pow(Math.random(),straightness)*tries.length), 1)[0]; 59 | var index = this.last+4+mod; 60 | if (index < 0) index += 8; 61 | if (index > 7) index -=8 62 | var dir = dirs[index]; 63 | 64 | var x = dir[0] + prevCell.x; 65 | var y = dir[1] + prevCell.y; 66 | if (x >= 0 && x < gridWidth - 1 && y >= 0 && y < gridHeight - 1){ 67 | var cell = grid[x][y]; 68 | if (cell.available && noCrossOver(index, x, y)){ 69 | this.cells.push(cell); 70 | cell.available = false; 71 | hasSpace = found = true; 72 | this.last = this.last+mod; 73 | if (this.last < 0) this.last += 8; 74 | if (this.last > 7) this.last -= 8; 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | function noCrossOver(index, x, y){ 82 | if (index == 0) return (grid[x+1][y].available || grid[x][y+1].available); 83 | if (index == 2) return (grid[x-1][y].available || grid[x][y+1].available); 84 | if (index == 4) return (grid[x-1][y].available || grid[x][y-1].available); 85 | if (index == 6) return (grid[x+1][y].available || grid[x][y-1].available); 86 | return true; 87 | } 88 | 89 | function findOpenDir(x, y){ 90 | var checks = [0, 1, 2, 3, 4, 5, 6, 7]; 91 | while (checks.length > 0){ 92 | var index = checks.splice(Math.floor(Math.random()*checks.length), 1)[0]; 93 | var dir = dirs[index]; 94 | var x2 = x + dirs[0]; 95 | var y2 = y + dirs[1]; 96 | if (x2 >= 0 && x2 < gridWidth - 1 && y2 >= 0 && y2 < gridHeight - 1){ 97 | if (grid[x2][y2].available) return index; 98 | } 99 | } 100 | return 0; 101 | } 102 | 103 | function setup(){ 104 | createCanvas(500, 500); 105 | ellipseMode(CENTER); 106 | colorMode(HSB, 360, 100, 100); 107 | 108 | resize(); 109 | } 110 | 111 | function recreate(){ 112 | gridWidth = Math.ceil(width/cellSize)+1; 113 | gridHeight = Math.ceil(height/cellSize)+1; 114 | grid = []; 115 | available = []; 116 | wires = []; 117 | 118 | for (var i = 0; i < gridWidth; i++){ 119 | grid.push([]); 120 | for (var j = 0; j < gridHeight; j++){ 121 | var cell = new Cell(i, j); 122 | grid[i][j] = cell; 123 | available.push(cell); 124 | } 125 | } 126 | 127 | while(available.length > 0){ 128 | var cell = available[Math.floor(Math.random()*available.length)]; 129 | cell.available = false; 130 | var wire = new Wire(cell); 131 | wire.generate(); 132 | wires.push(wire); 133 | for (var i = 0; i < wire.cells.length; i++){ 134 | available.splice(available.indexOf(wire.cells[i]), 1); 135 | } 136 | } 137 | draw() 138 | } 139 | 140 | function draw(){ 141 | background(240, 100, 15); 142 | if (grid == undefined) return; 143 | 144 | for (var i = 0; i < wires.length; i++){ 145 | if (wires[i].cells.length > cutOffLength) wires[i].render(); 146 | } 147 | noLoop(); 148 | } 149 | 150 | function resize(){ 151 | resizeCanvas(window.innerWidth, window.innerHeight); 152 | recreate(); 153 | } 154 | -------------------------------------------------------------------------------- /libs/verifyEmail.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | from libs.loadconf import secrets 3 | import json 4 | import requests 5 | from requests.auth import HTTPBasicAuth 6 | from time import sleep 7 | 8 | 9 | def valEmail(email, emptyWallet=""): 10 | accounts = ['One', 11 | 'Two', 12 | 'Three', 13 | 'Four'] 14 | credsURI = 'https://api.verifalia.com/v2.4/credits/balance' 15 | # check which account has an empty wallet (happens if it returns a 402) 16 | if emptyWallet: 17 | accounts.remove(emptyWallet) 18 | # loop each and check each accounts balances 19 | for accountNum in accounts: 20 | authentication = HTTPBasicAuth( 21 | secrets[f'verifalia{accountNum}ID'], secrets[f'verifalia{accountNum}Key']) 22 | response = requests.get(credsURI, auth=authentication) 23 | if response.json()["freeCredits"] >= 1: 24 | # either true or false 25 | return verifaliaAPI(email, accountNum) 26 | 27 | 28 | def verifaliaAPI(email, accountNum): 29 | # the base url is https://api.verifalia.com/v2.4 as it removes the need 30 | # for implementing an auto retry system 31 | 32 | # rate limits are 6 https requests per second with a max burst of 33 | # 15 https requests per second with unlimited concurrent email verification 34 | # jobs. 35 | # this rate limit is not likely to be hit by us 36 | verifyURI = 'https://api.verifalia.com/v2.4/email-validations?waitTime=5000' 37 | authentication = HTTPBasicAuth( 38 | secrets[f'verifalia{accountNum}ID'], secrets[f'verifalia{accountNum}Key']) 39 | headers = {'Content-Type': 'application/json'} 40 | data = { 41 | "entries": [ 42 | { 43 | "inputData": f"{email}" 44 | } 45 | ] 46 | } 47 | response = requests.post( 48 | verifyURI, auth=authentication, headers=headers, data=json.dumps(data)) 49 | if response.status_code == 402: 50 | # responds with 402 if payment is required (when free credits run out) 51 | return valEmail(email, emptyWallet=accountNum) 52 | elif response.status_code == 202: 53 | # responds with 202 if request was queued (shouldn't happen often) 54 | jobID = response.json()["overview"]["id"] 55 | jobURI = f'https://api.verifalia.com/v2.4/email-validations/{jobID}' 56 | # submitting a get request to this endpoint returns information about 57 | # the job such as completion status and validation results for any entries 58 | response = requests.get(jobURI, auth=authentication) 59 | # if response is 202 keep checking every second until it isn't 60 | while response.status_code == 202: 61 | sleep(1) 62 | response = requests.get(jobURI, auth=authentication) 63 | # check if the job is completed 64 | if response.status_code == 200 and response.json()["overview"]["status"] == "Completed": 65 | entryData = response.json()["entries"]["data"][0] 66 | # success and Deliverable indicate the given email address is valid 67 | # any other combination does not constitute a valid email address 68 | if entryData["status"] != 'success' and entryData["classification"] != 'Deliverable': 69 | # print("Invalid Student ID") 70 | # print(entryData["status"]) 71 | # print(entryData["classification"]) 72 | return False 73 | else: 74 | # print("Valid Student ID") 75 | # print(entryData["status"]) 76 | # print(entryData["classification"]) 77 | return True 78 | else: 79 | # if for whatever reason the above checks fail, return false as 80 | # there is likely a problem with the API... (or most likely my code) 81 | return False 82 | 83 | elif response.status_code == 200: 84 | # will contain the verification result 85 | entryData = response.json()["entries"]["data"][0] 86 | # success and Deliverable indicate the given email address is valid 87 | # any other combination does not constitute a valid email address 88 | if entryData["status"] != 'success' and entryData["classification"] != 'Deliverable': 89 | # print("Invalid Student ID") 90 | # print(entryData["status"]) 91 | # print(entryData["classification"]) 92 | return False 93 | else: 94 | # print("Valid Student ID") 95 | # print(entryData["status"]) 96 | # print(entryData["classification"]) 97 | return True 98 | else: 99 | # any other response code indicates a failure with the api or the 100 | # request sent to the api, therefore an email address should be 101 | # reported as invalid. Hopefully this would prompt someone to reach out 102 | # to the committee. 103 | return False 104 | -------------------------------------------------------------------------------- /config/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "en":{ 3 | "rules":{ 4 | "title":"Abertay Hackers Rules", 5 | "footer":"Anyone found to be in violation of these rules will face the consequences set forth in the Abertay Ethical Hacking constitution which can be found here: {}.", 6 | "ruleWord":"Rule", 7 | "rules" : [ 8 | "Obedience with all rules put forth by the Abertay Students Association and the Abertay Ethical Hacking Society.", 9 | "Compliance with the expectation to use an easily identifiable alias (i.e. name, initials or handle).", 10 | "Engage in a considerate manner at all times. Inconsiderate behaviour contains but is not limited to spam, excessive profanity, insults, harassment, and discrimination in any form.", 11 | "Adherence to a SFW environment with usage of the appropriate channel where possible. NSFW content depicting pornographic material, gore and illegal substances are not allowed." 12 | ] 13 | }, 14 | "commands":{ 15 | "contributors":{ 16 | "desc":"List the contributors to the bot", 17 | "usage":"{}contributors" 18 | }, 19 | "github":{ 20 | "desc":"Display the bot's Github link", 21 | "usage":"{}github" 22 | }, 23 | "rules":{ 24 | "desc":"Display the server rules", 25 | "usage":"{}rules [NUMBER]" 26 | }, 27 | "welcome":{ 28 | "desc":"Display the welcome message", 29 | "usage":"{}welcome" 30 | }, 31 | "invite":{ 32 | "desc":"Generate an invite code manually", 33 | "usage":"{}invite staff|sa|member|grad|fresher" 34 | }, 35 | "perma":{ 36 | "desc":"Generate a permanent invite code manually", 37 | "usage":"{}perma staff|sa|member|grad|fresher" 38 | }, 39 | "delPerma":{ 40 | "desc": "Delete a permanent invite code", 41 | "usage":"{}delperma code" 42 | }, 43 | "listPermaInvites":{ 44 | "desc": "List all permanent invites", 45 | "usage":"{}listPermaInvites" 46 | }, 47 | "getInviteQR":{ 48 | "desc": "Generate a QR code for any invite code", 49 | "usage":"{}getInviteCode code" 50 | }, 51 | "updateuser":{ 52 | "desc":"Swap the roles for a user", 53 | "usage":"{}updateuser user staff|sa|member|grad|fresher" 54 | }, 55 | "adduser":{ 56 | "desc":"Add a user to the database", 57 | "usage":"{}adduser user staff|sa|member|grad|fresher" 58 | }, 59 | "version":{ 60 | "desc":"Display the bot version", 61 | "usage":"{}version" 62 | }, 63 | "role":{ 64 | "desc":"Assign yourself a role!\nCurrently only the ctf role is available", 65 | "usage":"{}role CTF\n {}role helper" 66 | } 67 | }, 68 | "responses":{ 69 | "error":{ 70 | "improperPermissions":"You don't have permission to do that", 71 | "ruleOutOfBounds":"There are {} rules. Try one of those :)", 72 | "ruleLessThanZero":"{}? Really? You wanna try that one again, guv?", 73 | "notAnInt":"Argument not an integer", 74 | "invalidUser":"The user requested does not exist or is not a member of the server", 75 | "invalidArgument":"Invalid arguments provided. This command expects {}", 76 | "invalidArgumentOptions":"Invalid arguments provided. Please provide one of the following options:\n```{}```", 77 | "invalidInviteRole":"`{target}` is not a valid role. Pick between: {roleList}", 78 | "roleAssignNotAllowed":"`{}` is not on the list of assignable roles. Pick between:```{}```", 79 | "roleOpFailed":"Role operation failed. Speak to the secretary", 80 | "newRoleSameAsCurrent":"The role you are trying to apply is the same as the user's current role", 81 | "memberPredatesRoleManagement":"This member joined the server before role management was added. Please update their roles manually or use `{}adduser {} {}`.", 82 | "userAlreadyInDB":"The requested user is already in the database. Try `{}updateuser` instead", 83 | "manualAddFail":"Failed to add the user to the database. Please check the logs" 84 | }, 85 | "success":{ 86 | "userRolesUpdated":"User roles have been updated!", 87 | "manualAddSuccess":"User added to database successfully!", 88 | "roleAdded":"Added the role!", 89 | "roleRemoved":"Removed the role!" 90 | }, 91 | "await":{ 92 | "addingUser":"User is being added to the database..." 93 | } 94 | } 95 | }, 96 | "links":{ 97 | "constitution":"https://wiki.hacksoc.co.uk/information/constitution", 98 | "site":"https://hacksoc.co.uk", 99 | "discordPortal":"https://discord.hacksoc.co.uk", 100 | "botGithub":"https://github.com/AbertayHackers/hacksoc-bot" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /inviteSite/app.py: -------------------------------------------------------------------------------- 1 | import re, asyncio, os 2 | from secrets import token_hex as genToken 3 | from flask import Flask, request, session, render_template, flash, redirect, url_for, Markup 4 | from werkzeug.exceptions import HTTPException 5 | from flask_limiter import Limiter 6 | from datetime import timedelta, datetime 7 | from time import time 8 | from libs.loadconf import secrets 9 | from libs.genInvite import genInvite 10 | from libs.saferproxyfix import SaferProxyFix 11 | from libs.db import SignupConn 12 | from libs.sendMail import SendMail 13 | from libs.verifyEmail import valEmail 14 | 15 | 16 | limiter = Limiter(key_func=lambda:request.headers.get("X-Forwarded-For")) 17 | app = Flask("InviteSite") 18 | app.template_folder="inviteSite/views" 19 | app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(hours=6) 20 | app.config["SESSION_COOKIE_SECURE"] = True 21 | app.config["SECRET_KEY"] = secrets["flaskSecret"] 22 | app.wsgi_app = SaferProxyFix(app.wsgi_app) 23 | limiter.init_app(app) 24 | 25 | devModeActive = os.environ.get("DEV") 26 | 27 | @app.route("/") 28 | def registerHome(): 29 | return render_template("display.html", page="home.html"), 200 30 | 31 | @app.route("/verify") 32 | def verify(): 33 | return render_template("display.html", page="verify.html"), 200 34 | 35 | @app.route("/", methods=["POST"]) 36 | @limiter.limit("3/hour,1/second") 37 | def registerLogic(): 38 | studentID = request.form.get("studentID") 39 | if studentID: studentID = re.sub("[^0-9]", "", studentID) 40 | if not studentID or len(studentID) != 7: 41 | flash("Invalid Student ID") 42 | return redirect("/"), 302 43 | #Check to see if there's already an active invite 44 | conn = SignupConn() 45 | if conn.checkPreviousSignup(studentID): 46 | flash("Your student ID has already been used to join the Discord server. Please email us if this is a mistake.") 47 | return redirect("/") 48 | elif conn.checkValidInvites(studentID): 49 | flash(Markup("You already have an active invite pending verification.
Please request a new email if you haven't received a verification email in your university account")) 50 | return redirect(url_for("verify")) 51 | 52 | 53 | email = studentID + "@uad.ac.uk" 54 | if not valEmail(email=email): 55 | flash("Invalid Student ID or your email address hasn't been set up yet. If this is an error, please email us on the address above.") 56 | return redirect("/"), 302 57 | 58 | #Generate a verification token 59 | token = genToken(8) 60 | #print(token) #Used for debugging 61 | if devModeActive: 62 | print(f"Mail Sent... probably\nHave a token: {token}") 63 | else: 64 | try: 65 | with SendMail() as s: 66 | s.sendInviteVerification(email, token) 67 | except: 68 | flash("Failed to send email. Please reach out to team@hacksoc.co.uk") 69 | return redirect("/") 70 | 71 | #Ascertain role to be member or fresher 72 | perm = "fresher" if str(datetime.now().year)[2:] == studentID[:2] else "member" 73 | rowID = conn.autoInvite(studentID, perm, token) 74 | if not rowID: 75 | flash("Something went wrong! Please email us") 76 | return redirect(url_for("registerHome")) 77 | session["registerID"] = rowID 78 | return redirect(url_for("verify")) 79 | 80 | 81 | @app.route("/verify", methods=["POST"]) 82 | @limiter.limit("3/hour,1/second") 83 | def verifyToken(): 84 | token = request.form.get("code") 85 | registerID = session.get("registerID") 86 | if not token or len(token) != 16: 87 | flash("Invalid token") 88 | return redirect(url_for("verify")) 89 | elif not registerID: 90 | flash("No ID found. Please make sure that you have cookies enabled") 91 | return redirect(url_for("verify")) 92 | 93 | conn = SignupConn() 94 | codeInfo = conn.checkVerificationCode(registerID) 95 | if not codeInfo: 96 | flash(Markup("""Code not found.
97 | This can happen if the code does not exist, or has already been used. Please email us on team@hacksoc.co.uk if this is not the case.""")) 98 | return redirect(url_for("verify")) 99 | 100 | if int(time()) > int(codeInfo["verificationExpiry"]): 101 | flash("Verification code expired. Please try again") 102 | return redirect("/") 103 | elif token != codeInfo["verificationCode"]: 104 | flash("Incorrect Token") 105 | return redirect(url_for("verify")) 106 | conn.setVerificationUsed(registerID) 107 | code = genInvite() 108 | conn.insertInvite(registerID, code) 109 | if code: 110 | return redirect(f"https://discord.gg/{code}") 111 | flash("Failed to generate invite!") 112 | return redirect(url_for("verify")) 113 | 114 | @app.route("/resend", strict_slashes=False) 115 | @limiter.limit("3/hour,1/second") 116 | def resend(): 117 | registerID = session.get("registerID") 118 | 119 | if not registerID: 120 | flash("No registration attempt found -- please try again") 121 | return redirect("/") 122 | 123 | conn = SignupConn() 124 | codeInfo = conn.checkVerificationCode(registerID) 125 | if not codeInfo: 126 | flash(Markup("""Code not found.
127 | This can happen if the code does not exist, or has already been used.
128 | Please email us on team@hacksoc.co.uk if this is not the case.""")) 129 | return redirect(url_for("verify")) 130 | 131 | if int(time()) > int(codeInfo["verificationExpiry"]): 132 | flash("Verification code expired. Please try again") 133 | return redirect("/") 134 | 135 | email = codeInfo["studentID"] + "@uad.ac.uk" 136 | if devModeActive: 137 | print(f"Mail Sent... probably... \nEmail: {email}\nToken: {codeInfo['verificationCode']}") 138 | else: 139 | try: 140 | with SendMail() as s: 141 | s.sendInviteVerification(email, token) 142 | except: 143 | flash("Failed to re-send email. Please reach out to team@hacksoc.co.uk") 144 | return redirect(url_for("verify")) 145 | flash(Markup(f"Email re-sent to: {email}")) 146 | return redirect(url_for("verify")) 147 | 148 | @app.errorhandler(HTTPException) 149 | def handleError(e): 150 | return render_template("display.html", page="error.html", title=str(e.code), desc=e.name), e.code 151 | -------------------------------------------------------------------------------- /libs/db/signups.py: -------------------------------------------------------------------------------- 1 | from libs.db import Conn 2 | from time import time 3 | from libs.loadconf import config 4 | 5 | class SignupConn(Conn): 6 | def manualInvite(self, code: str, role: str) -> bool: 7 | sql = """INSERT INTO signups (perms, inviteCode, genType) VALUES (%s, %s, "MANUAL")""" 8 | if not self.curs.execute(sql, (role, code)): 9 | return False 10 | self.dbh.commit() 11 | return True 12 | 13 | def autoInvite(self, studentID: str, perms: str, verificationCode: str) -> bool: 14 | sql = """INSERT INTO signups (studentID, perms, verificationCode, verificationExpiry) VALUES (%s, %s, %s, %s)""" 15 | expires = int(time()) + config["inviteExpireTime"] 16 | if not self.curs.execute(sql, (studentID, perms, verificationCode, expires)): 17 | return False 18 | self.dbh.commit() 19 | return self.curs.lastrowid 20 | 21 | 22 | def checkValidInvites(self, studentID: str): 23 | sql = """SELECT verificationExpiry FROM signups WHERE inviteUsed = 0 AND studentID = %s""" 24 | if not self.curs.execute(sql, (studentID,)): 25 | return False 26 | invites = self.curs.fetchall() 27 | for i in invites: 28 | if i[0] > int(time()): 29 | return True 30 | return False 31 | 32 | def checkVerificationCode(self, rowID: int): 33 | sql = """SELECT studentID, verificationCode, verificationExpiry FROM signups WHERE id = %s AND verificationUsed = 0""" 34 | if not self.dictcurs.execute(sql, (rowID,)): 35 | return False 36 | return self.dictcurs.fetchall()[-1] 37 | 38 | def checkPreviousSignup(self, studentID: str) -> bool: 39 | sql = """SELECT studentID FROM signups WHERE studentID = %s AND inviteUsed = 1""" 40 | if self.curs.execute(sql, (studentID,)): 41 | return True 42 | return False 43 | 44 | def insertInvite(self, registerID: int, code: str) -> bool: 45 | sql = """UPDATE signups SET inviteCode = %s WHERE id = %s""" 46 | if not self.curs.execute(sql, (code, registerID)): 47 | return False 48 | self.dbh.commit() 49 | return True 50 | 51 | def insertPermaJoin(self, discordID, perms, inviteCode): 52 | sql = """INSERT INTO signups (discordID, perms, inviteCode, joinTime, genType, inviteUsed) VALUES (%s, %s, %s, NOW(), "PERMA", 1)""" 53 | if not self.curs.execute(sql, (discordID, perms, inviteCode)): 54 | return False 55 | self.dbh.commit() 56 | return True 57 | 58 | 59 | def setUsed(self, code: str) -> bool: 60 | sql = """UPDATE signups SET inviteUsed = 1 WHERE inviteCode = %s""" 61 | if not self.curs.execute(sql, (code,)): 62 | return False 63 | self.dbh.commit() 64 | return True 65 | 66 | def setVerificationUsed(self, rowID: int) -> bool: 67 | sql = """UPDATE signups SET verificationUsed = 1 WHERE id = %s""" 68 | if not self.curs.execute(sql, (rowID,)): 69 | return False 70 | self.dbh.commit() 71 | return True 72 | 73 | def checkRoleFromInvite(self, code: str): 74 | sql = """SELECT perms FROM signups WHERE inviteCode = %s""" 75 | self.curs.execute(sql, (code,)) 76 | if self.curs.rowcount != 1: 77 | return False 78 | return self.curs.fetchone()[0] 79 | 80 | def checkRoleFromID(self, discordID: int): 81 | sql = """SELECT perms FROM signups WHERE discordID = %s""" 82 | self.curs.execute(sql, (discordID,)) 83 | if self.curs.rowcount < 1: 84 | return False 85 | return self.curs.fetchall()[-1][0] 86 | 87 | def setDiscordID(self, discordID: int, code: str) -> bool: 88 | sql = """UPDATE signups SET discordID = %s, joinTime = NOW() WHERE inviteCode = %s""" 89 | if not self.curs.execute(sql, (discordID, code)): 90 | return False 91 | self.dbh.commit() 92 | return True 93 | 94 | def updateUserRole(self, discordID: int, newRole: str) -> bool: 95 | sql = f"""UPDATE signups SET perms = %s WHERE discordID = %s""" 96 | self.curs.execute(sql, (newRole, discordID)) 97 | self.dbh.commit() 98 | 99 | def manualUserInsert(self, discordID: int, role: str) -> bool: 100 | sql = """INSERT INTO signups (discordID, perms, genType) VALUES (%s, %s, "INSERT")""" 101 | if not self.curs.execute(sql, (discordID, role)): 102 | return False 103 | self.dbh.commit() 104 | return True 105 | 106 | def storePerma(self, code: str, perms: str) -> bool: 107 | sql = """INSERT INTO permaInvites (inviteCode, perms) VALUES (%s, %s)""" 108 | if not self.curs.execute(sql, (code,perms)): 109 | return False 110 | self.dbh.commit() 111 | return True 112 | 113 | def delPerma(self, code: str) -> bool: 114 | sql = """DELETE FROM permaInvites WHERE inviteCode = %s""" 115 | self.curs.execute(sql, (code,)) 116 | if self.curs.rowcount < 1: 117 | return False 118 | self.dbh.commit() 119 | return True 120 | 121 | def getPermaInvites(self, withPerms=False) -> list: 122 | sql = f"""SELECT inviteCode{", perms" if withPerms else ""} FROM permaInvites""" 123 | self.curs.execute(sql) 124 | if self.curs.rowcount == 0: 125 | return [] 126 | res = [] 127 | for i in self.curs.fetchall(): 128 | if withPerms: 129 | res.append(list(i)) 130 | else: 131 | res.append(i[0]) 132 | return res 133 | 134 | def getPermaInviteRole(self, code: str) -> str: 135 | sql = """SELECT perms FROM permaInvites WHERE inviteCode = %s""" 136 | if not self.curs.execute(sql, (code,)): 137 | return False 138 | elif self.curs.rowcount != 1: 139 | return False 140 | 141 | return self.curs.fetchone()[0] 142 | 143 | def getPermaUses(self, code: str): 144 | sql = """SELECT uses FROM permaInvites WHERE inviteCode = %s""" 145 | if not self.curs.execute(sql, (code,)) or self.curs.rowcount != 1: 146 | return False 147 | return self.curs.fetchone()[0] 148 | 149 | def incrementPermaUses(self, code: str) -> bool: 150 | sql = """UPDATE permaInvites SET uses = uses + 1 WHERE inviteCode = %s""" 151 | if not self.curs.execute(sql, (code,)): 152 | return False 153 | elif self.curs.rowcount != 1: 154 | return False 155 | return True 156 | -------------------------------------------------------------------------------- /cogs/admin.py: -------------------------------------------------------------------------------- 1 | import discord, re, qrcode 2 | from discord.ext import commands 3 | from libs.loadconf import ( 4 | config, 5 | strings, 6 | formatHelp, 7 | getResponse, 8 | getAvailableRoles, 9 | LoadRules, 10 | getGuild, 11 | getEnv, 12 | getRole, 13 | ) 14 | from libs.db import SignupConn 15 | from libs.colours import Colours 16 | from io import BytesIO 17 | 18 | cleanID = lambda x: re.sub("[^0-9]", "", x) 19 | 20 | class Admin(commands.Cog): 21 | def __init__(self, bot): 22 | self.bot = bot 23 | 24 | # Check for Committee Role 25 | async def cog_check(self, ctx): 26 | committee = discord.utils.get(ctx.guild.roles, name="Committee") 27 | if not committee in ctx.author.roles: 28 | Colours.warn(f"{ctx.message.author} tried to use an admin command") 29 | await ctx.send(getResponse("error", "improperPermissions")) 30 | return False 31 | return True 32 | 33 | @commands.command( 34 | name="rules", 35 | description=formatHelp("rules", "desc"), 36 | usage=formatHelp("rules", "usage"), 37 | aliases=["rule"] 38 | ) 39 | async def rules(self, ctx, ruleNum=None): 40 | rules = LoadRules() 41 | if not ruleNum: 42 | rulesList = "" 43 | for index, value in enumerate(rules.rules, start=1): 44 | rulesList += f"__**{rules.ruleWord} {index}:**__ {value}\n\n" 45 | 46 | rulesList += rules.footer 47 | embed = discord.Embed( 48 | colour=discord.Colour.green(), title=rules.title, description=rulesList 49 | ) 50 | await ctx.send(embed=embed) 51 | return 52 | 53 | # Assume a specific rule has been requested 54 | try: 55 | ruleNum = int(ruleNum) 56 | except: 57 | await ctx.send(getResponse("error", "notAnInt")) 58 | return 59 | 60 | if ruleNum > rules.getNumRules(): 61 | await ctx.send( 62 | getResponse("error", "ruleOutOfBounds").format(rules.getNumRules()) 63 | ) 64 | return 65 | elif ruleNum < 1: 66 | await ctx.send(getResponse("error", "ruleLessThanZero").format(ruleNum)) 67 | return 68 | 69 | await ctx.send(rules.getRule(ruleNum)) 70 | 71 | @commands.command( 72 | name="welcome", 73 | description=formatHelp("welcome", "desc"), 74 | usage=formatHelp("rules", "usage"), 75 | ) 76 | async def welcome(self, ctx, messageID=None): 77 | rulesChannel = getEnv("channel", "rules") 78 | channel = self.bot.get_channel(getEnv("channel", "welcome")) 79 | committeeRole = getRole(self.bot, "Committee") 80 | with open("prewrittenText/welcome.txt") as msg: 81 | if not messageID: 82 | await channel.send( 83 | msg.read().format( 84 | rulesChannel=rulesChannel, 85 | committeeRole=committeeRole.mention, 86 | constitution=strings["links"]["constitution"], 87 | site=strings["links"]["site"], 88 | ) 89 | ) 90 | else: 91 | try: 92 | message = await channel.fetch_message(messageID) 93 | await message.edit( 94 | content=msg.read().format( 95 | rulesChannel=rulesChannel, 96 | committeeRole=committeeRole.mention, 97 | constitution=strings["links"]["constitution"], 98 | site=strings["links"]["site"], 99 | ) 100 | ) 101 | await ctx.send("Updated the welcome message") 102 | except Exception as e: 103 | await ctx.send(f"Couldn't edit the message: `{e}`") 104 | 105 | 106 | @commands.command(name="invite", description=formatHelp("invite", "desc"), usage=formatHelp("invite", "usage")) 107 | async def invite(self, ctx, target=None): 108 | channel = self.bot.get_channel(getEnv("channel", "welcome")) 109 | roles, roleList = getAvailableRoles() 110 | #Validate input 111 | if target not in roles: 112 | await ctx.send(getResponse("error", "invalidInviteRole").format(target=target, roleList=roleList)) 113 | return 114 | 115 | #Get an ORM session 116 | conn = SignupConn() 117 | #Gen the invite 118 | invite = await channel.create_invite(max_age=0) 119 | await ctx.send(invite) 120 | conn.manualInvite(invite.code, target) 121 | 122 | 123 | @commands.command(name="perma", description=formatHelp("perma", "desc"), usage=formatHelp("perma", "usage")) 124 | async def perma(self, ctx, target=None): 125 | channel = self.bot.get_channel(getEnv("channel", "welcome")) 126 | roles, roleList = getAvailableRoles() 127 | #Validate input 128 | if target not in roles: 129 | await ctx.send(getResponse("error", "invalidInviteRole").format(target=target, roleList=roleList)) 130 | return 131 | 132 | conn = SignupConn() 133 | invite = await channel.create_invite(max_age=0, max_uses=0) 134 | conn.storePerma(invite.code, target) 135 | qr = qrcode.QRCode( 136 | version=1, 137 | error_correction=qrcode.constants.ERROR_CORRECT_L, 138 | box_size=10, 139 | border=2 140 | ) 141 | qr.add_data(invite) 142 | qr.make(fit=True) 143 | img = qr.make_image(fill_color="black", back_color="white") 144 | 145 | with BytesIO() as imgBin: 146 | img.save(imgBin, "PNG") 147 | imgBin.seek(0) 148 | await ctx.send(invite) 149 | await ctx.send(file=discord.File(fp=imgBin, filename="image.png")) 150 | 151 | @commands.command(name="delPerma", description=formatHelp("delPerma", "desc"), usage=formatHelp("delPerma", "usage")) 152 | async def delPerma(self, ctx, code=None): 153 | conn = SignupConn() 154 | code = str(code) 155 | if not conn.delPerma(str(code)): 156 | await ctx.send(f"No invites found with the code: `{code}`") 157 | else: 158 | for i in await ctx.guild.invites(): 159 | if i.code == code: 160 | await i.delete() 161 | await ctx.send(f"Invite deleted") 162 | 163 | @commands.command(name="listPermaInvites", description=formatHelp("listPermaInvites", "desc"), usage=formatHelp("listPermaInvites", "usage")) 164 | async def listPermaInvites(self, ctx): 165 | conn = SignupConn() 166 | invites = conn.getPermaInvites(withPerms=True) 167 | if len(invites) == 0: 168 | await ctx.send("No perma invites") 169 | return 170 | embed = discord.Embed(title="List of Perma Invites:") 171 | for i in invites: 172 | embed.add_field(name="​", value=f"""**Code:** `{i[0]}`\n**Type:** {i[1].capitalize()}""", inline=False) 173 | 174 | await ctx.send(embed=embed) 175 | 176 | @commands.command(name="getInviteQR", description=formatHelp("getInviteQR", "desc"), usage=formatHelp("getInviteQR", "usage")) 177 | async def getInviteQR(self, ctx, code=None): 178 | if not code: 179 | await ctx.send("No code provided") 180 | return 181 | 182 | 183 | qr = qrcode.QRCode( 184 | version=1, 185 | error_correction=qrcode.constants.ERROR_CORRECT_L, 186 | box_size=10, 187 | border=2 188 | ) 189 | qr.add_data(f"https://discord.gg/{code}") 190 | qr.make(fit=True) 191 | img = qr.make_image(fill_color="black", back_color="white") 192 | 193 | with BytesIO() as imgBin: 194 | img.save(imgBin, "PNG") 195 | imgBin.seek(0) 196 | await ctx.send(file=discord.File(fp=imgBin, filename="image.png")) 197 | 198 | @commands.command(name="updateuser", description=formatHelp("updateuser", "desc"), usage=formatHelp("updateuser", "usage")) 199 | async def updateuser(self, ctx, target="", role=None): 200 | roles, roleList = getAvailableRoles() 201 | if role not in roles: 202 | await ctx.send(getResponse("error", "invalidInviteRole").format(target=target, roleList=roleList)) 203 | return 204 | 205 | target = cleanID(target) 206 | if len(target) < 1: 207 | await ctx.send(getResponse("error", "invalidArgument").format("a user ID")) 208 | return 209 | target = int(target) 210 | try: 211 | user = getGuild(self.bot).get_member(target) 212 | except: 213 | await ctx.send(getResponse("error", "invalidUser")) 214 | return 215 | 216 | conn = SignupConn() 217 | currentRole = conn.checkRoleFromID(target) 218 | if not currentRole: 219 | await ctx.send(getResponse("error", "memberPredatesRoleManagement").format(config["prefix"], user.id, role)) 220 | return 221 | elif currentRole == role: 222 | await ctx.send(getResponse("error", "newRoleSameAsCurrent")) 223 | return 224 | conn.updateUserRole(target, role) 225 | 226 | for i in config["perms"][currentRole]: 227 | await user.remove_roles(getRole(self.bot, i)) 228 | for i in config["perms"][role]: 229 | await user.add_roles(getRole(self.bot, i)) 230 | 231 | await ctx.send(getResponse("success","userRolesUpdated")) 232 | 233 | @commands.command(name="adduser", description=formatHelp("adduser", "desc"), usage=formatHelp("adduser", "usage")) 234 | async def adduser(self, ctx, userID = None, role = None): 235 | roles, roleList = getAvailableRoles() 236 | if not userID: 237 | await ctx.send(getResponse("error", "invalidArgument").format("a user ID")) 238 | return 239 | elif role not in roles: 240 | await ctx.send(getResponse("error", "invalidInviteRole").format(target=role, roleList=roleList)) 241 | return 242 | userID = cleanID(userID) 243 | if len(userID) < 1: 244 | await ctx.send(getResponse("error", "invalidArgument").format("a user ID")) 245 | return 246 | userID = int(userID) 247 | user = getGuild(self.bot).get_member(userID) 248 | if not user: 249 | await ctx.send(getResponse("error", "invalidUser")) 250 | return 251 | 252 | conn = SignupConn() 253 | if conn.checkRoleFromID(userID): 254 | await ctx.send(getResponse("error", "userAlreadyInDB").format(config["prefix"])) 255 | return 256 | 257 | #Checks done -- add the user 258 | msg = await ctx.send(getResponse("await", "addingUser")) 259 | if not conn.manualUserInsert(userID, role): 260 | await ctx.send(getResponse("error", "manualAddFail")) 261 | return 262 | for i in config["perms"]: 263 | for j in config["perms"][i]: 264 | await user.remove_roles(getRole(self.bot, j)) 265 | for i in config["perms"][role]: 266 | await user.add_roles(getRole(self.bot, i)) 267 | 268 | await msg.edit(content=getResponse("success", "manualAddSuccess")) 269 | 270 | 271 | def setup(bot): 272 | bot.add_cog(Admin(bot)) 273 | --------------------------------------------------------------------------------