├── .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 |
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 |
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 |
10 |
11 | Abertay Hackers
12 | Please use this code to verify yourself in the Discord Signup Portal:
13 | {}
14 | |
15 |
16 |
17 | |
18 | If you did not request this email, please feel free to ignore it, or forward it to emailabuse@hacksoc.co.uk.
19 | |
20 |
21 |
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 |
--------------------------------------------------------------------------------