Add files via upload

This commit is contained in:
Sleepingpirates
2020-06-17 21:23:10 -04:00
committed by GitHub
parent 3cb5594662
commit a9bdbf800c
20 changed files with 1372 additions and 0 deletions

19
app/app/__init__.py Normal file
View File

@@ -0,0 +1,19 @@
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_login import LoginManager
app = Flask(__name__)
SECRET_KEY = os.urandom(32)
app.config['SECRET_KEY'] = SECRET_KEY
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///bot/app.db'
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
login_manager.login_message_category = 'info'
from app import routes

194
app/app/bot/Invitarr.py Normal file
View File

@@ -0,0 +1,194 @@
#Copyright 2020 Sleepingpirate.
import os
from os import environ
import discord
from discord.ext import commands
import asyncio
from plexapi.myplex import MyPlexAccount
from discord import Webhook, AsyncWebhookAdapter
import aiohttp
from dotenv import load_dotenv
import configparser
CONFIG_PATH = 'app/config/config.ini'
BOT_SECTION = 'bot_envs'
try:
config = configparser.ConfigParser()
config.read(CONFIG_PATH)
except:
print("Cannot find config")
# settings
Discord_bot_token = config.get(BOT_SECTION, 'discord_bot_token')
roleid = int(config.get(BOT_SECTION, 'role_id'))
PLEXUSER = config.get(BOT_SECTION, 'plex_user')
PLEXPASS = config.get(BOT_SECTION, 'plex_pass')
PLEX_SERVER_NAME = config.get(BOT_SECTION, 'plex_server_name')
Plex_LIBS = config.get(BOT_SECTION, 'plex_libs')
chan = int(config.get(BOT_SECTION, 'channel_id'))
ownerid = int(config.get(BOT_SECTION, 'owner_id'))
auto_remove_user = config.get(BOT_SECTION, 'auto_remove_user') if config.get(BOT_SECTION, 'auto_remove_user') else False
li = list(Plex_LIBS.split(','))
Plex_LIBS = li
#rolenames = list(roleid.split(','))
if auto_remove_user:
print("auto remove user = True")
import db as db
account = MyPlexAccount(PLEXUSER, PLEXPASS)
plex = account.resource(PLEX_SERVER_NAME).connect() # returns a PlexServer instance
def plexadd(plexname):
try:
plex.myPlexAccount().inviteFriend(user=plexname, server=plex, sections=Plex_LIBS, allowSync=False,
allowCameraUpload=False, allowChannels=False, filterMovies=None,
filterTelevision=None, filterMusic=None)
except Exception as e:
print(e)
return False
else:
print(plexname +' has been added to plex (☞ຈل͜ຈ)☞')
return True
def plexremove(plexname):
try:
plex.myPlexAccount().removeFriend(user=plexname)
except Exception as e:
print(e)
return False
else:
print(plexname +' has been removed from plex (☞ຈل͜ຈ)☞')
return True
class MyClient(discord.Client):
async def on_ready(self):
print('Made by Sleepingpirate https://github.com/Sleepingpirates/')
print('Logged in as')
print(self.user.name)
print(self.user.id)
print('------')
await client.change_presence(activity=discord.Game(name="Do -help in {}".format(client.get_channel(chan))))
async def on_member_update(self, before, after):
role = after.guild.get_role(roleid)
secure = client.get_channel(chan)
if (role in after.roles and role not in before.roles):
await after.send('Welcome To '+ PLEX_SERVER_NAME +'. Just reply with your email so we can add you to Plex!')
await after.send('I will wait 10 minutes for your message, if you do not send it by then I will cancel the command.')
def check(m):
return m.author == after and not m.guild
try:
email = await client.wait_for('message', timeout=600, check=check)
except asyncio.TimeoutError:
await after.send('Timed Out. Message Server Admin So They Can Add You Manually.')
else:
await asyncio.sleep(5)
await after.send('Got it we will be processing your email shortly')
print(email.content) #make it go to a log channel
plexname = str(email.content)
if plexadd(plexname):
if auto_remove_user:
db.save_user(str(after.id), email.content)
await asyncio.sleep(20)
await after.send('You have Been Added To Plex!')
await secure.send(plexname + ' ' + after.mention + ' was added to plex')
else:
await after.send('There was an error adding this email address. Message Server Admin.')
elif(role not in after.roles and role in before.roles):
if auto_remove_user:
try:
user_id = after.id
email = db.get_useremail(user_id)
plexremove(email)
deleted = db.delete_user(user_id)
if deleted:
print("Removed {} from db".format(email))
await secure.send(plexname + ' ' + after.mention + ' was removed from plex')
else:
print("Cannot remove this user from db.")
except:
print("Cannot remove this user from plex.")
async def on_message(self, message):
secure = client.get_channel(chan)
if message.author.id == self.user.id:
return
if message.author.id == ownerid:
if message.content.startswith('-db add'):
mgs = message.content.replace('-db add ','')
try:
mgs = mgs.split(' ')
email = mgs[0]
bad_chars = ['<','>','@','!']
user_id = mgs[1]
for i in bad_chars:
user_id = user_id.replace(i, '')
db.save_user(user_id, email)
await secure.send(email + ' ' + mgs[1] + ' was added to plex')
except:
await message.channel.send('Cannot add this user to db.')
print("Cannot add this user to db.")
await message.delete()
if str(message.channel) == str(secure):
if message.content.startswith('-db ls') or message.content.startswith('-db rm'):
embed = discord.Embed(title='Invitarr Database.')
all = db.read_useremail()
for index, peoples in enumerate(all):
index = index + 1
id = int(peoples[1])
dbuser = client.get_user(id)
dbemail = peoples[2]
embed.add_field(name=f"**{index}. {dbuser.name}**", value=dbemail+'\n', inline=False)
if message.content.startswith('-db ls'):
await secure.send(embed = embed)
else:
try:
position = message.content.replace("-db rm", "")
position = int(position) - 1
id = all[position][1]
email = db.get_useremail(id)
deleted = db.delete_user(id)
if deleted:
print("Removed {} from db".format(email))
await secure.send("Removed {} from db".format(email))
else:
print("Cannot remove this user from db.")
except Exception as e:
print(e)
if message.content.startswith('-help'):
embed = discord.Embed(title='Invitarr Bot Commands', description='Made by [Sleepingpirates](https://github.com/Sleepingpirates/Invitarr), [Join Discord Server](https://discord.gg/vcxCytN)')
embed.add_field(name='-plexadd <email>', value='This command is used to add an email to plex', inline=False)
embed.add_field(name='-plexrm <email>', value='This command is used to remove an email from plex', inline=False)
embed.add_field(name='-db ls', value='This command is used list Invitarrs database', inline=False)
embed.add_field(name='-db add <email> <@user>', value='This command is used add exsisting users email and discord id to the DB.', inline=False)
embed.add_field(name='-db rm <position>', value='This command is used remove a record from the Db. Use -db ls to determine record position. ex: -db rm 1', inline=False)
await secure.send(embed = embed)
async def on_member_remove(self, member):
if auto_remove_user:
try:
user_id = member.id ## not there
email = db.get_useremail(user_id)
plexremove(email)
deleted = db.delete_user(user_id)
if deleted:
print("Removed {} from db".format(email))
secure = client.get_channel(chan)
await secure.send(email + ' ' + member.mention + 'was removed from plex because they left the server')
else:
print("Cannot remove this user from db.")
except:
print("Cannot remove this user from plex.")
client = MyClient()
client.run(Discord_bot_token)

83
app/app/bot/db.py Normal file
View File

@@ -0,0 +1,83 @@
import sqlite3
DB_URL = 'app/config/app.db'
def create_connection(db_file):
""" create a database connection to a SQLite database """
conn = None
try:
conn = sqlite3.connect(db_file)
print("Connected to db")
except Error as e:
print("error in connecting to db")
finally:
if conn:
return conn
def checkTableExists(dbcon, tablename):
dbcur = dbcon.cursor()
dbcur.execute("""SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='{0}';""".format(tablename.replace('\'', '\'\'')))
if dbcur.fetchone()[0] == 1:
dbcur.close()
return True
dbcur.close()
return False
conn = create_connection(DB_URL)
# Checking if table exists
if checkTableExists(conn, 'clients'):
print('Table exists.')
else:
conn.execute('''
CREATE TABLE "clients" (
"id" INTEGER NOT NULL UNIQUE,
"discord_username" TEXT NOT NULL UNIQUE,
"email" TEXT NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT)
);
''')
def save_user(username, email):
if username and email:
conn.execute("INSERT INTO clients (discord_username, email) VALUES ('"+ username +"', '" + email + "')");
conn.commit()
print("User added to db.")
else:
return "Username or email cannot be empty"
def get_useremail(username):
if username:
try:
cursor = conn.execute('SELECT discord_username, email from clients where discord_username="{}";'.format(username))
for row in cursor:
email = row[1]
if email:
return email
else:
return "No users found"
except:
return "error in fetching from db"
else:
return "username cannot be empty"
def delete_user(username):
if username:
try:
conn.execute('DELETE from clients where discord_username="{}";'.format(username))
conn.commit()
return True
except:
return False
else:
return "username cannot be empty"
def read_useremail():
cur = conn.cursor()
cur.execute("SELECT * FROM clients")
rows = cur.fetchall()
all = []
for row in rows:
#print(row[1]+' '+row[2])
all.append(row)
return all

56
app/app/configHandler.py Normal file
View File

@@ -0,0 +1,56 @@
import configparser
config = configparser.ConfigParser()
CONFIG_PATH = 'app/config/config.ini'
BOT_SECTION = 'bot_envs'
CONFIG_KEYS = ['username', 'password', 'discord_bot_token', 'plex_user', 'plex_pass',
'role_id', 'plex_server_name', 'plex_libs', 'owner_id', 'channel_id',
'auto_remove_user']
def get_config():
"""
Function to return current config
"""
try:
config.read(CONFIG_PATH)
return config
except Exception as e:
print(e)
print('error in reading config')
return None
def change_config(key, value):
"""
Function to change the key, value pair in config
"""
try:
config = configparser.ConfigParser()
config.read(CONFIG_PATH)
except Exception as e:
print(e)
print("Cannot Read config.")
try:
config.set(BOT_SECTION, key, str(value))
except Exception as e:
config.add_section(BOT_SECTION)
config.set(BOT_SECTION, key, str(value))
try:
with open(CONFIG_PATH, 'w') as configfile:
config.write(configfile)
except Exception as e:
print(e)
print("Cannot write to config.")
def change_config_form(form_output):
"""
Function to change config on the basis of form_output from web
"""
for key in CONFIG_KEYS:
try:
change_config(key, form_output[key].data)
except:
pass

52
app/app/forms.py Normal file
View File

@@ -0,0 +1,52 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField, IntegerField
from wtforms.validators import DataRequired
class LoginForm(FlaskForm):
username = StringField('Username',
validators=[DataRequired()])
password = PasswordField('Password',
validators=[DataRequired()])
submit = SubmitField('Save Changes')
class GeneralForm(FlaskForm):
username = StringField('Username',
validators=[DataRequired()])
password = PasswordField('Password',
validators=[DataRequired()])
submit = SubmitField('Save Changes')
class BotForm(FlaskForm):
discord_bot_token = StringField('Discord Bot Token',
validators=[DataRequired()])
role_id = IntegerField('Role Id',
validators=[DataRequired()])
channel_id = IntegerField('Channel Id',
validators=[DataRequired()])
owner_id = IntegerField('Owner Id',
validators=[DataRequired()])
# Auto Remove User
auto_remove_user = BooleanField('On')
submit = SubmitField('Save Changes')
class PlexForm(FlaskForm):
plex_user = StringField('Plex User',
validators=[DataRequired()])
plex_pass = StringField('Plex Pass',
validators=[DataRequired()])
plex_server_name = StringField('Plex Server Name',
validators=[DataRequired()])
plex_libs = StringField('Plex Libs',
validators=[DataRequired()])
submit = SubmitField('Save Changes')

15
app/app/models.py Normal file
View File

@@ -0,0 +1,15 @@
from app import db, login_manager
from flask_login import UserMixin
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
password = db.Column(db.String(60), nullable=False)
def __repr__(self):
return f"User('{self.username}')"

120
app/app/routes.py Normal file
View File

@@ -0,0 +1,120 @@
from flask import render_template, flash, request, redirect, url_for
from flask_login import login_user, current_user, logout_user, login_required
import subprocess
from app import app, db, bcrypt
from app.models import User
from app.forms import LoginForm, GeneralForm, BotForm, PlexForm
from app import configHandler
db.create_all()
BOT_SECTION = 'bot_envs'
proc = None
def manage_bot(option):
global proc
if option == 'start':
proc = subprocess.Popen(["python", "app/bot/Invitarr.py"])
elif option == 'kill':
if proc:
proc.terminate()
try:
manage_bot('start')
except:
print("Some error in starting bot. Please check the config vars")
@app.route("/login", methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('home'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and bcrypt.check_password_hash(user.password, form.password.data):
login_user(user, remember=True)
next_page = request.args.get('next')
return redirect(next_page) if next_page else redirect(url_for('home'))
else:
flash('Login Unsuccessful. Please check email and password')
return render_template('login.html', title='Login', form=form)
@app.route('/logout')
@login_required
def logout():
flash('Logged out.')
logout_user()
return redirect(url_for('login'))
@app.route('/', methods=['GET', 'POST'])
@login_required
def home():
form = GeneralForm()
if request.method == 'GET':
user = User.query.all()[0]
form.username.data = user.username
form.password.data = user.password
elif request.method == 'POST':
if form.validate_on_submit():
user = User.query.all()[0]
user.username = form.username.data
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
user.password = hashed_password
db.session.commit()
logout_user()
flash('Details updated.')
return redirect(url_for('login'))
return render_template('index.html', form=form)
@app.route('/bot', methods=['GET', 'POST'])
@login_required
def bot():
form = BotForm()
if request.method == 'GET':
try:
config = configHandler.get_config()
form.discord_bot_token.data = config.get(BOT_SECTION, 'discord_bot_token')
form.role_id.data = config.get(BOT_SECTION, 'role_id')
form.channel_id.data = config.get(BOT_SECTION, 'channel_id')
form.owner_id.data = config.get(BOT_SECTION, 'owner_id')
form.auto_remove_user.data = config.get(BOT_SECTION, 'auto_remove_user')
except:
pass
elif request.method == 'POST':
if form.validate_on_submit():
try:
configHandler.change_config_form(form)
flash('Settings updated.')
manage_bot('kill')
manage_bot('start')
except:
flash('Some error in updating settings')
return render_template('bot.html', form = form)
@app.route('/plex', methods=['GET', 'POST'])
@login_required
def plex():
form = PlexForm()
if request.method == 'GET':
try:
config = configHandler.get_config()
form.plex_user.data = config.get(BOT_SECTION, 'plex_user')
form.plex_pass.data = config.get(BOT_SECTION, 'plex_pass')
form.plex_server_name.data = config.get(BOT_SECTION, 'plex_server_name')
form.plex_libs.data = config.get(BOT_SECTION, 'plex_libs')
except:
pass
elif request.method == 'POST':
if form.validate_on_submit():
try:
configHandler.change_config_form(form)
flash('Settings updated.')
except:
flash('Some error in updating settings')
try:
manage_bot('kill')
manage_bot('start')
except Exception as e:
raise Exception(e)
return render_template('plex.html', form = form)

BIN
app/app/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FF4A4F;}
.st1{fill:#E94A4F;}
.st2{fill:#FFFFFF;}
.st3{fill:#2A2A2C;}
.st4{fill:#171719;}
.st5{fill:#57555C;}
.st6{fill:#3C3B41;}
.st7{fill:#C8C6CC;}
</style>
<circle class="st0" cx="256" cy="256" r="256"/>
<path class="st1" d="M512,256c0-13.4-1-26.6-3-39.4L397.9,105.5H108.4H82.9v241.4v97.6l0,0C128.5,486.4,189.2,512,256,512
C397.4,512,512,397.4,512,256z"/>
<path class="st2" d="M82.9,105.5v339.1c8,7.4,16.6,14.3,25.5,20.6V105.5L82.9,105.5L82.9,105.5z"/>
<rect x="334.6" y="152.9" class="st3" width="105.8" height="241.5"/>
<polygon class="st4" points="397.9,105.5 334.6,152.9 334.6,394.4 397.9,346.9 "/>
<rect x="82.9" y="105.5" class="st5" width="315" height="241.5"/>
<rect x="240.4" y="105.5" class="st6" width="157.5" height="241.5"/>
<path class="st2" d="M187.4,158.4l14,14c-6,5.2-11,11.6-14.6,18.8l-16.1-16.1c-6,5.8-15.4,5.7-21.3-0.2c-5.9-5.9-5.9-15.6,0-21.5
c3.3-3.3,7.7-4.7,11.9-4.4c-0.3-4.3,1.1-8.7,4.4-11.9c5.9-5.9,15.6-5.9,21.5,0C193.1,143,193.2,152.5,187.4,158.4z"/>
<path class="st7" d="M331.4,302.7c-3.3,3.3-7.7,4.7-11.9,4.4c0.3,4.3-1.1,8.7-4.4,11.9c-5.9,5.9-15.6,5.9-21.5,0
c-5.9-5.9-5.9-15.4-0.2-21.3L272,276.3v-5.2c1.6,0.3,3.2,0.5,4.8,0.5c6.6,0,12.5-2.7,16.8-7L310,281c5.9-5.8,15.4-5.7,21.3,0.2
C337.3,287.1,337.3,296.8,331.4,302.7z"/>
<path class="st2" d="M204,271.6c1.6,0,3.3-0.2,4.8-0.5v5.2l-21.4,21.4c5.8,5.9,5.7,15.4-0.2,21.3c-5.9,5.9-15.6,5.9-21.5,0
c-3.3-3.3-4.7-7.7-4.4-11.9c-4.3,0.3-8.7-1.1-11.9-4.4c-5.9-5.9-5.9-15.6,0-21.5c5.9-5.9,15.4-5.9,21.3-0.2l16.5-16.5
C191.5,268.9,197.4,271.6,204,271.6z"/>
<path class="st7" d="M331.4,174.9c-5.9,5.9-15.4,5.9-21.3,0.2l-16.1,16.1c-3.6-7.2-8.6-13.6-14.6-18.8l14-14
c-5.8-6-5.7-15.4,0.2-21.3c5.9-5.9,15.6-5.9,21.5,0c3.3,3.3,4.7,7.7,4.4,11.9c4.3-0.3,8.7,1.1,11.9,4.4
C337.3,159.4,337.3,169,331.4,174.9z"/>
<path class="st2" d="M286.7,237.7c2.6-6.1,4.1-12.9,4.1-20c0-27.9-22.6-50.5-50.5-50.5s-50.5,22.6-50.5,50.5c0,7.1,1.5,13.8,4.1,20
c-2.7,2.6-4.4,6.3-4.4,10.4c0,7.9,6.4,14.4,14.4,14.4c3.1,0,6-1,8.4-2.7c1.8,1.2,3.7,2.3,5.7,3.3v15.8c0,3.8,3.1,6.8,6.8,6.8h0.8
v-18.3h6.6v18.3h4.8v-18.3h6.6v18.3h4.8v-18.3h6.6v18.3h0.8c3.8,0,6.8-3.1,6.8-6.8v-15.8c2-1,3.9-2.1,5.7-3.3
c2.4,1.7,5.3,2.7,8.4,2.7c7.9,0,14.4-6.4,14.4-14.4C291.2,243.9,289.5,240.3,286.7,237.7z M222.7,226.2h-5c-5.3,0-9.6-4.3-9.6-9.6
v-5c0-5.3,4.3-9.6,9.6-9.6h5c5.3,0,9.6,4.3,9.6,9.6v5C232.4,221.8,228,226.2,222.7,226.2z M246.5,246.1h-6.1h-6.1
c-2.9,0-4.7-3.1-3.3-5.6l3-5.3l3-5.3c1.4-2.5,5.1-2.5,6.5,0l3,5.3l3,5.3C251.2,243,249.4,246.1,246.5,246.1z M272.7,216.5
c0,5.3-4.3,9.6-9.6,9.6h-5c-5.3,0-9.6-4.3-9.6-9.6v-5c0-5.3,4.3-9.6,9.6-9.6h5c5.3,0,9.6,4.3,9.6,9.6V216.5z"/>
<path class="st7" d="M286.7,237.7c2.6-6.1,4.1-12.9,4.1-20c0-27.9-22.6-50.5-50.5-50.5v60.8c1.3,0,2.5,0.6,3.3,1.9l3,5.3l3,5.3
c1.4,2.5-0.4,5.6-3.3,5.6h-6.1v21.2h3.3v18.3h4.8v-18.3h6.6v18.3h0.8c3.8,0,6.8-3.1,6.8-6.8v-15.9c2-1,3.9-2.1,5.7-3.3
c2.4,1.7,5.3,2.7,8.4,2.7c7.9,0,14.4-6.4,14.4-14.4C291.2,243.9,289.5,240.3,286.7,237.7z M272.7,216.5c0,5.3-4.3,9.6-9.6,9.6h-5
c-5.3,0-9.6-4.3-9.6-9.6v-5c0-5.3,4.3-9.6,9.6-9.6h5c5.3,0,9.6,4.3,9.6,9.6V216.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

86
app/app/static/main.css Normal file
View File

@@ -0,0 +1,86 @@
/* Universal */
:root {
--main-bg-color: #000000;
--content-background: #17151C;
--secondary-content-background: #2A2731;
--primary-text-color: #FFFFFF;
}
body{
background-color: var(--main-bg-color);
color: var(--primary-text-color);
}
/* Header */
h3{
font-weight: bold;
display: inline-block;
margin: 0.5em 0 1.5em 0.25em;
padding-top: 0.5em;
}
.header img{
vertical-align: middle;
}
a.nav-link{
color: var(--primary-text-color);
}
.header button{
max-height: 50px;
/* margin: 1em 0.5em; */
}
/* Form */
.form-container{
padding: 1.5em;
padding-top: 2em;
border-radius: 0 0 10px 10px;
background-color: var(--content-background);
}
.form-stringfield{
background-color: var(--secondary-content-background);
color: var(--primary-text-color);
}
.form-stringfield:focus{
background-color: var(--secondary-content-background);
color: var(--primary-text-color);
}
.custom-checkbox{
padding-left: 3em;
}
/* Modal */
.modal-content{
background-color: var(--secondary-content-background);
padding: 1em !important;
}
.modal-body{
text-align: center;
}
.modal-header{
padding: 1em;
border-bottom: none;
}
.modal-header h4{
color: var(--primary-text-color)
}
/* Login Page */
.login{
margin: auto;
margin-top: 5em;
text-align: center;
}
.login h2{
margin-top: 1em;
font-weight: bold;
}

194
app/app/templates/bot.html Normal file
View File

@@ -0,0 +1,194 @@
{%extends 'layout.html'%}
{% block content %}
<div class="header mt-5 d-flex">
<img src="{{ url_for('static', filename='img/logo.svg') }}" class="img align-self-center" height="45">
<h3 class="align-self-center">Invitarr Configuration</h3>
<!-- <button class="ml-auto btn btn-danger align-self-center">Restart bot</button> -->
</div>
<nav>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('home') }}">General</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="#">Bot</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('plex') }}">Plex</a>
</li>
<li class="nav-item ml-auto">
<a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
</li>
</ul>
</nav>
<div class="form-container">
<!-- Form Section -->
<form method="POST" action="">
{{ form.hidden_tag() }}
<div class="form-group mb-3">
<div class="row">
<div class="col-md-3">
{{ form.discord_bot_token.label(class="form-control-label")}}
</div>
<div class="col-md-1"></div>
<div class="col-md-6">
{% if form.discord_bot_token.errors %}
{{ form.discord_bot_token(class="form-control form-stringfield")}}
<small id="emailHelp" class="form-text text-muted">Some text describing this field</small>
<div class="invalid-feedback">
{% for error in form.discord_bot_token.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.discord_bot_token(class="form-control form-stringfield")}}
{% endif %}
</div>
</div>
</div>
<div class="form-group my-3">
<div class="row">
<div class="col-md-3">
{{ form.role_id.label(class="form-control-label")}}
</div>
<div class="col-md-1"></div>
<div class="col-md-6">
{% if form.role_id.errors %}
{{ form.role_id(class="form-control form-stringfield")}}
<div class="invalid-feedback">
{% for error in form.role_id.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.role_id(class="form-control form-stringfield")}}
{% endif %}
</div>
</div>
</div>
<div class="form-group my-3">
<div class="row">
<div class="col-md-3">
{{ form.channel_id.label(class="form-control-label")}}
</div>
<div class="col-md-1"></div>
<div class="col-md-6">
{% if form.channel_id.errors %}
{{ form.channel_id(class="form-control form-stringfield")}}
<div class="invalid-feedback">
{% for error in form.channel_id.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.channel_id(class="form-control form-stringfield")}}
{% endif %}
</div>
</div>
</div>
<div class="form-group my-3">
<div class="row">
<div class="col-md-3">
{{ form.owner_id.label(class="form-control-label")}}
</div>
<div class="col-md-1"></div>
<div class="col-md-6">
{% if form.owner_id.errors %}
{{ form.owner_id(class="form-control form-stringfield")}}
<div class="invalid-feedback">
{% for error in form.owner_id.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.owner_id(class="form-control form-stringfield")}}
{% endif %}
</div>
</div>
</div>
<div class="my-3">
<div class="row">
<div class="col-md-3">
<p>Auto Remove User</p>
</div>
<div class="col-md-1"></div>
<div class="col-md-1 custom-control custom-control-inline custom-switch custom-checkbox">
{% if form.auto_remove_user.errors %}
{{ form.auto_remove_user(class="custom-control-input form-stringfield", id="removeSwitch")}}
{{ form.auto_remove_user.label(class="custom-control-label", for="removeSwitch")}}
<div class="invalid-feedback">
{% for error in form.auto_remove_user.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.auto_remove_user(class="custom-control-input form-stringfield", id="removeSwitch")}}
{{ form.auto_remove_user.label(class="custom-control-label", for="removeSwitch")}}
{% endif %}
</div>
</div>
</div>
<div class="form-group my-5 submit-btn">
{{ form.submit(class="btn btn-primary submit-btn") }}
</div>
</form>
</div>
<div id="myModal" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
{% with messages = get_flashed_messages() %}
{% if messages %}
<h4>
{% for message in messages %}
{{ message }}
{% endfor %}
</h4>
{% endif %}
{% endwith %}
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block script %}
<script>
$(document).ready(()=> {
// Trigger flash modal
let messages = "{{ get_flashed_messages() }}";
if (typeof messages != 'undefined' && messages != '[]') {
$("#myModal").modal();
};
// Adding Validation
number_validaitons = ['role_id', 'channel_id', 'owner_id']
number_validaitons.forEach(element => {
$('#' + element).keyup(function () {
this.value = this.value.replace(/[^0-9\.]/g,'');
});
});
let checkbox_state = $('input[name=auto_remove_user]').is(':checked');
if(checkbox_state == false){
$('.custom-control-label').text('Off');
}
$('#removeSwitch').on('change', (e)=> {
checkbox_state = $('input[name=auto_remove_user]').is(':checked');
if(checkbox_state == false){
$('.custom-control-label').text('Off');
} else {
$('.custom-control-label').text('On');
}
});
});
</script>
{% endblock script %}

View File

@@ -0,0 +1,131 @@
{%extends 'layout.html'%}
{% block content %}
<div class="header mt-5">
<img src="{{ url_for('static', filename='img/logo.svg') }}" class="img" height="45">
<h3>Invitarr Configuration</h3>
</div>
<nav>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="#">General</a>
</li>
<li class="nav-item">
<a class="nav-link " href="{{ url_for('bot') }}">Bot</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('plex') }}">Plex</a>
</li>
<li class="nav-item ml-auto">
<a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
</li>
</ul>
</nav>
<div class="form-container">
<!-- Form Section -->
<form method="POST" action="">
{{ form.hidden_tag() }}
<div class="form-group mb-3">
<div class="row">
<div class="col-md-3">
{{ form.username.label(class="form-control-label")}}
</div>
<div class="col-md-1"></div>
<div class="col-md-6">
{% if form.username.errors %}
{{ form.username(class="form-control form-stringfield")}}
<small id="emailHelp" class="form-text text-muted">Some text describing this field</small>
<div class="invalid-feedback">
{% for error in form.username.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.username(class="form-control form-stringfield")}}
{% endif %}
</div>
</div>
</div>
<div class="form-group my-3">
<div class="row">
<div class="col-md-3">
{{ form.password.label(class="form-control-label")}}
</div>
<div class="col-md-1"></div>
<div class="col-md-6">
{% if form.password.errors %}
{{ form.password(class="form-control form-stringfield")}}
<div class="invalid-feedback">
{% for error in form.password.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.password(class="form-control form-stringfield")}}
{% endif %}
</div>
</div>
</div>
<div class="form-group my-5 submit-btn">
{{ form.submit(class="btn btn-primary submit-btn") }}
</div>
</form>
</div>
<div id="myModal" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
{% with messages = get_flashed_messages() %}
{% if messages %}
<h4>
{% for message in messages %}
{{ message }}
{% endfor %}
</h4>
{% endif %}
{% endwith %}
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block script %}
<script>
$(document).ready(()=> {
// Trigger flash modal
let messages = "{{ get_flashed_messages() }}";
if (typeof messages != 'undefined' && messages != '[]') {
$("#myModal").modal();
};
// Adding Validation
number_validaitons = ['role_id', 'channel_id', 'owner_id']
number_validaitons.forEach(element => {
$('#' + element).keyup(function () {
this.value = this.value.replace(/[^0-9\.]/g,'');
});
});
let checkbox_state = $('input[name=auto_remove_user]').is(':checked');
if(checkbox_state == false){
$('.custom-control-label').text('Off');
}
$('#removeSwitch').on('change', (e)=> {
checkbox_state = $('input[name=auto_remove_user]').is(':checked');
if(checkbox_state == false){
$('.custom-control-label').text('Off');
} else {
$('.custom-control-label').text('On');
}
});
});
</script>
{% endblock script %}

View File

@@ -0,0 +1,42 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/css/select2.min.css" rel="stylesheet" />
{% if title %}
<title>{{ title }} - Invitarr</title>
{% else %}
<title>Invitarr</title>
{% endif %}
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/js/select2.min.js"></script>
{% block script %}{% endblock %}
<script>
$( document ).ready(function() {
$(".topbar-quicklinks").hide();
$(".topbar-toggle").click(() => {
$(".topbar-quicklinks").toggle();
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,105 @@
{%extends 'layout.html'%}
{% block content %}
<div class="login">
<img src="{{url_for('static', filename='img/logo.svg')}}" class="img" height="120">
<h2 class="mb-5">Invitarr</h2>
<form method="POST" action="">
{{ form.hidden_tag() }}
<div class="form-group mb-3">
<div class="row">
<div class="col-md-4 offset-4">
{% if form.username.errors %}
{{ form.username(class="form-control form-stringfield", placeholder="Username*")}}
<small id="emailHelp" class="form-text text-muted">Some text describing this field</small>
<div class="invalid-feedback">
{% for error in form.username.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.username(class="form-control form-stringfield", placeholder="Username*")}}
{% endif %}
</div>
</div>
</div>
<div class="form-group my-3">
<div class="row">
<div class="col-md-4 offset-4">
{% if form.password.errors %}
{{ form.password(class="form-control form-stringfield", placeholder="Password*")}}
<div class="invalid-feedback">
{% for error in form.password.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.password(class="form-control form-stringfield", placeholder="Password*")}}
{% endif %}
</div>
</div>
</div>
<div class="form-group my-5 submit-btn">
{{ form.submit(class="btn btn-primary submit-btn") }}
</div>
</form>
</div>
<div id="myModal" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
{% with messages = get_flashed_messages() %}
{% if messages %}
<h4>
{% for message in messages %}
{{ message }}
{% endfor %}
</h4>
{% endif %}
{% endwith %}
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block script %}
<script>
$(document).ready(()=> {
// Trigger flash modal
let messages = "{{ get_flashed_messages() }}";
if (typeof messages != 'undefined' && messages != '[]') {
$("#myModal").modal();
};
// Adding Validation
number_validaitons = ['role_id', 'channel_id', 'owner_id']
number_validaitons.forEach(element => {
$('#' + element).keyup(function () {
this.value = this.value.replace(/[^0-9\.]/g,'');
});
});
let checkbox_state = $('input[name=auto_remove_user]').is(':checked');
if(checkbox_state == false){
$('.custom-control-label').text('Off');
}
$('#removeSwitch').on('change', (e)=> {
checkbox_state = $('input[name=auto_remove_user]').is(':checked');
if(checkbox_state == false){
$('.custom-control-label').text('Off');
} else {
$('.custom-control-label').text('On');
}
});
});
</script>
{% endblock script %}

155
app/app/templates/plex.html Normal file
View File

@@ -0,0 +1,155 @@
{%extends 'layout.html'%}
{% block content %}
<div class="header mt-5">
<img src="{{ url_for('static', filename='img/logo.svg') }}" class="img" height="45">
<h3>Invitarr Configuration</h3>
</div>
<nav>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('home') }}">General</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('bot') }}">Bot</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="#">Plex</a>
</li>
<li class="nav-item ml-auto">
<a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
</li>
</ul>
</nav>
<div class="form-container">
<!-- Form Section -->
<form method="POST" action="">
{{ form.hidden_tag() }}
<div class="form-group mb-3">
<div class="row">
<div class="col-md-3">
{{ form.plex_user.label(class="form-control-label")}}
</div>
<div class="col-md-1"></div>
<div class="col-md-6">
{% if form.plex_user.errors %}
{{ form.plex_user(class="form-control form-stringfield")}}
<small id="emailHelp" class="form-text text-muted">Some text describing this field</small>
<div class="invalid-feedback">
{% for error in form.plex_user.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.plex_user(class="form-control form-stringfield")}}
{% endif %}
</div>
</div>
</div>
<div class="form-group my-3">
<div class="row">
<div class="col-md-3">
{{ form.plex_pass.label(class="form-control-label")}}
</div>
<div class="col-md-1"></div>
<div class="col-md-6">
{% if form.plex_pass.errors %}
{{ form.plex_pass(class="form-control form-stringfield")}}
<div class="invalid-feedback">
{% for error in form.plex_pass.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.plex_pass(class="form-control form-stringfield")}}
{% endif %}
</div>
</div>
</div>
<div class="form-group my-3">
<div class="row">
<div class="col-md-3">
{{ form.plex_server_name.label(class="form-control-label")}}
</div>
<div class="col-md-1"></div>
<div class="col-md-6">
{% if form.plex_server_name.errors %}
{{ form.plex_server_name(class="form-control form-stringfield")}}
<div class="invalid-feedback">
{% for error in form.plex_server_name.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.plex_server_name(class="form-control form-stringfield")}}
{% endif %}
</div>
</div>
</div>
<div class="form-group my-3">
<div class="row">
<div class="col-md-3">
{{ form.plex_libs.label(class="form-control-label")}}
</div>
<div class="col-md-1"></div>
<div class="col-md-6">
{% if form.plex_libs.errors %}
{{ form.plex_libs(class="form-control form-stringfield")}}
<div class="invalid-feedback">
{% for error in form.plex_libs.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.plex_libs(class="form-control form-stringfield")}}
{% endif %}
<small id="emailHelp" class="form-text text-muted">Enter comma seperated values</small>
</div>
</div>
</div>
<div class="form-group my-5 submit-btn">
{{ form.submit(class="btn btn-primary submit-btn") }}
</div>
</form>
</div>
<div id="myModal" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
{% with messages = get_flashed_messages() %}
{% if messages %}
<h4>
{% for message in messages %}
{{ message }}
{% endfor %}
</h4>
{% endif %}
{% endwith %}
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block script %}
<script>
$(document).ready(()=> {
// Trigger flash modal
let messages = "{{ get_flashed_messages() }}";
if (typeof messages != 'undefined' && messages != '[]') {
$("#myModal").modal();
};
$(".plex-libs").select2({
theme: "classic",
tags: true,
tokenSeparators: [',', ' ']
});
});
</script>
{% endblock script %}

33
app/requirements.txt Normal file
View File

@@ -0,0 +1,33 @@
aiohttp==3.6.2
async-timeout==3.0.1
attrs==19.3.0
bcrypt==3.1.7
certifi==2020.4.5.2
cffi==1.14.0
chardet==3.0.4
click==7.1.2
discord.py==1.3.3
Flask==1.1.2
Flask-Bcrypt==0.7.1
Flask-Login==0.5.0
Flask-SQLAlchemy==2.4.3
Flask-WTF==0.14.3
idna==2.9
idna-ssl==1.1.0
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
multidict==4.7.6
plex.py==0.9.0
PlexAPI==4.0.0
pycparser==2.20
python-dotenv==0.13.0
requests==2.23.0
six==1.15.0
SQLAlchemy==1.3.17
typing-extensions==3.7.4.2
urllib3==1.25.9
websockets==8.1
Werkzeug==1.0.1
WTForms==2.3.1
yarl==1.4.2

17
app/run.py Normal file
View File

@@ -0,0 +1,17 @@
from app import app
from app import db, bcrypt
from app.models import User
from os import path
'''
if path.exists("app.config.app.db") is False:
db.create_all()
try:
hashed_password = bcrypt.generate_password_hash(DEFAULT_PASS).decode('utf-8')
user = User(username=DEFAULT_USER, password=hashed_password)
db.session.add(user)
db.session.commit()
except:
print("Some error in setting up.")
'''
if __name__ == "__main__":
app.run(debug=True)

14
app/setup.py Normal file
View File

@@ -0,0 +1,14 @@
from app import db, bcrypt
from app.models import User
DEFAULT_USER = "admin"
DEFAULT_PASS = "admin"
db.create_all()
try:
hashed_password = bcrypt.generate_password_hash(DEFAULT_PASS).decode('utf-8')
user = User(username=DEFAULT_USER, password=hashed_password)
db.session.add(user)
db.session.commit()
except:
print("Some error in setting up.")

4
app/uwsgi.ini Normal file
View File

@@ -0,0 +1,4 @@
[uwsgi]
module = run
callable = app
enable-threads = true