Initial structure for discordbot addition

This commit is contained in:
Jeremy Zhang
2017-05-02 18:37:24 +00:00
parent 2623724b0b
commit a6766b2008
43 changed files with 17 additions and 8 deletions

View File

36
webapp/titanembeds/app.py Normal file
View File

@ -0,0 +1,36 @@
from config import config
from database import db
from flask import Flask, render_template, request, session, url_for, redirect, jsonify
from flask_sslify import SSLify
from titanembeds.utils import rate_limiter, cache, discord_api
import blueprints.api
import blueprints.user
import blueprints.embed
import os
os.chdir(config['app-location'])
app = Flask(__name__, static_folder="static")
app.config['SQLALCHEMY_DATABASE_URI'] = config['database-uri']
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Suppress the warning/no need this on for now.
app.config['RATELIMIT_HEADERS_ENABLED'] = True
app.config['SQLALCHEMY_POOL_RECYCLE'] = 250
app.config['RATELIMIT_STORAGE_URL'] = 'keyvalprops://'
app.secret_key = config['app-secret']
db.init_app(app)
rate_limiter.init_app(app)
sslify = SSLify(app, permanent=True)
app.register_blueprint(blueprints.api.api, url_prefix="/api", template_folder="/templates")
app.register_blueprint(blueprints.user.user, url_prefix="/user", template_folder="/templates")
app.register_blueprint(blueprints.embed.embed, url_prefix="/embed", template_folder="/templates")
@app.route("/")
def index():
return render_template("index.html.j2")
@app.before_request
def before_request():
db.create_all()
discord_api.init_discordrest()

View File

@ -0,0 +1 @@
from api import api

View File

@ -0,0 +1,385 @@
from titanembeds.database import db, Guilds, UnauthenticatedUsers, UnauthenticatedBans, AuthenticatedUsers, KeyValueProperties
from titanembeds.decorators import valid_session_required, discord_users_only
from titanembeds.utils import check_guild_existance, guild_query_unauth_users_bool, get_client_ipaddr, discord_api, rate_limiter, channel_ratelimit_key, guild_ratelimit_key
from titanembeds.oauth import user_has_permission, generate_avatar_url, check_user_can_administrate_guild
from flask import Blueprint, abort, jsonify, session, request
from sqlalchemy import and_
import random
import requests
import json
import datetime
import re
from config import config
api = Blueprint("api", __name__)
def user_unauthenticated():
if 'unauthenticated' in session:
return session['unauthenticated']
return True
def checkUserRevoke(guild_id, user_key=None):
revoked = True #guilty until proven not revoked
if user_unauthenticated():
dbUser = UnauthenticatedUsers.query.filter(and_(UnauthenticatedUsers.guild_id == guild_id, UnauthenticatedUsers.user_key == user_key)).first()
revoked = dbUser.isRevoked()
else:
banned = checkUserBanned(guild_id)
if banned:
return revoked
member = discord_api.get_guild_member_nocache(guild_id, session['user_id'])
if member['code'] == 200:
revoked = False
return revoked
def checkUserBanned(guild_id, ip_address=None):
banned = True
if user_unauthenticated():
dbUser = UnauthenticatedBans.query.filter(and_(UnauthenticatedBans.guild_id == guild_id, UnauthenticatedBans.ip_address == ip_address)).all()
if not dbUser:
banned = False
else:
for usr in dbUser:
if usr.lifter_id is not None:
banned = False
else:
banned = False
bans = discord_api.get_guild_bans(guild_id)['content']
for user in bans:
if session['user_id'] == user['user']['id']:
return True
return banned
def update_user_status(guild_id, username, user_key=None):
if user_unauthenticated():
ip_address = get_client_ipaddr()
status = {
'authenticated': False,
'avatar': None,
'manage_embed': False,
'ip_address': ip_address,
'username': username,
'user_key': user_key,
'guild_id': guild_id,
'user_id': session['user_id'],
'banned': checkUserBanned(guild_id, ip_address),
'revoked': checkUserRevoke(guild_id, user_key),
}
if status['banned'] or status['revoked']:
session['user_keys'].pop(guild_id, None)
return status
dbUser = UnauthenticatedUsers.query.filter(and_(UnauthenticatedUsers.guild_id == guild_id, UnauthenticatedUsers.user_key == user_key)).first()
dbUser.bumpTimestamp()
if dbUser.username != username or dbUser.ip_address != ip_address:
dbUser.username = username
dbUser.ip_address = ip_address
db.session.commit()
else:
status = {
'authenticated': True,
'avatar': session["avatar"],
'manage_embed': check_user_can_administrate_guild(guild_id),
'username': username,
'discriminator': session['discriminator'],
'guild_id': guild_id,
'user_id': session['user_id'],
'banned': checkUserBanned(guild_id),
'revoked': checkUserRevoke(guild_id)
}
if status['banned'] or status['revoked']:
return status
dbUser = db.session.query(AuthenticatedUsers).filter(and_(AuthenticatedUsers.guild_id == guild_id, AuthenticatedUsers.client_id == status['user_id'])).first()
dbUser.bumpTimestamp()
return status
def check_user_in_guild(guild_id):
if user_unauthenticated():
return guild_id in session['user_keys']
else:
dbUser = db.session.query(AuthenticatedUsers).filter(and_(AuthenticatedUsers.guild_id == guild_id, AuthenticatedUsers.client_id == session['user_id'])).first()
return 200 == discord_api.get_guild_member_nocache(guild_id, session['user_id'])['code'] and dbUser is not None
def format_post_content(message):
message = message.replace("<", "\<")
message = message.replace(">", "\>")
pattern = re.compile(r'\[@[0-9]+\]')
for match in re.findall(pattern, message):
mention = "<@" + match[2: len(match) - 1] + ">"
message = message.replace(match, mention, 1)
if (session['unauthenticated']):
message = "**[{}#{}]** {}".format(session['username'], session['user_id'], message)
else:
message = "**<{}#{}>** {}".format(session['username'], session['discriminator'], message) # I would like to do a @ mention, but i am worried about notif spam
return message
def get_guild_channels(guild_id):
if user_unauthenticated():
member_roles = [guild_id] #equivilant to @everyone role
else:
member = discord_api.get_guild_member(guild_id, session['user_id'])['content']
member_roles = member['roles']
if guild_id not in member_roles:
member_roles.append(guild_id)
guild_channels = discord_api.get_guild_channels(guild_id)['content']
guild_roles = discord_api.get_guild_roles(guild_id)["content"]
guild_owner = discord_api.get_guild(guild_id)['content']['owner_id']
result_channels = []
for channel in guild_channels:
if channel['type'] == 0:
result = {"channel": channel, "read": False, "write": False}
if guild_owner == session['user_id']:
result["read"] = True
result["write"] = True
result_channels.append(result)
continue
channel_perm = 0
# @everyone
for role in guild_roles:
if role["id"] == guild_id:
channel_perm |= role["permissions"]
continue
# User Guild Roles
for m_role in member_roles:
for g_role in guild_roles:
if g_role["id"] == m_role:
channel_perm |= g_role["permissions"]
continue
# If has server administrator permission
if user_has_permission(channel_perm, 3):
result["read"] = True
result["write"] = True
result_channels.append(result)
continue
denies = 0
allows = 0
# channel specific
for overwrite in channel["permission_overwrites"]:
if overwrite["type"] == "role" and overwrite["id"] in member_roles:
denies |= overwrite["deny"]
allows |= overwrite["allow"]
channel_perm = (channel_perm & ~denies) | allows
# member specific
for overwrite in channel["permission_overwrites"]:
if overwrite["type"] == "member" and overwrite["id"] == session["user_id"]:
channel_perm = (channel_perm & ~overwrite['deny']) | overwrite['allow']
break
result["read"] = user_has_permission(channel_perm, 10)
result["write"] = user_has_permission(channel_perm, 11)
# If default channel, you can read
if channel["id"] == guild_id:
result["read"] = True
# If you cant read channel, you cant write in it
if not user_has_permission(channel_perm, 10):
result["read"] = False
result["write"] = False
#if result["read"]:
result_channels.append(result)
return sorted(result_channels, key=lambda k: k['channel']['position'])
def filter_guild_channel(guild_id, channel_id):
channels = get_guild_channels(guild_id)
for chan in channels:
if chan["channel"]["id"] == guild_id:
return chan
return None
def get_online_discord_users(guild_id):
embed = discord_api.get_widget(guild_id)
apimembers = discord_api.list_all_guild_members(guild_id)
apimembers_filtered = {}
for member in apimembers:
apimembers_filtered[member["user"]["id"]] = member
guild_roles = discord_api.get_guild_roles(guild_id)["content"]
guildroles_filtered = {}
for role in guild_roles:
guildroles_filtered[role["id"]] = role
for member in embed['members']:
apimem = apimembers_filtered.get(member["id"])
member["hoist-role"] = None
member["color"] = None
if apimem:
for roleid in reversed(apimem["roles"]):
role = guildroles_filtered[roleid]
if role["color"] != 0:
member["color"] = '{0:02x}'.format(role["color"]) #int to hex
if role["hoist"]:
member["hoist-role"] = {}
member["hoist-role"]["name"] = role["name"]
member["hoist-role"]["id"] = role["id"]
member["hoist-role"]["position"] = role["position"]
return embed['members']
def get_online_embed_users(guild_id):
time_past = (datetime.datetime.now() - datetime.timedelta(seconds = 60)).strftime('%Y-%m-%d %H:%M:%S')
unauths = db.session.query(UnauthenticatedUsers).filter(UnauthenticatedUsers.last_timestamp > time_past, UnauthenticatedUsers.revoked == False, UnauthenticatedUsers.guild_id == guild_id).all()
auths = db.session.query(AuthenticatedUsers).filter(AuthenticatedUsers.last_timestamp > time_past, AuthenticatedUsers.guild_id == guild_id).all()
users = {'unauthenticated':[], 'authenticated':[]}
for user in unauths:
meta = {
'username': user.username,
'discriminator': user.discriminator,
}
users['unauthenticated'].append(meta)
for user in auths:
client_id = user.client_id
u = discord_api.get_guild_member(guild_id, client_id)['content']['user']
meta = {
'id': u['id'],
'username': u['username'],
'discriminator': u['discriminator'],
'avatar_url': generate_avatar_url(u['id'], u['avatar']),
}
users['authenticated'].append(meta)
return users
@api.route("/fetch", methods=["GET"])
@valid_session_required(api=True)
@rate_limiter.limit("2 per 2 second", key_func = channel_ratelimit_key)
def fetch():
guild_id = request.args.get("guild_id")
channel_id = request.args.get('channel_id')
after_snowflake = request.args.get('after', None, type=int)
if user_unauthenticated():
key = session['user_keys'][guild_id]
else:
key = None
status = update_user_status(guild_id, session['username'], key)
messages = {}
if status['banned'] or status['revoked']:
status_code = 403
else:
chan = filter_guild_channel(guild_id, channel_id)
if not chan.get("read"):
status_code = 401
else:
messages = discord_api.get_channel_messages(channel_id, after_snowflake)
status_code = messages['code']
response = jsonify(messages=messages.get('content', messages), status=status)
response.status_code = status_code
return response
@api.route("/post", methods=["POST"])
@valid_session_required(api=True)
@rate_limiter.limit("1 per 10 second", key_func = channel_ratelimit_key)
def post():
guild_id = request.form.get("guild_id")
channel_id = request.form.get('channel_id')
content = request.form.get('content')
content = format_post_content(content)
if user_unauthenticated():
key = session['user_keys'][guild_id]
else:
key = None
status = update_user_status(guild_id, session['username'], key)
message = {}
if status['banned'] or status['revoked']:
status_code = 401
else:
chan = filter_guild_channel(guild_id, channel_id)
if not chan.get("write"):
status_code = 401
else:
message = discord_api.create_message(channel_id, content)
status_code = message['code']
response = jsonify(message=message.get('content', message), status=status)
response.status_code = status_code
return response
@api.route("/create_unauthenticated_user", methods=["POST"])
@rate_limiter.limit("1 per 15 minute", key_func=guild_ratelimit_key)
def create_unauthenticated_user():
session['unauthenticated'] = True
username = request.form['username']
guild_id = request.form['guild_id']
ip_address = get_client_ipaddr()
username = username.strip()
if len(username) < 2 or len(username) > 32:
abort(406)
if not all(x.isalnum() or x.isspace() or "-" == x or "_" == x for x in username):
abort(406)
if not check_guild_existance(guild_id):
abort(404)
if not guild_query_unauth_users_bool(guild_id):
abort(401)
if not checkUserBanned(guild_id, ip_address):
session['username'] = username
if 'user_id' not in session or len(str(session["user_id"])) > 4:
session['user_id'] = random.randint(0,9999)
user = UnauthenticatedUsers(guild_id, username, session['user_id'], ip_address)
db.session.add(user)
db.session.commit()
key = user.user_key
if 'user_keys' not in session:
session['user_keys'] = {guild_id: key}
else:
session['user_keys'][guild_id] = key
status = update_user_status(guild_id, username, key)
return jsonify(status=status)
else:
status = {'banned': True}
response = jsonify(status=status)
response.status_code = 403
return response
@api.route("/query_guild", methods=["GET"])
@valid_session_required(api=True)
def query_guild():
guild_id = request.args.get('guild_id')
if check_guild_existance(guild_id):
if check_user_in_guild(guild_id):
channels = get_guild_channels(guild_id)
discordmembers = get_online_discord_users(guild_id)
embedmembers = get_online_embed_users(guild_id)
return jsonify(channels=channels, discordmembers=discordmembers, embedmembers=embedmembers)
abort(403)
abort(404)
@api.route("/create_authenticated_user", methods=["POST"])
@discord_users_only(api=True)
def create_authenticated_user():
guild_id = request.form.get('guild_id')
if session['unauthenticated']:
response = jsonify(error=True)
response.status_code = 401
return response
else:
if not check_guild_existance(guild_id):
abort(404)
if not checkUserBanned(guild_id):
db_user = db.session.query(AuthenticatedUsers).filter(and_(AuthenticatedUsers.guild_id == guild_id, AuthenticatedUsers.client_id == session['user_id'])).first()
if not db_user:
db_user = AuthenticatedUsers(guild_id, session['user_id'])
db.session.add(db_user)
db.session.commit()
if not check_user_in_guild(guild_id):
discord_api.add_guild_member(guild_id, session['user_id'], session['user_keys']['access_token'])
status = update_user_status(guild_id, session['username'])
return jsonify(status=status)
else:
status = {'banned': True}
response = jsonify(status=status)
response.status_code = 403
return response
@api.route("/cleanup-keyval-db", methods=["DELETE"])
def cleanup_keyval_db():
if request.form.get("secret", None) == config["app-secret"]:
q = KeyValueProperties.query.filter(KeyValueProperties.expiration < datetime.datetime.now()).all()
for m in q:
db.session.delete(m)
db.session.commit()
return ('', 204)
abort(401)

View File

@ -0,0 +1 @@
from embed import embed

View File

@ -0,0 +1,41 @@
from flask import Blueprint, render_template, abort, redirect, url_for, session
from titanembeds.utils import check_guild_existance, discord_api, guild_query_unauth_users_bool
from titanembeds.oauth import generate_guild_icon_url, generate_avatar_url
from config import config
import random
embed = Blueprint("embed", __name__)
def get_logingreeting():
greetings = [
"Let's get to know each other! My name is Titan, what's yours?",
"Hello and welcome!",
"What brings you here today?",
"....what do you expect this text to say?",
"Aha! ..made you look!",
"Initiating launch sequence...",
"Captain, what's your option?",
"Alright, here's the usual~",
]
return random.choice(greetings)
@embed.route("/<string:guild_id>")
def guild_embed(guild_id):
if check_guild_existance(guild_id):
guild = discord_api.get_guild(guild_id)['content']
return render_template("embed.html.j2",
login_greeting=get_logingreeting(),
guild_id=guild_id, guild=guild,
generate_guild_icon=generate_guild_icon_url,
unauth_enabled=guild_query_unauth_users_bool(guild_id),
client_id=config['client-id']
)
abort(404)
@embed.route("/signin_complete")
def signin_complete():
return render_template("signin_complete.html.j2")
@embed.route("/login_discord")
def login_discord():
return redirect(url_for("user.login_authenticated", redirect=url_for("embed.signin_complete", _external=True)))

View File

@ -0,0 +1 @@
from user import user

View File

@ -0,0 +1,226 @@
from flask import Blueprint, request, redirect, jsonify, abort, session, url_for, render_template
from config import config
from titanembeds.decorators import discord_users_only
from titanembeds.utils import discord_api
from titanembeds.database import db, Guilds, UnauthenticatedUsers, UnauthenticatedBans
from titanembeds.oauth import authorize_url, token_url, make_authenticated_session, get_current_authenticated_user, get_user_managed_servers, check_user_can_administrate_guild, check_user_permission, generate_avatar_url, generate_guild_icon_url, generate_bot_invite_url
import time
import datetime
user = Blueprint("user", __name__)
@user.route("/login_authenticated", methods=["GET"])
def login_authenticated():
session["redirect"] = request.args.get("redirect")
scope = ['identify', 'guilds', 'guilds.join']
discord = make_authenticated_session(scope=scope)
authorization_url, state = discord.authorization_url(
authorize_url,
access_type="offline"
)
session['oauth2_state'] = state
return redirect(authorization_url)
@user.route('/callback', methods=["GET"])
def callback():
state = session.get('oauth2_state')
if not state or request.values.get('error'):
return redirect(url_for('user.logout'))
discord = make_authenticated_session(state=state)
discord_token = discord.fetch_token(
token_url,
client_secret=config['client-secret'],
authorization_response=request.url)
if not discord_token:
return redirect(url_for('user.logout'))
session['user_keys'] = discord_token
session['unauthenticated'] = False
user = get_current_authenticated_user()
session['user_id'] = user['id']
session['username'] = user['username']
session['discriminator'] = user['discriminator']
session['avatar'] = generate_avatar_url(user['id'], user['avatar'])
if session["redirect"]:
redir = session["redirect"]
session['redirect'] = None
return redirect(redir)
return redirect(url_for("user.dashboard"))
@user.route('/logout', methods=["GET"])
def logout():
redir = session.get("redirect", None)
session.clear()
if redir:
session['redirect'] = redir
return redirect(session['redirect'])
return redirect(url_for("index"))
@user.route("/dashboard")
@discord_users_only()
def dashboard():
guilds = get_user_managed_servers()
if not guilds:
session["redirect"] = url_for("user.dashboard")
return redirect(url_for("user.logout"))
error = request.args.get("error")
if session["redirect"] and not (error and error == "access_denied"):
redir = session['redirect']
session['redirect'] = None
return redirect(redir)
return render_template("dashboard.html.j2", servers=guilds, icon_generate=generate_guild_icon_url)
@user.route("/administrate_guild/<guild_id>", methods=["GET"])
@discord_users_only()
def administrate_guild(guild_id):
if not check_user_can_administrate_guild(guild_id):
return redirect(url_for("user.dashboard"))
guild = discord_api.get_guild(guild_id)
if guild['code'] != 200:
session["redirect"] = url_for("user.administrate_guild", guild_id=guild_id, _external=True)
return redirect(generate_bot_invite_url(guild_id))
session["redirect"] = None
db_guild = db.session.query(Guilds).filter(Guilds.guild_id == guild_id).first()
if not db_guild:
db_guild = Guilds(guild_id)
db.session.add(db_guild)
db.session.commit()
permissions=[]
if check_user_permission(guild_id, 5):
permissions.append("Manage Embed Settings")
if check_user_permission(guild_id, 2):
permissions.append("Ban Members")
if check_user_permission(guild_id, 1):
permissions.append("Kick Members")
all_members = db.session.query(UnauthenticatedUsers).filter(UnauthenticatedUsers.guild_id == guild_id).order_by(UnauthenticatedUsers.last_timestamp).all()
all_bans = db.session.query(UnauthenticatedBans).filter(UnauthenticatedBans.guild_id == guild_id).all()
users = prepare_guild_members_list(all_members, all_bans)
dbguild_dict = {"unauth_users": db_guild.unauth_users}
return render_template("administrate_guild.html.j2", guild=guild['content'], dbguild=dbguild_dict, members=users, permissions=permissions)
@user.route("/administrate_guild/<guild_id>", methods=["POST"])
@discord_users_only()
def update_administrate_guild(guild_id):
if not check_user_can_administrate_guild(guild_id):
abort(403)
guild = discord_api.get_guild(guild_id)
if guild['code'] != 200:
abort(guild['code'])
db_guild = db.session.query(Guilds).filter(Guilds.guild_id == guild_id).first()
if db_guild is None:
abort(400)
db_guild.unauth_users = request.form.get("unauth_users", db_guild.unauth_users) in ["true", True]
db.session.commit()
return jsonify(
id=db_guild.id,
guild_id=db_guild.guild_id,
unauth_users=db_guild.unauth_users,
)
@user.route('/me')
@discord_users_only()
def me():
return jsonify(user=get_current_authenticated_user())
def prepare_guild_members_list(members, bans):
all_users = []
ip_pool = []
members = sorted(members, key=lambda k: datetime.datetime.strptime(str(k.last_timestamp), "%Y-%m-%d %H:%M:%S"), reverse=True)
for member in members:
user = {
"id": member.id,
"username": member.username,
"discrim": member.discriminator,
"ip": member.ip_address,
"last_visit": member.last_timestamp,
"kicked": member.revoked,
"banned": False,
"banned_timestamp": None,
"banned_by": None,
"banned_reason": None,
"ban_lifted_by": None,
"aliases": [],
}
for banned in bans:
if banned.ip_address == member.ip_address:
if banned.lifter_id is None:
user['banned'] = True
user["banned_timestamp"] = banned.timestamp
user['banned_by'] = banned.placer_id
user['banned_reason'] = banned.reason
user['ban_lifted_by'] = banned.lifter_id
continue
if user["ip"] not in ip_pool:
all_users.append(user)
ip_pool.append(user["ip"])
else:
for usr in all_users:
if user["ip"] == usr["ip"]:
alias = user["username"]+"#"+str(user["discrim"])
if len(usr["aliases"]) < 5 and alias not in usr["aliases"]:
usr["aliases"].append(alias)
continue
return all_users
@user.route("/ban", methods=["POST"])
@discord_users_only(api=True)
def ban_unauthenticated_user():
guild_id = request.form.get("guild_id", None)
user_id = request.form.get("user_id", None)
reason = request.form.get("reason", None)
if reason is not None:
reason = reason.strip()
if reason == "":
reason = None
if not guild_id or not user_id:
abort(400)
if not check_user_permission(guild_id, 2):
abort(401)
db_user = db.session.query(UnauthenticatedUsers).filter(UnauthenticatedUsers.guild_id == guild_id, UnauthenticatedUsers.id == user_id).order_by(UnauthenticatedUsers.id.desc()).first()
if db_user is None:
abort(404)
db_ban = db.session.query(UnauthenticatedBans).filter(UnauthenticatedBans.guild_id == guild_id, UnauthenticatedBans.ip_address == db_user.ip_address).first()
if db_ban is not None:
if db_ban.lifter_id is None:
abort(409)
db.session.delete(db_ban)
db_ban = UnauthenticatedBans(guild_id, db_user.ip_address, db_user.username, db_user.discriminator, reason, session["user_id"])
db.session.add(db_ban)
db.session.commit()
return ('', 204)
@user.route("/ban", methods=["DELETE"])
@discord_users_only(api=True)
def unban_unauthenticated_user():
guild_id = request.args.get("guild_id", None)
user_id = request.args.get("user_id", None)
if not guild_id or not user_id:
abort(400)
if not check_user_permission(guild_id, 2):
abort(401)
db_user = db.session.query(UnauthenticatedUsers).filter(UnauthenticatedUsers.guild_id == guild_id, UnauthenticatedUsers.id == user_id).order_by(UnauthenticatedUsers.id.desc()).first()
if db_user is None:
abort(404)
db_ban = db.session.query(UnauthenticatedBans).filter(UnauthenticatedBans.guild_id == guild_id, UnauthenticatedBans.ip_address == db_user.ip_address).first()
if db_ban is None:
abort(404)
if db_ban.lifter_id is not None:
abort(409)
db_ban.liftBan(session["user_id"])
return ('', 204)
@user.route("/revoke", methods=["POST"])
@discord_users_only(api=True)
def revoke_unauthenticated_user():
guild_id = request.form.get("guild_id", None)
user_id = request.form.get("user_id", None)
if not guild_id or not user_id:
abort(400)
if not check_user_permission(guild_id, 1):
abort(401)
db_user = db.session.query(UnauthenticatedUsers).filter(UnauthenticatedUsers.guild_id == guild_id, UnauthenticatedUsers.id == user_id).order_by(UnauthenticatedUsers.id.desc()).first()
if db_user is None:
abort(404)
if db_user.isRevoked():
abort(409)
db_user.revokeUser()
return ('', 204)

View File

@ -0,0 +1,9 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
from guilds import Guilds
from unauthenticated_users import UnauthenticatedUsers
from unauthenticated_bans import UnauthenticatedBans
from authenticated_users import AuthenticatedUsers
from keyvalue_properties import KeyValueProperties, set_keyvalproperty, get_keyvalproperty, getexpir_keyvalproperty, setexpir_keyvalproperty, ifexists_keyvalproperty, delete_keyvalproperty

View File

@ -0,0 +1,20 @@
from titanembeds.database import db
import datetime
import time
class AuthenticatedUsers(db.Model):
__tablename__ = "authenticated_users"
id = db.Column(db.Integer, primary_key=True) # Auto increment id
guild_id = db.Column(db.String(255)) # Guild pretaining to the authenticated user
client_id = db.Column(db.String(255)) # Client ID of the authenticated user
last_timestamp = db.Column(db.TIMESTAMP) # The timestamp of when the user has last sent the heartbeat
def __init__(self, guild_id, client_id):
self.guild_id = guild_id
self.client_id = client_id
self.last_timestamp = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S')
def bumpTimestamp(self):
self.last_timestamp = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S')
db.session.commit()
return self.last_timestamp

View File

@ -0,0 +1,35 @@
import urlparse
from limits.storage import Storage
from redislite import Redis
import time
class LimitsRedisLite(Storage): # For Python Limits
STORAGE_SCHEME = "redislite"
def __init__(self, uri, **options):
self.redis_instance = Redis(urlparse.urlparse(uri).netloc)
def check(self):
return True
def get_expiry(self, key):
return (self.redis_instance.ttl(key) or 0) + time.time()
def incr(self, key, expiry, elastic_expiry=False):
if not self.redis_instance.exists(key):
self.redis_instance.set(key, 1, ex=expiry)
else:
oldexp = oldexp = self.get_expiry(key) - time.time()
if oldexp <= 0:
self.redis_instance.delete(key)
return self.incr(key, expiry, elastic_expiry)
self.redis_instance.set(key, int(self.redis_instance.get(key))+1, ex=int(round(oldexp)))
return int(self.get(key))
def get(self, key):
value = self.redis_instance.get(key)
if value:
return int(value)
return 0
def reset(self):
return self.redis_instance.flushdb()

View File

@ -0,0 +1,19 @@
from titanembeds.database import db
class Guilds(db.Model):
__tablename__ = "guilds"
id = db.Column(db.Integer, primary_key=True) # Auto incremented id
guild_id = db.Column(db.String(255)) # Discord guild id
unauth_users = db.Column(db.Boolean()) # If allowed unauth users
def __init__(self, guild_id):
self.guild_id = guild_id
self.unauth_users = True # defaults to true
def __repr__(self):
return '<Guilds {0} {1}>'.format(self.id, self.guild_id)
def set_unauthUsersBool(self, value):
self.unauth_users = value
db.session.commit()
return self.unauth_users

View File

@ -0,0 +1,98 @@
from titanembeds.database import db
from datetime import datetime, timedelta
from limits.storage import Storage
import time
def set_keyvalproperty(key, value, expiration=None):
q = db.session.query(KeyValueProperties).filter(KeyValueProperties.key == key)
if q.count() == 0:
db.session.add(KeyValueProperties(key=key, value=value, expiration=expiration))
else:
if expiration is not None:
converted_expr = datetime.fromtimestamp(time.time() + expiration)
else:
converted_expr = None
firstobj = q.first()
firstobj.value = value
firstobj.expiration = converted_expr
db.session.commit()
def get_keyvalproperty(key):
q = db.session.query(KeyValueProperties).filter(KeyValueProperties.key == key)
now = datetime.now()
if q.count() > 0 and (q.first().expiration is None or q.first().expiration > now):
return q.first().value
return None
def getexpir_keyvalproperty(key):
q = db.session.query(KeyValueProperties).filter(KeyValueProperties.key == key)
now = datetime.now()
if q.count() > 0 and (q.first().expiration is not None and q.first().expiration > now):
return int(q.first().expiration.strftime('%s'))
return 0
def setexpir_keyvalproperty(key, expiration=None):
q = db.session.query(KeyValueProperties).filter(KeyValueProperties.key == key)
if q.count() > 0:
if expiration:
q.first().expiration = datetime.now()
else:
q.first().expiration = None
db.session.commit()
def ifexists_keyvalproperty(key):
q = db.session.query(KeyValueProperties).filter(KeyValueProperties.key == key)
return q.count() > 0
def delete_keyvalproperty(key):
q = db.session.query(KeyValueProperties).filter(KeyValueProperties.key == key).first()
if q:
db.session.delete(q)
db.session.commit()
class KeyValueProperties(db.Model):
__tablename__ = "keyvalue_properties"
id = db.Column(db.Integer, primary_key=True) # Auto incremented id
key = db.Column(db.String(255)) # Property Key
value = db.Column(db.Text()) # Property value
expiration = db.Column(db.TIMESTAMP) # Suggested Expiration for value (None = no expire) in secs
def __init__(self, key, value, expiration=None):
self.key = key
self.value = value
if expiration:
self.expiration = datetime.now() + timedelta(seconds = expiration)
else:
self.expiration = None
class LimitsKeyValueProperties(Storage): # For Python Limits
STORAGE_SCHEME = "keyvalprops"
def __init__(self, uri, **options):
pass
def check(self):
return True
def get_expiry(self, key):
return getexpir_keyvalproperty(key) + time.time()
def incr(self, key, expiry, elastic_expiry=False):
if not ifexists_keyvalproperty(key):
set_keyvalproperty(key, 1, expiration=expiry)
else:
oldexp = getexpir_keyvalproperty(key) - time.time()
if oldexp <= 0:
delete_keyvalproperty(key)
return self.incr(key, expiry, elastic_expiry)
set_keyvalproperty(key, int(get_keyvalproperty(key))+1, expiration=int(round(oldexp)))
return int(self.get(key))
def get(self, key):
value = get_keyvalproperty(key)
if value:
return int(value)
return 0
def reset(self):
return False

View File

@ -0,0 +1,33 @@
from titanembeds.database import db
import datetime
import time
class UnauthenticatedBans(db.Model):
__tablename__ = "unauthenticated_bans"
id = db.Column(db.Integer, primary_key=True) # Auto increment id
guild_id = db.Column(db.String(255)) # Guild pretaining to the unauthenticated user
ip_address = db.Column(db.String(255)) # The IP Address of the user
last_username = db.Column(db.String(255)) # The username when they got banned
last_discriminator = db.Column(db.Integer) # The discrim when they got banned
timestamp = db.Column(db.TIMESTAMP) # The timestamp of when the user got banned
reason = db.Column(db.Text()) # The reason of the ban set by the guild moderators
lifter_id = db.Column(db.String(255)) # Discord Client ID of the user who lifted the ban
placer_id = db.Column(db.String(255)) # The id of who placed the ban
def __init__(self, guild_id, ip_address, last_username, last_discriminator, reason, placer_id):
self.guild_id = guild_id
self.ip_address = ip_address
self.last_username = last_username
self.last_discriminator = last_discriminator
self.timestamp = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S')
self.reason = reason
self.lifter_id = None
self.placer_id = placer_id
def liftBan(self, lifter_id):
self.lifter_id = lifter_id
db.session.commit()
return self.lifter_id
def __repr__(self):
return '<UnauthenticatedBans {0} {1} {2} {3} {4} {5}'.format(self.id, self.guild_id, self.ip_address, self.last_username, self.last_discriminator, self.timestamp)

View File

@ -0,0 +1,46 @@
from titanembeds.database import db
import datetime
import time
import random
import string
class UnauthenticatedUsers(db.Model):
__tablename__ = "unauthenticated_users"
id = db.Column(db.Integer, primary_key=True) # Auto increment id
guild_id = db.Column(db.String(255)) # Guild pretaining to the unauthenticated user
username = db.Column(db.String(255)) # The username of the user
discriminator = db.Column(db.Integer) # The discriminator to distinguish unauth users with each other
user_key = db.Column(db.Text()) # The secret key used to identify the user holder
ip_address = db.Column(db.String(255)) # The IP Address of the user
last_timestamp = db.Column(db.TIMESTAMP) # The timestamp of when the user has last sent the heartbeat
revoked = db.Column(db.Boolean()) # If the user's key has been revoked and a new one is required to be generated
def __init__(self, guild_id, username, discriminator, ip_address):
self.guild_id = guild_id
self.username = username
self.discriminator = discriminator
self.user_key = "".join(random.choice(string.ascii_letters) for _ in range(0, 32))
self.ip_address = ip_address
self.last_timestamp = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S')
self.revoked = False
def __repr__(self):
return '<UnauthenticatedUsers {0} {1} {2} {3} {4} {5} {6} {7}>'.format(self.id, self.guild_id, self.username, self.discriminator, self.user_key, self.ip_address, self.last_timestamp, self.revoked)
def isRevoked(self):
return self.revoked
def changeUsername(self, username):
self.username = username
db.session.commit()
return self.username
def revokeUser(self):
self.revoked = True
db.session.commit()
return self.revoked
def bumpTimestamp(self):
self.last_timestamp = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S')
db.session.commit()
return self.last_timestamp

View File

@ -0,0 +1,28 @@
from functools import wraps
from flask import url_for, redirect, session, jsonify, abort
def valid_session_required(api=False):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'unauthenticated' not in session or 'user_id' not in session or 'username' not in session:
if api:
return jsonify(error=True, message="Unauthenticated session"), 401
redirect(url_for('user.logout'))
if session['unauthenticated'] and 'user_keys' not in session:
session['user_keys'] = {}
return f(*args, **kwargs)
return decorated_function
return decorator
def discord_users_only(api=False):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'unauthenticated' not in session or session['unauthenticated']:
if api:
return jsonify(error=True, message="Not logged in as a discord user"), 401
return redirect(url_for("user.login_authenticated"))
return f(*args, **kwargs)
return decorated_function
return decorator

View File

@ -0,0 +1,230 @@
import requests
import sys
import time
import json
from titanembeds.utils import cache
from titanembeds.database import db, KeyValueProperties, get_keyvalproperty, set_keyvalproperty, ifexists_keyvalproperty
from flask import request
_DISCORD_API_BASE = "https://discordapp.com/api/v6"
def json_or_text(response):
text = response.text
if response.headers['content-type'] == 'application/json':
return response.json()
return text
class DiscordREST:
def __init__(self, bot_token):
self.global_redis_prefix = "discordapiratelimit/"
self.bot_token = bot_token
self.user_agent = "TitanEmbeds (https://github.com/EndenDragon/Titan) Python/{} requests/{}".format(sys.version_info, requests.__version__)
def init_discordrest(self):
if not self._bucket_contains("global_limited"):
self._set_bucket("global_limited", False)
self._set_bucket("global_limit_expire", 0)
def _get_bucket(self, key):
value = get_keyvalproperty(self.global_redis_prefix + key)
return value
def _set_bucket(self, key, value):
return set_keyvalproperty(self.global_redis_prefix + key, value)
def _bucket_contains(self, key):
return ifexists_keyvalproperty(self.global_redis_prefix + key)
def request(self, verb, url, **kwargs):
headers = {
'User-Agent': self.user_agent,
'Authorization': 'Bot {}'.format(self.bot_token),
}
params = None
if 'params' in kwargs:
params = kwargs['params']
data = None
if 'data' in kwargs:
data = kwargs['data']
if 'json' in kwargs:
headers['Content-Type'] = 'application/json'
data = json.dumps(data)
for tries in range(5):
curepoch = time.time()
if self._get_bucket("global_limited") == "True":
time.sleep(int(self._get_bucket("global_limit_expire")) - curepoch)
curepoch = time.time()
if self._bucket_contains(url) and int(self._get_bucket(url)) > curepoch:
time.sleep(int(self._get_bucket(url)) - curepoch)
url_formatted = _DISCORD_API_BASE + url
req = requests.request(verb, url_formatted, params=params, data=data, headers=headers)
remaining = None
if 'X-RateLimit-Remaining' in req.headers:
remaining = req.headers['X-RateLimit-Remaining']
if remaining == '0' and req.status_code != 429:
self._set_bucket(url, int(req.headers['X-RateLimit-Reset']))
if 300 > req.status_code >= 200:
self._set_bucket("global_limited", False)
return {
'success': True,
'content': json_or_text(req),
'code': req.status_code,
}
if req.status_code == 429:
if 'X-RateLimit-Global' not in req.headers:
self._set_bucket(url, int(req.headers['X-RateLimit-Reset']))
else:
self._set_bucket("global_limited", True)
self._set_bucket("global_limit_expire", time.time() + int(req.headers['Retry-After']))
if req.status_code == 502 and tries <= 5:
time.sleep(1 + tries * 2)
continue
if req.status_code == 403 or req.status_code == 404:
return {
'success': False,
'code': req.status_code,
}
return {
'success': False,
'code': req.status_code,
'content': json_or_text(req),
}
#####################
# Channel
#####################
def get_channel_messages(self, channel_id, after_snowflake=None):
_endpoint = "/channels/{channel_id}/messages".format(channel_id=channel_id)
params = {}
if after_snowflake is not None:
params = {'after': after_snowflake}
r = self.request("GET", _endpoint, params=params)
return r
def create_message(self, channel_id, content):
_endpoint = "/channels/{channel_id}/messages".format(channel_id=channel_id)
payload = {'content': content}
r = self.request("POST", _endpoint, data=payload)
return r
#####################
# Guild
#####################
def get_guild(self, guild_id):
_endpoint = "/guilds/{guild_id}".format(guild_id=guild_id)
r = self.request("GET", _endpoint)
return r
@cache.cache('get_guild_channels', expire=200)
def get_guild_channels(self, guild_id):
_endpoint = "/guilds/{guild_id}/channels".format(guild_id=guild_id)
r = self.request("GET", _endpoint)
return r
def get_guild_roles(self, guild_id):
_endpoint = "/guilds/{guild_id}/roles".format(guild_id=guild_id)
r = self.request("GET", _endpoint)
return r
@cache.cache('get_guild_member', expire=200)
def get_guild_member(self, guild_id, user_id):
_endpoint = "/guilds/{guild_id}/members/{user_id}".format(guild_id=guild_id, user_id=user_id)
r = self.request("GET", _endpoint)
return r
def get_guild_member_nocache(self, guild_id, user_id):
_endpoint = "/guilds/{guild_id}/members/{user_id}".format(guild_id=guild_id, user_id=user_id)
r = self.request("GET", _endpoint)
return r
def modify_guild_member(self, guild_id, user_id, **kwargs):
_endpoint = "/guilds/{guild_id}/members/{user_id}".format(guild_id=guild_id, user_id=user_id)
r = self.request("PATCH", _endpoint, data=kwargs, json=True)
return r
def add_guild_member(self, guild_id, user_id, access_token, **kwargs):
_endpoint = "/guilds/{guild_id}/members/{user_id}".format(user_id=user_id, guild_id=guild_id)
payload = {'access_token': access_token}
payload.update(kwargs)
r = self.request("PUT", _endpoint, data=payload, json=True)
return r
def get_guild_embed(self, guild_id):
_endpoint = "/guilds/{guild_id}/embed".format(guild_id=guild_id)
r = self.request("GET", _endpoint)
return r
def modify_guild_embed(self, guild_id, **kwargs):
_endpoint = "/guilds/{guild_id}/embed".format(guild_id=guild_id)
r = self.request("PATCH", _endpoint, data=kwargs, json=True)
return r
def get_guild_bans(self, guild_id):
_endpoint = "/guilds/{guild_id}/bans".format(guild_id=guild_id)
r = self.request("GET", _endpoint)
return r
@cache.cache('list_all_guild_members', expire=200)
def list_all_guild_members(self, guild_id):
_endpoint = "/guilds/{guild_id}/members".format(guild_id=guild_id)
count = 1
last_usrid = ""
users = []
params = {"limit": 1000}
while count > 0:
r = self.request("GET", _endpoint, params=params)
if r["success"] == True:
content = r["content"]
count = len(content)
users.extend(content)
if count > 0:
params["after"] = content[-1]["user"]["id"]
else:
count = 0
return users
#####################
# User
#####################
@cache.cache('get_all_guilds', expire=100)
def get_all_guilds(self):
_endpoint = "/users/@me/guilds"
params = {}
guilds = []
count = 1 #priming the loop
last_guild = ""
while count > 0:
r = self.request("GET", _endpoint, params=params)
if r['success'] == True:
content = r['content']
count = len(content)
guilds.extend(content)
if count > 0:
params['after'] = content[-1]['id']
else:
count = 0
return guilds
#####################
# Widget Handler
#####################
@cache.cache('get_widget', expire=200)
def get_widget(self, guild_id):
_endpoint = _DISCORD_API_BASE + "/servers/{guild_id}/widget.json".format(guild_id=guild_id)
embed = self.get_guild_embed(guild_id)
if not embed['content']['enabled']:
self.modify_guild_embed(guild_id, enabled=True, channel_id=guild_id)
widget = requests.get(_endpoint).json()
return widget

View File

@ -0,0 +1,95 @@
from config import config
from requests_oauthlib import OAuth2Session
from titanembeds.utils import cache, make_guilds_cache_key
from flask import session, abort, url_for
authorize_url = "https://discordapp.com/api/oauth2/authorize"
token_url = "https://discordapp.com/api/oauth2/token"
avatar_base_url = "https://cdn.discordapp.com/avatars/"
guild_icon_url = "https://cdn.discordapp.com/icons/"
def update_user_token(discord_token):
session['user_keys'] = discord_token
def make_authenticated_session(token=None, state=None, scope=None):
return OAuth2Session(
client_id=config['client-id'],
token=token,
state=state,
scope=scope,
redirect_uri=url_for("user.callback", _external=True),
auto_refresh_kwargs={
'client_id': config['client-id'],
'client_secret': config['client-secret'],
},
auto_refresh_url=token_url,
token_updater=update_user_token,
)
def discordrest_from_user(endpoint):
token = session['user_keys']
discord = make_authenticated_session(token=token)
req = discord.get("https://discordapp.com/api/v6{}".format(endpoint))
return req
def get_current_authenticated_user():
req = discordrest_from_user("/users/@me")
if req.status_code != 200:
abort(req.status_code)
user = req.json()
return user
def user_has_permission(permission, index):
return bool((int(permission) >> index) & 1)
@cache.cache(make_guilds_cache_key, expire=120)
def get_user_guilds():
req = discordrest_from_user("/users/@me/guilds")
return req
def get_user_managed_servers():
guilds = get_user_guilds()
if guilds.status_code != 200:
abort(guilds.status_code)
guilds = guilds.json()
filtered = []
for guild in guilds:
permission = guild['permissions'] # Manage Server, Ban Members, Kick Members
if guild['owner'] or user_has_permission(permission, 5) or user_has_permission(permission, 2) or user_has_permission(permission, 1):
filtered.append(guild)
filtered = sorted(filtered, key=lambda guild: guild['name'])
return filtered
def get_user_managed_servers_safe():
guilds = get_user_managed_servers()
if guilds:
return guilds
return []
def get_user_managed_servers_id():
guilds = get_user_managed_servers_safe()
ids=[]
for guild in guilds:
ids.append(guild['id'])
return ids
def check_user_can_administrate_guild(guild_id):
guilds = get_user_managed_servers_id()
return guild_id in guilds
def check_user_permission(guild_id, id):
guilds = get_user_managed_servers_safe()
for guild in guilds:
if guild['id'] == guild_id:
return user_has_permission(guild['permissions'], id) or guild['owner']
return False
def generate_avatar_url(id, av):
return avatar_base_url + str(id) + '/' + str(av) + '.jpg'
def generate_guild_icon_url(id, hash):
return guild_icon_url + str(id) + "/" + str(hash) + ".jpg"
def generate_bot_invite_url(guild_id):
url = "https://discordapp.com/oauth2/authorize?&client_id={}&scope=bot&permissions={}&guild_id={}&response_type=code&redirect_uri={}".format(config['client-id'], '536083583', guild_id, url_for("user.dashboard", _external=True))
return url

View File

@ -0,0 +1,77 @@
/* Responsive table CSS, directly from materializecss - slightly modified */
/* Used to accomplish having permanent horizontal table */
table.responsive-table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
display: block;
position: relative;
table-layout: fixed;
/* sort out borders */
}
table.responsive-table td:empty:before {
content: '\00a0';
}
table.responsive-table th,
table.responsive-table td {
margin: 0;
vertical-align: top;
}
table.responsive-table th {
text-align: left;
}
table.responsive-table thead {
display: block;
float: left;
}
table.responsive-table thead tr {
display: block;
padding: 0 10px 0 0;
}
table.responsive-table thead tr th::before {
content: "\00a0";
}
table.responsive-table tbody {
display: block;
width: auto;
position: relative;
overflow-x: auto;
white-space: nowrap;
}
table.responsive-table tbody tr {
display: inline-block;
vertical-align: top;
}
table.responsive-table th {
display: block;
text-align: right;
}
table.responsive-table td {
display: block;
min-height: 1.25em;
text-align: left;
padding: 13px 5px;
}
table.responsive-table tr {
padding: 0 10px;
}
table.responsive-table thead {
border: 0;
border-right: 1px solid #d0d0d0;
}
table.responsive-table.bordered th {
border-bottom: 0;
border-left: 0;
}
table.responsive-table.bordered td {
border-left: 0;
border-right: 0;
border-bottom: 0;
}
table.responsive-table.bordered tr {
border: 0;
}
table.responsive-table.bordered tbody tr {
border-right: 1px solid #d0d0d0;
}

View File

@ -0,0 +1,187 @@
html {
background-color: #455a64;
color: white;
}
main {
min-height: calc(100vh - 80px);
overflow-x: hidden;
}
footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background-color: #37474f;
}
nav {
background-color: #263238;
background: linear-gradient(rgba(38, 50, 56, 1), rgba(255,0,0,0));
box-shadow: none;
}
nav .brand-logo {
font-size: 1.5rem;
}
@media only screen and (min-width: 993px) {
.container {
width: 85%;
}
}
.side-nav {
color: white;
background-color: #607d8b;
}
.side-nav .userView .name {
font-size: 20px;
}
.side-nav li>a {
color: #eceff1;
}
.side-nav .subheader {
color: #cfd8dc;
font-variant: small-caps;
}
.role-title {
margin-bottom: -15px !important;
font-variant: normal !important;
font-size: 80% !important;
}
.divider {
background-color: #90a4ae;
}
.channel-hash {
font-size: 95%;
color: #b0bec5;
}
.membercircle {
margin-top: 5px;
height: 40px;
}
.membername {
position: absolute;
padding-left: 10px;
}
.chatcontent {
padding-left: 1%;
padding-top: 1%;
padding-bottom: 40px;
/* https://css-tricks.com/snippets/css/prevent-long-urls-from-breaking-out-of-container/ */
/* These are technically the same, but use both */
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
/* This is the dangerous one in WebKit, as it breaks things wherever */
word-break: break-all;
/* Instead use this non-standard one: */
word-break: break-word;
/* Adds a hyphen where the word breaks, if supported (No Blink) */
-ms-hyphens: auto;
-moz-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}
@media only screen and (min-width: 601px) {
nav a.button-collapse {
display: block;
}
}
.chatusername {
font-weight: bold;
color: #eceff1;
}
.chattimestamp {
font-size: 10px;
color: #90a4ae;
margin-right: 3px;
}
.footercontainer {
width: 100%;
position: relative;
margin: 10px;
white-space: nowrap;
overflow: hidden;
}
#messageboxouter {
width: 100%;
overflow: hidden;
}
.currentuserchip {
display: inline-block;
position: relative;
top: -6px;
padding: 6px;
padding-right: 9px;
background-color: #455a64;
}
.currentuserimage {
width: 30px;
}
.currentusername {
position: relative;
top: 7px;
left: 5px;
}
.input-field {
position: relative;
top: -19px;
}
.left {
float: left;
}
.modal {
background-color: #546e7a;
}
.betatag {
font-variant: small-caps;
font-size: 15px;
color: #eceff1;
}
#channeltopic {
width: 80%;
margin-left: 30px;
margin-right: auto;
font-size: 85%;
}
a {
color: #82b1ff;
}
#fetching-indicator {
position: absolute;
right: 1vw;
bottom: 13px;
width: 25px;
height: 25px;
}

View File

@ -0,0 +1,60 @@
html {
background-color: #7986cb;
color: white;
}
body {
display: flex;
min-height: 100vh;
flex-direction: column;
}
main {
flex: 1 0 auto;
}
nav {
background-color: #3f51b5;
background: linear-gradient(rgba(63, 81, 181, 1), rgba(255,0,0,0));
box-shadow: none;
}
.page-footer {
background-color: transparent;
}
@media only screen and (max-width: 992px) {
nav .brand-logo {
left: 10%;
}
}
.btn {
background-color: #303f9f;
}
.btn:hover {
background-color: #3f51b5;
}
.btn:focus {
background-color: #536dfe;
}
.avatar_menu {
background-size: contain;
}
.center_content {
display: block;
margin-left: auto;
margin-right: auto;
}
.betatag {
font-variant: small-caps;
font-size: 25px;
border-radius: 50px;
border: 2px solid #37474f;
color: #eceff1;
}

View File

@ -0,0 +1,77 @@
$('#unauth_users').change(function() {
var pathname = window.location.pathname;
var checked = $(this).is(':checked')
var payload = {"unauth_users": checked}
$.post(pathname, payload, function(data) {
Materialize.toast('Updated guest users setting!', 2000)
});
});
function initiate_ban(guild_id, user_id) {
var reason = prompt("Please enter your reason for ban");
var payload = {
"reason": reason,
"guild_id": guild_id,
"user_id": user_id,
}
var pathname = document.location.origin + "/user/ban"
if (reason != null) {
$.post(pathname, payload)
.done(function(){
location.reload();
})
.fail(function(xhr, status, error) {
if (error == "CONFLICT") {
Materialize.toast('User is already banned!', 2000)
} else {
Materialize.toast('An error has occured!', 2000)
}
});
}
}
function remove_ban(guild_id, user_id) {
var payload = {
"guild_id": guild_id,
"user_id": user_id,
}
var pathname = document.location.origin + "/user/ban"
$.ajax({
url: pathname + '?' + $.param(payload),
type: 'DELETE',
success: function() {
location.reload();
},
error: function(jqxhr, status, error) {
if (error == "CONFLICT") {
Materialize.toast('User is already pardoned!', 2000)
} else {
Materialize.toast('An error has occured!', 2000)
}
}
});
}
function revoke_user(guild_id, user_id) {
var payload = {
"guild_id": guild_id,
"user_id": user_id,
}
var confirmation = confirm("Are you sure that you want to kick user?")
var pathname = document.location.origin + "/user/revoke"
if (confirmation) {
$.post(pathname, payload)
.done(function(){
location.reload();
})
.fail(function(xhr, status, error) {
if (error == "CONFLICT") {
Materialize.toast('User is already revoked!', 2000)
} else {
Materialize.toast('An error has occured!', 2000)
}
});
}
}

View File

@ -0,0 +1,550 @@
/* global $ */
/* global Materialize */
/* global Mustache */
/* global guild_id */
/* global bot_client_id */
/* global moment */
(function () {
var logintimer; // timer to keep track of user inactivity after hitting login
var fetchtimeout; // fetch routine timer
var currently_fetching; // fetch lock- if true, do not fetch
var last_message_id; // last message tracked
var selected_channel = guild_id; // user selected channel, defaults to #general channel
var guild_channels = {}; // all server channels used to highlight channels in messages
var times_fetched = 0; // kept track of how many times that it has fetched
var fetch_error_count = 0; // Number of errors fetch has encountered
function element_in_view(element, fullyInView) {
var pageTop = $(window).scrollTop();
var pageBottom = pageTop + $(window).height();
var elementTop = $(element).offset().top;
var elementBottom = elementTop + $(element).height();
if (fullyInView === true) {
return ((pageTop < elementTop) && (pageBottom > elementBottom));
} else {
return ((elementTop <= pageBottom) && (elementBottom >= pageTop));
}
}
function query_guild() {
var funct = $.ajax({
dataType: "json",
url: "/api/query_guild",
data: {"guild_id": guild_id}
});
return funct.promise();
}
function create_authenticated_user() {
var funct = $.ajax({
method: "POST",
dataType: "json",
url: "/api/create_authenticated_user",
data: {"guild_id": guild_id}
});
return funct.promise();
}
function create_unauthenticated_user(username) {
var funct = $.ajax({
method: "POST",
dataType: "json",
url: "/api/create_unauthenticated_user",
data: {"username": username, "guild_id": guild_id}
});
return funct.promise();
}
function fetch(channel_id, after=null) {
var funct = $.ajax({
method: "GET",
dataType: "json",
url: "/api/fetch",
data: {"guild_id": guild_id,"channel_id": channel_id, "after": after}
});
return funct.promise();
}
function post(channel_id, content) {
var funct = $.ajax({
method: "POST",
dataType: "json",
url: "/api/post",
data: {"guild_id": guild_id, "channel_id": channel_id, "content": content}
});
return funct.promise();
}
$(function(){
$("#loginmodal").modal({
dismissible: false, // Modal can be dismissed by clicking outside of the modal
opacity: .5, // Opacity of modal background
inDuration: 300, // Transition in duration
outDuration: 200, // Transition out duration
startingTop: '4%', // Starting top style attribute
endingTop: '10%', // Ending top style attribute
}
);
$('#loginmodal').modal('open');
lock_login_fields();
var guild = query_guild();
guild.fail(function() {
unlock_login_fields();
});
guild.done(function(data) {
initialize_embed(data);
});
});
function lock_login_fields() {
$("#loginProgress").show();
$("#discordlogin_btn").attr("disabled",true);
$("#custom_username_field").prop("disabled",true);
logintimer = setTimeout(function() {
unlock_login_fields();
}, 60000);
}
function unlock_login_fields() {
$("#loginProgress").hide();
$("#discordlogin_btn").attr("disabled",false);
$("#custom_username_field").prop("disabled",false);
clearTimeout(logintimer);
}
function initialize_embed(guildobj) {
if (guildobj === undefined) {
var guild = query_guild();
guild.done(function(data) {
prepare_guild(data);
$('#loginmodal').modal('close');
unlock_login_fields();
});
} else {
prepare_guild(guildobj);
$('#loginmodal').modal('close');
unlock_login_fields();
}
}
function prepare_guild(guildobj) {
fill_channels(guildobj.channels);
fill_discord_members(guildobj.discordmembers);
fill_authenticated_users(guildobj.embedmembers.authenticated);
fill_unauthenticated_users(guildobj.embedmembers.unauthenticated);
run_fetch_routine();
}
function fill_channels(channels) {
var template = $('#mustache_channellistings').html();
Mustache.parse(template);
$("#channels-list").empty();
for (var i = 0; i < channels.length; i++) {
var chan = channels[i];
guild_channels[chan.channel.id] = chan;
if (chan.read) {
var rendered = Mustache.render(template, {"channelid": chan.channel.id, "channelname": chan.channel.name});
$("#channels-list").append(rendered);
$("#channel-" + chan.channel.id.toString()).click({"channel_id": chan.channel.id.toString()}, function(event) {
select_channel(event.data.channel_id);
});
if (chan.channel.id == selected_channel) {
if (chan.write) {
$("#messagebox").prop('disabled', false);
$("#messagebox").prop('placeholder', "Enter message");
} else {
$("#messagebox").prop('disabled', true);
$("#messagebox").prop('placeholder', "Messages is disabled in this channel.");
}
$("#channeltopic").text(chan.channel.topic);
}
}
}
$("#channel-"+selected_channel).parent().addClass("active");
}
function mention_member(member_id) {
if (!$('#messagebox').prop('disabled')) {
$('#messagebox').val( $('#messagebox').val() + "[@" + member_id + "] " );
$('.button-collapse').sideNav('hide');
$("#messagebox").focus();
}
}
function fill_discord_members(discordmembers) {
var template = $('#mustache_authedusers').html();
Mustache.parse(template);
$("#discord-members").empty();
var guild_members = {};
for (var i = 0; i < discordmembers.length; i++) {
var member = discordmembers[i];
if (member["hoist-role"]) {
if (!(member["hoist-role"]["id"] in guild_members)) {
guild_members[member["hoist-role"]["id"]] = {};
guild_members[member["hoist-role"]["id"]]["name"] = member["hoist-role"]["name"];
guild_members[member["hoist-role"]["id"]]["members"] = [];
guild_members[member["hoist-role"]["id"]]["position"] = member["hoist-role"]["position"]
}
guild_members[member["hoist-role"]["id"]]["members"].push(member);
} else {
if (!("0" in guild_members)) {
guild_members["0"] = {};
guild_members["0"]["name"] = null;
guild_members["0"]["members"] = [];
guild_members["0"]["position"] = 0;
}
guild_members["0"]["members"].push(member);
}
}
var guild_members_arr = [];
for (key in guild_members) {
guild_members_arr.push(guild_members[key]);
}
guild_members_arr.sort(function(a, b) {
return parseInt(b.position) - parseInt(a.position);
});
var template_role = $('#mustache_memberrole').html();
Mustache.parse(template_role);
var template_user = $('#mustache_authedusers').html();
Mustache.parse(template_user);
$("#discord-members").empty();
var discordmembercnt = 0;
for (var i = 0; i < guild_members_arr.length; i++) {
var roleobj = guild_members_arr[i];
if (!roleobj["name"]) {
roleobj["name"] = "Uncategorized";
}
var rendered_role = Mustache.render(template_role, {"name": roleobj["name"] + " - " + roleobj["members"].length});
discordmembercnt += roleobj["members"].length;
$("#discord-members").append(rendered_role);
for (var j = 0; j < roleobj.members.length; j++) {
var member = roleobj.members[j];
var rendered_user = Mustache.render(template_user, {"id": member.id.toString() + "d", "username": member.username, "avatar": member.avatar_url});
$("#discord-members").append(rendered_user);
$( "#discorduser-" + member.id.toString() + "d").click({"member_id": member.id.toString()}, function(event) {
mention_member(event.data.member_id);
});
if (member.color) {
$( "#discorduser-" + member.id.toString() + "d").css("color", "#" + member.color);
}
}
}
$("#discord-members-count").html(discordmembercnt);
}
function fill_authenticated_users(users) {
var template = $('#mustache_authedusers').html();
Mustache.parse(template);
$("#embed-discord-members").empty();
$("#embed-discord-members-count").html(users.length);
for (var i = 0; i < users.length; i++) {
var member = users[i];
var rendered = Mustache.render(template, {"id": member.id.toString() + "a", "username": member.username, "avatar": member.avatar_url});
$("#embed-discord-members").append(rendered);
$( "#discorduser-" + member.id.toString() + "a").click({"member_id": member.id.toString()}, function(event) {
mention_member(event.data.member_id);
});
}
}
function fill_unauthenticated_users(users) {
var template = $('#mustache_unauthedusers').html();
Mustache.parse(template);
$("#embed-unauth-users").empty();
$("#guest-members-count").html(users.length);
for (var i = 0; i < users.length; i++) {
var member = users[i];
var rendered = Mustache.render(template, {"username": member.username, "discriminator": member.discriminator});
$("#embed-unauth-users").append(rendered);
}
}
function wait_for_discord_login() {
_wait_for_discord_login(0);
}
function _wait_for_discord_login(index) {
setTimeout(function() {
var usr = create_authenticated_user();
usr.done(function(data) {
initialize_embed();
return;
});
usr.fail(function(data) {
if (data.status == 403) {
Materialize.toast('Authentication error! You have been banned.', 10000);
} else if (index < 10) {
_wait_for_discord_login(index + 1);
}
});
}, 5000);
}
function select_channel(channel_id) {
if (selected_channel != channel_id) {
selected_channel = channel_id;
last_message_id = null;
$("#channels-list > li.active").removeClass("active");
$("#channel-"+selected_channel).parent().addClass("active");
clearTimeout(fetchtimeout);
run_fetch_routine();
}
}
function replace_message_mentions(message) {
var mentions = message.mentions;
for (var i = 0; i < mentions.length; i++) {
var mention = mentions[i];
message.content = message.content.replace(new RegExp("<@" + mention.id + ">", 'g'), "@" + mention.username + "#" + mention.discriminator);
message.content = message.content.replace(new RegExp("<@!" + mention.id + ">", 'g'), "@" + mention.username + "#" + mention.discriminator);
message.content = message.content.replace("<@&" + guild_id + ">", "@everyone");
}
return message;
}
function getPosition(string, subString, index) {
return string.split(subString, index).join(subString).length;
}
function format_bot_message(message) {
if (message.author.id == bot_client_id && (message.content.includes("**") && ( (message.content.includes("<")&&message.content.includes(">")) || (message.content.includes("[") && message.content.includes("]")) ))) {
var usernamefield = message.content.substring(getPosition(message.content, "**", 1)+3, getPosition(message.content, "**", 2)-1);
message.content = message.content.substring(usernamefield.length+7);
message.author.username = usernamefield.split("#")[0];
message.author.discriminator = usernamefield.split("#")[1];
}
return message;
}
function parse_message_time(message) {
var mome = moment(message.timestamp);
message.formatted_timestamp = mome.toDate().toString();
message.formatted_time = mome.format("h:mm A");
return message;
}
function parse_message_attachments(message) {
for (var i = 0; i < message.attachments.length; i++) {
var attach = "";
if (message.content.length != 0) {
attach = " ";
}
attach += message.attachments[i].url;
message.content += attach;
}
return message;
}
function handle_last_message_mention() {
var lastmsg = $("#chatcontent p:last-child");
var content = lastmsg.text().toLowerCase();
var username_discrim = $("#currentusername").text().toLowerCase();
if (content.includes("@everyone") || content.includes("@" + username_discrim)) {
lastmsg.css( "color", "#ff5252" );
lastmsg.css( "font-weight", "bold" );
}
}
function escapeHtml(unsafe) { /* http://stackoverflow.com/questions/6234773/can-i-escape-html-special-chars-in-javascript */
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function nl2br (str, is_xhtml) { /* http://stackoverflow.com/questions/2919337/jquery-convert-line-breaks-to-br-nl2br-equivalent/ */
var breakTag = (is_xhtml || typeof is_xhtml === 'undefined') ? '<br />' : '<br>';
return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ breakTag +'$2');
}
function parse_channels_in_message(message) {
var channelids = Object.keys(guild_channels);
for (var i = 0; i < channelids.length; i++) {
var pattern = "<#" + channelids[i] + ">";
message.content = message.content.replace(new RegExp(pattern, "g"), "#" + guild_channels[channelids[i]].channel.name);
}
return message;
}
function fill_discord_messages(messages, jumpscroll) {
if (messages.length == 0) {
return last_message_id;
}
var last = 0;
var template = $('#mustache_usermessage').html();
Mustache.parse(template);
for (var i = messages.length-1; i >= 0; i--) {
var message = messages[i];
message = replace_message_mentions(message);
message = format_bot_message(message);
message = parse_message_time(message);
message = parse_message_attachments(message);
message = parse_channels_in_message(message);
var rendered = Mustache.render(template, {"id": message.id, "full_timestamp": message.formatted_timestamp, "time": message.formatted_time, "username": message.author.username, "discriminator": message.author.discriminator, "content": nl2br(escapeHtml(message.content))});
$("#chatcontent").append(rendered);
last = message.id;
handle_last_message_mention();
}
$("html, body").animate({ scrollTop: $(document).height() }, "slow");
$('#chatcontent').linkify({
target: "_blank"
});
return last;
}
function run_fetch_routine() {
if (currently_fetching) {
return;
}
currently_fetching = true;
times_fetched += 1;
var channel_id = selected_channel;
var fet;
var jumpscroll;
$("#fetching-indicator").fadeIn(800);
if (last_message_id == null) {
$("#chatcontent").empty();
fet = fetch(channel_id);
jumpscroll = true;
} else {
fet = fetch(channel_id, last_message_id);
jumpscroll = element_in_view($('#discordmessage_'+last_message_id), true);
}
fet.done(function(data) {
var status = data.status;
update_embed_userchip(status.authenticated, status.avatar, status.username, status.user_id, status.discriminator);
last_message_id = fill_discord_messages(data.messages, jumpscroll);
if (status.manage_embed) {
$("#administrate_link").show();
} else {
$("#administrate_link").hide();
}
if (times_fetched % 10 == 0) {
var guild = query_guild();
guild.done(function(guildobj) {
fill_channels(guildobj.channels);
fill_discord_members(guildobj.discordmembers);
fill_authenticated_users(guildobj.embedmembers.authenticated);
fill_unauthenticated_users(guildobj.embedmembers.unauthenticated);
fetchtimeout = setTimeout(run_fetch_routine, 5000);
});
} else {
fetchtimeout = setTimeout(run_fetch_routine, 5000);
}
});
fet.fail(function(data) {
if (data.status == 403) {
$('#loginmodal').modal('open');
Materialize.toast('Authentication error! You have been disconnected by the server.', 10000);
} else if (data.status == 401) {
$('#loginmodal').modal('open');
Materialize.toast('Session expired! You have been logged out.', 10000);
}
});
fet.catch(function(data) {
if (500 <= data.status && data.status < 600) {
if (fetch_error_count % 5 == 0) {
Materialize.toast('Fetching messages error! EndenDragon probably broke something. Sorry =(', 10000);
}
fetch_error_count += 1;
fetchtimeout = setTimeout(run_fetch_routine, 10000);
}
});
fet.always(function() {
currently_fetching = false;
$("#fetching-indicator").fadeOut(800);
});
}
function update_embed_userchip(authenticated, avatar, username, userid, discrim=null) {
if (authenticated) {
$("#currentuserimage").show();
$("#currentuserimage").attr("src", avatar);
$("#currentusername").text(username + "#" + discrim);
} else {
$("#currentuserimage").hide();
$("#currentusername").text(username + "#" + userid);
}
}
$("#discordlogin_btn").click(function() {
lock_login_fields();
wait_for_discord_login();
});
$("#custom_username_field").keyup(function(event){
if (event.keyCode == 13) {
if (!(new RegExp(/^[a-z\d\-_\s]+$/i).test($(this).val()))) {
Materialize.toast('Illegal username provided! Only alphanumeric, spaces, dashes, and underscores allowed in usernames.', 10000);
return;
}
if($(this).val().length >= 2 && $(this).val().length <= 32) {
lock_login_fields();
var usr = create_unauthenticated_user($(this).val());
usr.done(function(data) {
initialize_embed();
});
usr.fail(function(data) {
if (data.status == 429) {
Materialize.toast('Sorry! You are allowed to log in as a guest once every 15 minutes.', 10000);
} else if (data.status == 403) {
Materialize.toast('Authentication error! You have been banned.', 10000);
} else if (data.status == 406) {
Materialize.toast('Illegal username provided! Only alphanumeric, spaces, dashes, and underscores allowed in usernames.', 10000);
}
unlock_login_fields();
});
}
}
});
$("#messagebox").keyup(function(event){
if ($(this).val().length == 1) {
$(this).val($.trim($(this).val()));
}
if(event.keyCode == 13 && $(this).val().length >= 1 && $(this).val().length <= 350) {
$(this).val($.trim($(this).val()));
$(this).blur();
$("#messagebox").attr('readonly', true);
var funct = post(selected_channel, $(this).val());
funct.done(function(data) {
$("#messagebox").val("");
clearTimeout(fetchtimeout);
run_fetch_routine();
});
funct.fail(function(data) {
Materialize.toast('Failed to send message.', 10000);
});
funct.catch(function(data) {
if (data.status == 429) {
Materialize.toast('You are sending messages too fast! 1 message per 10 seconds', 10000);
}
});
funct.always(function() {
$("#messagebox").attr('readonly', false);
});
}
});
$('#guild-btn').sideNav({
menuWidth: 300, // Default is 300
edge: 'left', // Choose the horizontal origin
closeOnClick: true, // Closes side-nav on <a> clicks, useful for Angular/Meteor
draggable: true // Choose whether you can drag to open on touch screens
}
);
$('#members-btn').sideNav({
menuWidth: 300, // Default is 300
edge: 'right', // Choose the horizontal origin
draggable: true // Choose whether you can drag to open on touch screens
}
);
})();

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,127 @@
{% extends 'site_layout.html.j2' %}
{% block title %}Administrate Guild: {{ guild['name'] }}{% endblock %}
{% block additional_head_elements %}
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/administrate_guild.css') }}">
{% endblock %}
{% block content %}
<h1>Administrating: {{ guild['name'] }}</h1>
<p class="flow-text">For this server, you are allowed the following actions:
{% for permission in permissions %}
{{ permission }}
{% if not loop.last %}
,
{% endif %}
{% endfor %}
.</p>
<div class="row">
<div class="col s12">
<h2 class="header">Embed URLs</h2>
<div class="card horizontal black-text indigo lighten-5 z-depth-3 hoverable">
<div class="card-stacked">
<div class="card-content">
<p class="flow-text">Direct Link</p>
<input readonly value="{{ url_for("embed.guild_embed", guild_id=guild['id'], _external=True) }}" id="disabled" type="text" onClick="this.setSelectionRange(0, this.value.length)">
<p class="flow-text">iFrame Embed</p>
<input readonly value="&lt;iframe src=&quot;{{ url_for("embed.guild_embed", guild_id=guild['id'], _external=True) }}&quot; height=&quot;600&quot; width=&quot;800&quot; /&gt;" id="disabled" type="text" onClick="this.setSelectionRange(0, this.value.length)">
</div>
</div>
</div>
</div>
{% if "Manage Embed Settings" in permissions %}
<div class="col s12">
<h2 class="header">Embed Settings</h2>
<div class="card horizontal black-text indigo lighten-5 z-depth-3 hoverable">
<div class="card-stacked">
<div class="card-content">
<p class="flow-text">Unauthenticated (Guest) Users</p>
<div class="switch">
<label>
Disable
<input type="checkbox" id="unauth_users" name="unauth_users" {% if dbguild['unauth_users'] %}checked{% endif %} >
<span class="lever"></span>
Enable
</label>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if "Ban Members" in permissions or "Kick Members" in permissions %}
<div class="col s12">
<h2 class="header">Moderate Unauthenticated Members</h2>
<div class="card horizontal black-text indigo lighten-5 z-depth-3 hoverable">
<div class="card-stacked" style="overflow-x: hidden;">
<div class="card-content">
<div class="row">
<div class="col s12">
<p class="flow-text">Select Action</p>
<table class="striped responsive-table">
<thead>
<tr>
<th>Kick User</th>
<th>Ban User</th>
<th>Username</th>
<th>Discrim</th>
<th>Last Visit</th>
<th>IP Address Hash</th>
<th>Banned Timestamp</th>
<th>Banned by</th>
<th>Banned Reason</th>
<th>Ban Lifted by</th>
<th>Recent Aliases</th>
</tr>
</thead>
<tbody>
{% for member in members %}
<tr>
<td><a class="waves-effect waves-light btn orange" {% if "Kick Members" not in permissions or member["kicked"] %}disabled{% endif %} onclick='revoke_user( "{{ guild['id'] }}" , {{ member['id'] }} )' >Kick</a></td>
{% if not member["banned"] %}
<td><a class="waves-effect waves-light btn red" {% if "Ban Members" not in permissions %}disabled{% endif %} {% if "Ban Members" in permissions %} onclick='initiate_ban( "{{ guild['id'] }}" , {{ member['id'] }} )' {% endif %} >Ban</a></td>
{% else %}
<td><a class="waves-effect waves-light btn red lighten-2" {% if "Ban Members" not in permissions %}disabled{% endif %} {% if "Ban Members" in permissions %} onclick='remove_ban( "{{ guild['id'] }}" , {{ member['id'] }} )' {% endif %} >Lift</a></td>
{% endif %}
<td>{{ member['username'] }}</td>
<td>{{ member['discrim'] }}</td>
<td>{{ member['last_visit'] }}</td>
<td>{{ member['ip'] }}</td>
<td>{{ member['banned_timestamp'] }}</td>
<td>{{ member['banned_by'] }}</td>
<td>{{ member['banned_reason'] }}</td>
<td>{{ member['ban_lifted_by'] }}</td>
<td>
<ul>
{% if member['aliases']|length > 0 %}
{% for alias in member['aliases'] %}
<li>{{ alias }}</li>
{% endfor %}
{% else %}
<li>None</li>
{% endif %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>Note that all bans are by IP. Seeing duplicates? It is because users are generated a unique session on each browser load. (Though we try to remove/concat any duplicates IP hashes)</p>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block script %}
<script type="text/javascript" src="{{ url_for('static', filename='js/administrate_guild.js') }}"></script>
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends 'site_layout.html.j2' %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1>User Dashboard</h1>
<p class="flow-text">Select a server to configure Titan Embeds.</p>
<p>*List missing some servers? It's because you must have either <strong>Manage Server</strong>, <strong>Kick Members</strong>, or <strong>Ban Members</strong> permissions to modify embed settings.</p>
<div class="row">
{% for server in servers %}
<div class="col l4 m6 s12">
<div class="card-panel indigo lighten-5 z-depth-3 hoverable">
<div class="row valign-wrapper">
<div class="col s3">
{% if server.icon %}
<img src="{{ icon_generate(server.id, server.icon) }}" alt="" class="circle responsive-img">
{% else %}
<span class="black-text">No icon :(</span>
{% endif %}
</div>
<div class="col s7">
<span class="black-text">
<p class="flow-text truncate">{{ server.name }}</p>
<br>
<a class="waves-effect waves-light btn" href="{{url_for('user.administrate_guild', guild_id=server['id'])}}">Modify</a>
</span>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,151 @@
<!DOCTYPE html>
<html>
<head>
<!--Import Google Icon Font-->
<link href="//fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!--Import materialize.css-->
<link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/css/materialize.min.css" integrity="sha256-6DQKO56c9MZL0LAc7QNtxqJyqSa3rS9Gq5FVcIhtA+w=" crossorigin="anonymous" media="screen,projection"/>
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/embedstyle.css') }}">
<!--Let browser know website is optimized for mobile-->
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{{ guild['name'] }} - Embed - Titan Embeds for Discord</title>
{% include 'google_analytics.html.j2' %}
</head>
<body>
<div class="navbar-fixed">
<nav>
<div class="nav-wrapper">
<a href="#" data-activates="guild-nav" class="button-collapse" id="guild-btn"><i class="material-icons">menu</i></a>
<div class="container">
<a href="{{ url_for("index") }}" target="_blank" class="brand-logo"><b>Titan</b>Embeds <span class="betatag">BETA</span></a>
</div>
<a href="#" data-activates="members-nav" class="button-collapse right" id="members-btn"><i class="material-icons">person</i></a>
</div>
</nav>
</div>
<main>
<div id="chatcontent" class="chatcontent"></div>
</main>
<ul id="guild-nav" class="side-nav">
<li>
<div class="userView">
{% if guild['icon'] %}
<img class="circle" src="{{ generate_guild_icon( guild['id'], guild['icon'] ) }}">
{% endif %}
<span class="name">{{ guild['name'] }}</span>
</div>
</li>
<li><a class="subheader">Actions</a></li>
<li><a href="{{ url_for("user.administrate_guild", guild_id=guild['id']) }}" class="waves-effect" target="_blank" id="administrate_link" style="display: none;">Manage Guild Embed</a></li>
<li><a href="https://discordapp.com/channels/{{ guild['id'] }}/" class="waves-effect" target="_blank">Open Server on Discordapp</a></li>
<li><div class="divider"></div></li>
<li><a class="subheader">Channel Topic</a></li>
<div id="channeltopic"></div>
<li><div class="divider"></div></li>
<li><a class="subheader">Channels</a></li>
<span id="channels-list"></span>
</ul>
<ul id="members-nav" class="side-nav">
<li><a class="subheader">Online Server Members - <span id="discord-members-count"></span></a></li>
<span id="discord-members"></span>
<li><div class="divider"></div></li>
<li><a class="subheader">Authenticated Embed Users - <span id="embed-discord-members-count"></span></a></li>
<span id="embed-discord-members"></span>
<li><a class="subheader">Guest Embed Users - <span id="guest-members-count"></span></a></li>
<span id="embed-unauth-users"></span>
</ul>
<div id="loginmodal" class="modal">
<div class="modal-content">
<h4>{{ login_greeting }}</h4>
<p class="flow-text">Please choose one of the following methods to authenticate!</p>
<div class="progress" id="loginProgress" style="display: none;">
<div class="indeterminate"></div>
</div>
<div class="row">
<div class="col s12 m4">
<a id="discordlogin_btn" href="{{ url_for("embed.login_discord", _external=True) }}" class="waves-effect waves-light btn-large" target="_blank">Discord Login</a>
<p>*You will be invited into this server.</p>
</div>
{% if unauth_enabled %}
<div class="col s12 m8">
<p>Of course, you also have the option to login by picking a temporary username for your current browsing session.</p>
<input id="custom_username_field" type="text" {% if session.unauthenticated and session.username %}value="{{ session['username'] }}"{% endif %}>
<label class="active" for="custom_username_field">Username (Hit ENTER/RETURN key to confirm)</label>
</div>
{% endif %}
</div>
</div>
</div>
<footer id="footer" class="footer">
<div id="fetching-indicator" class="preloader-wrapper small active" style="display: none;">
<div class="spinner-layer spinner-blue-only">
<div class="circle-clipper left">
<div class="circle"></div>
</div><div class="gap-patch">
<div class="circle"></div>
</div><div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
</div>
<div id="footercontainer" class="footercontainer">
<div class="currentuserchip left" id="nameplate">
<div class="left"><img id="currentuserimage" src="" class="circle left currentuserimage" style="display: none;"></div>
<div id="currentusername" class="currentusername left">Titan#0001</div>
</div>
<div id="messageboxouter" class="input-field inline"><textarea placeholder="Enter message" id="messagebox" type="text" class="materialize-textarea" rows="1"></textarea></div>
</div>
</footer>
<!--Import jQuery before materialize.js-->
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/js/materialize.min.js" integrity="sha256-ToPQhpo/E89yaCd7+V8LUCjobNRkjilRXfho6x3twLU=" crossorigin="anonymous"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js" integrity="sha256-iaqfO5ue0VbSGcEiQn+OeXxnxAMK2+QgHXIDA5bWtGI=" crossorigin="anonymous"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js" integrity="sha256-1hjUhpc44NwiNg8OwMu2QzJXhD8kcj+sJA3aCQZoUjg=" crossorigin="anonymous"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jQuery-linkify/2.1.4/linkify.min.js" integrity="sha256-/qh8j6L0/OTx+7iY8BAeLirxCDBsu3P15Ci5bo7BJaU=" crossorigin="anonymous"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jQuery-linkify/2.1.4/linkify-jquery.min.js" integrity="sha256-BlSfVPlZijMLojgte2AtSget879chk1+8Z8bEH/L4Cs=" crossorigin="anonymous"></script>
{% raw %}
<script id="mustache_channellistings" type="text/template">
<li><a class="waves-effect truncate" id="channel-{{channelid}}"><span class="channel-hash">#</span> {{channelname}}</a></li>
</script>
<script id="mustache_authedusers" type="text/template">
<li><a class="waves-effect truncate" id="discorduser-{{id}}"><img class="circle membercircle" src="{{avatar}}"> <span class="membername">{{username}}</span></a></li>
</script>
<script id="mustache_unauthedusers" type="text/template">
<li><a class="waves-effect truncate"><span class="membername">{{username}}#{{discriminator}}</span></a></li>
</script>
<script id="mustache_usermessage" type="text/template">
<p><span id="discordmessage_{{id}}" title="{{full_timestamp}}" class="chattimestamp">{{time}}</span> <span class="chatusername">{{username}}#{{discriminator}}</span> {{{content}}}</p>
</script>
<script id="mustache_memberrole" type="text/template">
<li><a class="subheader role-title">{{name}}</a></li>
</script>
{% endraw %}
<script>
const guild_id = "{{ guild_id }}";
const bot_client_id = "{{ client_id }}";
</script>
<script type="text/javascript" src="{{ url_for('static', filename='js/embed.js') }}"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
<script>
!function(T,i,t,a,n){T.GoogleAnalyticsObject=t;T[t]||(T[t]=function(){
(T[t].q=T[t].q||[]).push(arguments)});T[t].l=+new Date;a=i.createElement('script');
n=i.scripts[0];a.src='//www.google-analytics.com/analytics.js';
n.parentNode.insertBefore(a,n)}(window,document,'ga');
ga('create', 'UA-97073231-1', 'auto');
ga('send', 'pageview');
</script>

View File

@ -0,0 +1,17 @@
{% extends 'site_layout.html.j2' %}
{% block title %}Index{% endblock %}
{% block content %}
<h1 class="center-align">Embed Discord like a<br><strong>true Titan</strong></h1>
<p class="flow-text center-align">Add <strong>Titan</strong> to your discord server to create your own personalized chat embed!</p>
<a class="waves-effect waves-light btn btn-large center_content" href="{{ url_for('user.dashboard') }}">Start here!</a>
<br /><br />
<div style="display: flex;align-items: center;">
<video preload="true" autoplay loop style="width:100%; border-radius: 10px;">
<source src="{{url_for('static', filename='titanembeds.mp4')}}" type="video/mp4">
<source src="{{url_for('static', filename='titanembeds.webm')}}" type="video/webm; codecs=vp8, vorbis">
<source type="video/ogg; codecs=theora, vorbis" src="{{url_for('static', filename='titanembeds.ogg')}}">
Your browser does not support the video tag.
</video>
</div>
{% endblock %}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>Sign in completed - Titan Embeds</title>
</head>
<body>
<p>Sign in complete! You may now close the window.</p>
<script>
window.close()
</script>
</body>
</html>

View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html>
<head>
<!--Import Google Icon Font-->
<link href="//fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!--Import materialize.css-->
<link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/css/materialize.min.css" integrity="sha256-6DQKO56c9MZL0LAc7QNtxqJyqSa3rS9Gq5FVcIhtA+w=" crossorigin="anonymous" media="screen,projection"/>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!--Let browser know website is optimized for mobile-->
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{% block title %}{% endblock %} - Titan Embeds for Discord</title>
{% block additional_head_elements %}{% endblock %}
{% include 'google_analytics.html.j2' %}
</head>
<body>
<main>
{% if session['unauthenticated'] is defined and not session['unauthenticated'] %}
<ul id="menu_dropdown" class="dropdown-content">
<li><a href="{{ url_for('user.dashboard') }}">Dashboard</a></li>
<li class="divider"></li>
<li><a href="{{ url_for('user.logout') }}">Logout</a></li>
</ul>
{% endif %}
<nav>
<div class="nav-wrapper container">
<a href="/" class="brand-logo"><b>Titan</b>Embeds <span class="betatag">BETA</span></a>
<ul id="nav-mobile" class="right">
<li><a href="{{url_for("embed.guild_embed", guild_id="295085744249110529")}}" class="waves-effect btn z-depth-3">Visit Us!</a></li>
{% if session['unauthenticated'] is defined and not session['unauthenticated'] %}
<li><a id="menu_drop" data-activates="menu_dropdown" class="waves-effect btn z-depth-3 btn-floating dropdown-button avatar_menu" style='background-image: url(" {{ session['avatar'] }} ")'></a></li>
{% else %}
<li><a href="{{ url_for('user.login_authenticated') }}" class="waves-effect btn z-depth-3">Login</a></li>
{% endif %}
</ul>
</div>
</nav>
<div class="container">
{% block content %}{% endblock %}
</div>
</main>
<footer class="page-footer">
<div class="footer-copyright">
<div class="container">
A project by EndenDragon
<a class="grey-text text-lighten-4 right" href="https://github.com/EndenDragon/Titan">GitHub Repo</a>
</div>
</div>
</footer>
<!--Import jQuery before materialize.js-->
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/js/materialize.min.js" integrity="sha256-ToPQhpo/E89yaCd7+V8LUCjobNRkjilRXfho6x3twLU=" crossorigin="anonymous"></script>
{% block script %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,77 @@
from beaker.cache import CacheManager
from beaker.util import parse_cache_config_options
from titanembeds.database import db, Guilds, KeyValueProperties
from flask import request, session
from flask_limiter import Limiter
from config import config
import random
import string
import hashlib
cache_opts = {
'cache.type': 'ext:database',
'cache.lock_dir': 'tmp/cachelock',
'cache.url': config["database-uri"],
'cache.sa.pool_recycle': 250,
}
cache = CacheManager(**parse_cache_config_options(cache_opts))
from titanembeds.discordrest import DiscordREST
discord_api = DiscordREST(config['bot-token'])
def get_client_ipaddr():
if "X-Real-IP" in request.headers: # pythonanywhere specific
ip = request.headers['X-Real-IP']
else: # general
ip = request.remote_addr
return hashlib.sha512(config['app-secret'] + ip).hexdigest()[:15]
def generate_session_key():
sess = session.get("sessionunique", None)
if not sess:
rand_str = lambda n: ''.join([random.choice(string.lowercase) for i in xrange(n)])
session['sessionunique'] = rand_str(25)
sess = session['sessionunique']
return sess #Totally unique
def make_cache_key(*args, **kwargs):
path = request.path
args = str(hash(frozenset(request.args.items())))
ip = get_client_ipaddr()
sess = generate_session_key()
return (path + args + sess + ip).encode('utf-8')
def make_guilds_cache_key():
sess = generate_session_key()
ip = get_client_ipaddr()
return (sess + ip + "user_guilds").encode('utf-8')
def make_guildchannels_cache_key():
guild_id = request.values.get('guild_id', "0")
sess = generate_session_key()
ip = get_client_ipaddr()
return (sess + ip + guild_id + "user_guild_channels").encode('utf-8')
def channel_ratelimit_key(): # Generate a bucket with given channel & unique session key
sess = generate_session_key()
channel_id = request.values.get('channel_id', "0")
return (sess + channel_id).encode('utf-8')
def guild_ratelimit_key():
ip = get_client_ipaddr()
guild_id = request.values.get('guild_id', "0")
return (ip + guild_id).encode('utf-8')
def check_guild_existance(guild_id):
dbGuild = Guilds.query.filter_by(guild_id=guild_id).first()
if not dbGuild:
return False
guild = discord_api.get_guild(guild_id)
return guild['code'] == 200
def guild_query_unauth_users_bool(guild_id):
dbGuild = db.session.query(Guilds).filter(Guilds.guild_id==guild_id).first()
return dbGuild.unauth_users
rate_limiter = Limiter(key_func=get_client_ipaddr) # Default limit by ip address