diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..216fe01 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing to Titan Embeds +There are many ways to contribute to Titan. There is no one right method to contribute. As long as your Pull Request is valid and is beneficial to the project, we'll take it. Whether you are a designated developer of this project, or just a seasonal hacker who want to fix a mistake that we probably made, you're welcomed to help as long as you abide by these guidelines. This document outlines of the practices and mistakes involving with contributing to the project. + +## Development Environment +For those who would like to run the codebase yourself, you may follow the instructions to the webapp and discordbot to setup the components on your own server. You could however also develop the code on Cloud9. It is free* and very simple to get it off running under ten minutes or less. Either way, if you would like to contribute to the project, I strongly advise you to run Titan on a development server. Making changes on a dev server has many benefits. Especially if you are making changes other than wording issues, the GitHub editor is not the way to go. You may follow these steps to run Titan with Cloud9. + +*If you happen to have a credit/debit card, you may skip steps 1-3 (With steps 4-5 links modified) to use the official Cloud9 site. They just use the card as a verification form and will refund your money immediately.* +**Credit to to make this possible** +1. Sign up for this class, its free, and you need it for credentials to cloud9 +2. Once you signed up, go to your email and confirm their verification link +3. Visit their version of cloud9 (same thing as the offical, just more cats) This is where you can use c9 +4. Add your SSH key from this link to github +5. At the top right corner, click on New Workspace (To create one for Titan) +6. Fill in the details, click on Python as the template environment +7. Set the `Clone from Git or Mercurial url` to `git@github.com:TitanEmbeds/Titan.git` This should pull titan to your workspace. +8. Right click `cloud9_install.sh` file at the left sidebar and click run. This will set everything up. +9. Afterwards, just edit the respective config.py files in the webapp/discordbot directories and you are ready to go! +10. Now you're ready to run Titan... webapp! To make the webapp to work, rightclick `run_c9.py` file and click run. Congratz! It will tell you the exact url where your stuff is running at. +11. For discord bot, you can change the directory to the discordbot `cd discordbot/` and run `python3.5 run.py` to start the bot from the bash console! +12. To make the login system work, go back to your discord bot applications page... for the redirect uris, add these: `http://xxx.cs50.io/user/callback` and `http://xxx.cs50.io/user/dashboard`. Replacing the `xxx` with your subdomain url in the webapp. That outta make the login work! (Take note that there is no http**s** in http). +Now that you set everything up, take a step back and learn some ubuntu/bash to get familiar with it. Some things like git commit/push/pull, etc might be helpful. Maybe you can get phpmyadmin and inspect the database yourself, in gui form . + +## Pull Requesting +If you do not have write access to the codebase, please make a fork of the project. Edit your changes on your fork and push it out onto GitHub. Afterwards, you may submit a pull request for your changes to be merged to the master branch (production). If you do however have write access to the repository, please create a branch and propose pull requests for me to merge into the production. + +I have recently decided to restrict pushing into the master branch so that all commits to the codebase is complete and meaningful. The production environment is not used for testing and every new errors in the error log makes me feel a little bit sadder. Using branches and pull requests also means that I may squash and edit the commit messages before pulling into the master so they may look more nicer. +To create a new branch, run this command `git checkout -b `. Then use `git checkout ` to switch between branches. + +Make sure that you thoughly test your changes so that it works and doesn't introduce new bugs. *I won't merge any pull requests until your changes are complete.* I do not like to accept features that are "half done" as these may be left abandoned at any time and may look odd. Please keep in mind to create one branch/pull request for every new feature. + +Although I try to be as lenient as possible, please follow the best coding and git practices. If you need help, please join the Discord server and talk to me - EndenDragon. I don't bite. I am more than welcomed to help you if you're stuck during the process of contribution. Sorry if the guidelines above are a bit scary, I just wanted to establish some common ground. Happy hacking, and thank you for making Titan better for everyone! diff --git a/README.md b/README.md index 8b2bdf2..bcf10f5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # Titan +**Visit our website! [https://titanembeds.com/](https://titanembeds.com/) And get started *right away*!** + There was a time when Discord doesn't support embedding the chat on a webpage. But with Titan, you can! It is as simple as 1, 2, 3! 1. Invite the bot to your server (You must have "Manage Server" permissions) 2. Configure the embed to your liking (toggling guest users, etc) @@ -12,11 +14,11 @@ There was a time when Discord doesn't support embedding the chat on a webpage. B # Installation Would you like to run your own copy of Titan Embeds? There are two parts that integrate nicely together. The webapp (website) handles the frontend and communication with the database to retrieve server messages, etc. The discordbot (bot) handles the communcation -between Discord's websockets and pushing out the data to the database for the webapp. Check out the respective folder for their installation (pay attention to the python versions!) instructions. +between Discord's websockets and pushing out the data to the database for the webapp. Check out the respective folder for their installation instructions. # Database installation To set up the database for it to work with the webapp and the discordbot, one must use **alembic** to *migrate* their databases to the current database state. To do so, please follow these instructions. -1. Install alembic with **Python 2.7's pip** `pip install alembic` +1. Install alembic with **Python 3.5's pip** `pip install alembic` 2. Change your directory to the webapp where the alembic files are located `cd webapp` 3. Clone `alembic.example.ini` into your own `alembic.ini` file to find and edit the following line `sqlalchemy.url` to equal your database uri. [See here](http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls) if you need help understanding how database uri works in SQLalchemy. 4. In your terminal, run `alembic upgrade head` to upgrade your database tables to the current version on git. As long as there are only *INFO* messages and no errors, you should be fine. diff --git a/cloud9_install.sh b/cloud9_install.sh index 2eab9d7..97919d6 100644 --- a/cloud9_install.sh +++ b/cloud9_install.sh @@ -1,25 +1,22 @@ #!/usr/bin/env bash -echo "[C9Setup] Installing mysql, and creating titan db table" +echo "[C9Setup] Installing mysql, redis, and creating titan db table" cd ~/workspace/ mysql-ctl start mysql -u root -e "CREATE DATABASE titan;" +sudo service redis-server start echo "[C9Setup] Copying config.py for webapp/discordbot and alembic.ini" cp ~/workspace/webapp/config.example.py ~/workspace/webapp/config.py cp ~/workspace/discordbot/config.example.py ~/workspace/discordbot/config.py cp ~/workspace/webapp/alembic.example.ini ~/workspace/webapp/alembic.ini -echo "[C9Setup] Installing dependancies for discordbot" -cd ~/workspace/discordbot/ +echo "[C9Setup] Installing Titan dependencies" +cd ~/workspace/ sudo python3.5 -m pip install -r requirements.txt -sudo python3.5 -m pip install pymysql - -echo "[C9Setup] Installing webapp dependancies" -cd ~/workspace/webapp -sudo pip install -r requirements.txt -sudo pip install alembic pymysql +sudo python3.5 -m pip install alembic pymysql eventlet uwsgi echo "[C9Setup] Auto populating alembic.ini database url and titan database table" +cd ~/workspace/webapp #sqlalchemy.url = mysql+pymysql://root@localhost/titan sed -i '32s/.*/sqlalchemy.url = mysql+pymysql:\/\/root@localhost\/titan/' ~/workspace/webapp/alembic.ini alembic upgrade head @@ -33,7 +30,8 @@ sed -i "11s/.*/\'database-uri\': \"mysql+pymysql:\/\/root@localhost\/titan\",/" #'app-location': "/home/ubuntu/workspace/webapp/", sed -i "8s/.*/\'app-location\': \"\/home\/ubuntu\/workspace\/webapp\/\",/" ~/workspace/webapp/config.py -echo "[C9Setup] Making sure everything can be runned" +echo "[C9Setup] Making sure everything can be ran" +cd ~/workspace/ sudo chmod -R 777 * echo "------------------------------" @@ -44,4 +42,4 @@ echo "" echo "After you finished editing those files, you may right click on run_c9.py and click run in the menu to start the webapp." echo "To run the discordbot, change your directory to discord bot: cd discordbot/" echo "and type the following command: python3.5 run.py" -echo "------------------------------" \ No newline at end of file +echo "------------------------------" diff --git a/discordbot/cleanup_db_messages.py b/discordbot/cleanup_db_messages.py new file mode 100644 index 0000000..bcbaef3 --- /dev/null +++ b/discordbot/cleanup_db_messages.py @@ -0,0 +1,97 @@ +from config import config +from titanembeds.database import DatabaseInterface, Guilds, Messages +from titanembeds.commands import Commands +import discord +import aiohttp +import asyncio +import sys +import logging +import json +from asyncio_extras import threadpool +logging.basicConfig(filename='titanbot.log',level=logging.INFO,format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p') +logging.getLogger('TitanBot') +logging.getLogger('sqlalchemy') + +########################### +# Cleanup DB Messages # +# # +# Cleans the database # +# messages store # +########################### + +class Titan(discord.Client): + def __init__(self): + super().__init__() + self.aiosession = aiohttp.ClientSession(loop=self.loop) + self.http.user_agent += ' TitanEmbeds-Bot' + self.database = DatabaseInterface(self) + self.command = Commands(self, self.database) + + def _cleanup(self): + try: + self.loop.run_until_complete(self.logout()) + except: # Can be ignored + pass + pending = asyncio.Task.all_tasks() + gathered = asyncio.gather(*pending) + try: + gathered.cancel() + self.loop.run_until_complete(gathered) + gathered.exception() + except: # Can be ignored + pass + + def run(self): + try: + self.loop.run_until_complete(self.start(config["bot-token"])) + except discord.errors.LoginFailure: + print("Invalid bot token in config!") + finally: + try: + self._cleanup() + except Exception as e: + print("Error in cleanup:", e) + self.loop.close() + + async def on_ready(self): + print('Titan [DiscordBot] [UTILITY: Cleanup database messages]') + print('Logged in as the following user:') + print(self.user.name) + print(self.user.id) + print('------') + + try: + await self.database.connect(config["database-uri"] + "?charset=utf8mb4") + except Exception: + self.logger.error("Unable to connect to specified database!") + traceback.print_exc() + await self.logout() + return + + print("working on this...") + async with threadpool(): + with self.database.get_session() as session: + guilds = session.query(Guilds).all() + for guild in guilds: + print("id-{} snowflake-{} name-{}".format(guild.id, guild.guild_id, guild.name)) + try: + channelsjson = json.loads(guild.channels) + except: + continue + for channel in channelsjson: + chanid = channel["id"] + msgs = session.query(Messages).filter(Messages.channel_id == chanid).order_by(Messages.timestamp.desc()).offset(50).all() + for msg in msgs: + session.delete(msg) + session.commit() + print("done!") + await self.logout() + +def main(): + print("Starting...") + te = Titan() + te.run() + gc.collect() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/discordbot/config.example.py b/discordbot/config.example.py index 7fda5e8..a6304d1 100644 --- a/discordbot/config.example.py +++ b/discordbot/config.example.py @@ -2,4 +2,10 @@ config = { 'bot-token': "Discord bot token", 'database-uri': "driver://username:password@host:port/database", -} \ No newline at end of file + + 'redis-uri': "redis://", + + 'errorreporting-channelid': "", + + 'logging-location': "/home/titan/Titan/discordbot/titanbot.log", +} diff --git a/discordbot/requirements.txt b/discordbot/requirements.txt deleted file mode 100644 index 400bc22..0000000 --- a/discordbot/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -discord.py -sqlalchemy -asyncio_extras diff --git a/discordbot/titanembeds/bot.py b/discordbot/titanembeds/bot.py index 238842c..df63af1 100644 --- a/discordbot/titanembeds/bot.py +++ b/discordbot/titanembeds/bot.py @@ -1,22 +1,29 @@ from config import config from titanembeds.database import DatabaseInterface from titanembeds.commands import Commands +from titanembeds.socketio import SocketIOInterface import discord import aiohttp import asyncio import sys import logging +import json logging.basicConfig(filename='titanbot.log',level=logging.INFO,format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p') +handler = logging.FileHandler(config.get("logging-location", "titanbot.log")) logging.getLogger('TitanBot') logging.getLogger('sqlalchemy') class Titan(discord.Client): def __init__(self): - super().__init__() + super().__init__(max_messages=20000) self.aiosession = aiohttp.ClientSession(loop=self.loop) self.http.user_agent += ' TitanEmbeds-Bot' self.database = DatabaseInterface(self) self.command = Commands(self, self.database) + self.socketio = SocketIOInterface(self, config["redis-uri"]) + + self.database_connected = False + self.loop.create_task(self.send_webserver_heartbeat()) def _cleanup(self): try: @@ -31,6 +38,29 @@ class Titan(discord.Client): gathered.exception() except: # Can be ignored pass + + async def wait_until_dbonline(self): + while not self.database_connected: + await asyncio.sleep(1) # Wait until db is connected + + async def send_webserver_heartbeat(self): + await self.wait_until_ready() + await self.wait_until_dbonline() + last_db_conn_status = False + while not self.is_closed: + try: + await self.database.send_webserver_heartbeat() + self.database_connected = True + except: + self.database_connected = False + if last_db_conn_status != self.database_connected and config.get("errorreporting-channelid"): + error_channel = self.get_channel(config["errorreporting-channelid"]) + if self.database_connected: + await self.send_message(error_channel, "Titan has obtained connection to the database!") + else: + await self.send_message(error_channel, "Titan has lost connection to the database! Don't panic!! We'll sort this out... hopefully soon.") + last_db_conn_status = self.database_connected + await asyncio.sleep(60) def run(self): try: @@ -52,11 +82,12 @@ class Titan(discord.Client): print('------') await self.change_presence( - game=discord.Game(name="Embed your Discord server! Visit https://TitanEmbeds.tk/"), status=discord.Status.online + game=discord.Game(name="Embed your Discord server! Visit https://TitanEmbeds.com/"), status=discord.Status.online ) try: - await self.database.connect(config["database-uri"] + "?charset=utf8mb4") + await self.database.connect(config["database-uri"]) + self.database_connected = True except Exception: self.logger.error("Unable to connect to specified database!") traceback.print_exc() @@ -68,7 +99,10 @@ class Titan(discord.Client): await self.database.update_guild(server) if server.large: await self.request_offline_members(server) - server_bans = await self.get_bans(server) + if server.me.server_permissions.ban_members: + server_bans = await self.get_bans(server) + else: + server_bans = [] for member in server.members: banned = member.id in [u.id for u in server_bans] await self.database.update_guild_member( @@ -83,7 +117,9 @@ class Titan(discord.Client): print("Skipping indexing server due to no-init flag") async def on_message(self, message): + await self.wait_until_dbonline() await self.database.push_message(message) + await self.socketio.on_message(message) msg_arr = message.content.split() # split the message if len(message.content.split()) > 1 and message.server: #making sure there is actually stuff in the message and have arguments and check if it is sent in server (not PM) @@ -95,75 +131,139 @@ class Titan(discord.Client): await getattr(self.command, msg_cmd)(message) #actually run cmd, passing in msg obj async def on_message_edit(self, message_before, message_after): + await self.wait_until_dbonline() await self.database.update_message(message_after) + await self.socketio.on_message_update(message_after) async def on_message_delete(self, message): + await self.wait_until_dbonline() await self.database.delete_message(message) + await self.socketio.on_message_delete(message) async def on_server_join(self, guild): - await asyncio.sleep(1) - if not guild.me.server_permissions.administrator: - await asyncio.sleep(1) - await self.leave_server(guild) - return - + await self.wait_until_dbonline() await self.database.update_guild(guild) for channel in guild.channels: + if not channel.permissions_for(channel.server.me).read_messages: + continue async for message in self.logs_from(channel, limit=50, reverse=True): await self.database.push_message(message) for member in guild.members: await self.database.update_guild_member(member, True, False) - banned = await self.get_bans(guild) - for ban in banned: - await self.database.update_guild_member(ban, False, True) + if guild.me.server_permissions.ban_members: + banned = await self.get_bans(guild) + for ban in banned: + await self.database.update_guild_member(ban, False, True) async def on_server_remove(self, guild): + await self.wait_until_dbonline() await self.database.remove_guild(guild) async def on_server_update(self, guildbefore, guildafter): + await self.wait_until_dbonline() await self.database.update_guild(guildafter) + await self.socketio.on_guild_update(guildafter) async def on_server_role_create(self, role): + await self.wait_until_dbonline() if role.name == self.user.name and role.managed: await asyncio.sleep(2) await self.database.update_guild(role.server) + await self.socketio.on_guild_role_create(role) async def on_server_role_delete(self, role): + await self.wait_until_dbonline() if role.server.me not in role.server.members: return await self.database.update_guild(role.server) + await self.socketio.on_guild_role_delete(role) async def on_server_role_update(self, rolebefore, roleafter): + await self.wait_until_dbonline() await self.database.update_guild(roleafter.server) + await self.socketio.on_guild_role_update(roleafter) async def on_channel_delete(self, channel): + await self.wait_until_dbonline() await self.database.update_guild(channel.server) + await self.socketio.on_channel_delete(channel) async def on_channel_create(self, channel): + await self.wait_until_dbonline() await self.database.update_guild(channel.server) + await self.socketio.on_channel_create(channel) async def on_channel_update(self, channelbefore, channelafter): + await self.wait_until_dbonline() await self.database.update_guild(channelafter.server) + await self.socketio.on_channel_update(channelafter) async def on_member_join(self, member): + await self.wait_until_dbonline() await self.database.update_guild_member(member, active=True, banned=False) + await self.socketio.on_guild_member_add(member) async def on_member_remove(self, member): + await self.wait_until_dbonline() await self.database.update_guild_member(member, active=False, banned=False) + await self.socketio.on_guild_member_remove(member) async def on_member_update(self, memberbefore, memberafter): + await self.wait_until_dbonline() await self.database.update_guild_member(memberafter) + await self.socketio.on_guild_member_update(memberafter) async def on_member_ban(self, member): + await self.wait_until_dbonline() if self.user.id == member.id: return await self.database.update_guild_member(member, active=False, banned=True) async def on_member_unban(self, server, user): + await self.wait_until_dbonline() await self.database.unban_server_user(user, server) async def on_server_emojis_update(self, before, after): + await self.wait_until_dbonline() if len(after) == 0: await self.database.update_guild(before[0].server) + await self.socketio.on_guild_emojis_update(before) else: await self.database.update_guild(after[0].server) + await self.socketio.on_guild_emojis_update(after) + + async def on_webhooks_update(self, server): + await self.wait_until_dbonline() + await self.database.update_guild(server) + + async def on_socket_raw_receive(self, msg): + if type(msg) is not str: + return + msg = json.loads(msg) + if msg["op"] != 0: + return + action = msg["t"] + await asyncio.sleep(1) + if action == "MESSAGE_UPDATE": + if not self.in_messages_cache(msg["d"]["id"]): + channel = self.get_channel(msg["d"]["channel_id"]) + message = await self.get_message(channel, msg["d"]["id"]) + await self.on_message_edit(None, message) + if action == "MESSAGE_DELETE": + if not self.in_messages_cache(msg["d"]["id"]): + await self.process_raw_message_delete(msg["d"]["id"], msg["d"]["channel_id"]) + if action == "MESSAGE_DELETE_BULK": + for msgid in msg["d"]["ids"]: + if not self.in_messages_cache(msgid): + await self.process_raw_message_delete(msgid, msg["d"]["channel_id"]) + + async def process_raw_message_delete(self, msg_id, channel_id): + channel = self.get_channel(channel_id) + msg = discord.Message(channel=channel, reactions=[], id=msg_id, type=0, timestamp="2017-01-15T02:59:58", content="What fun is there in making sense?") # Procreate a fake message object + await self.on_message_delete(msg) + + def in_messages_cache(self, msg_id): + for msg in self.messages: + if msg.id == msg_id: + return True + return False diff --git a/discordbot/titanembeds/database/__init__.py b/discordbot/titanembeds/database/__init__.py index cd53bcd..082c12d 100644 --- a/discordbot/titanembeds/database/__init__.py +++ b/discordbot/titanembeds/database/__init__.py @@ -7,6 +7,7 @@ from sqlalchemy.ext.declarative import declarative_base import json import discord +import time Base = declarative_base() @@ -15,6 +16,9 @@ from titanembeds.database.messages import Messages from titanembeds.database.guild_members import GuildMembers from titanembeds.database.unauthenticated_users import UnauthenticatedUsers from titanembeds.database.unauthenticated_bans import UnauthenticatedBans +from titanembeds.database.keyvalue_properties import KeyValueProperties + +from titanembeds.utils import get_message_author, get_message_mentions, get_webhooks_list, get_emojis_list, get_roles_list, get_channels_list, list_role_ids class DatabaseInterface(object): # Courtesy of https://github.com/SunDwarf/Jokusoramame @@ -56,38 +60,15 @@ class DatabaseInterface(object): message.channel.id, message.id, message.content, - json.dumps(self.get_message_author(message)), + json.dumps(get_message_author(message)), str(message.timestamp), edit_ts, - json.dumps(self.get_message_mentions(message.mentions)), + json.dumps(get_message_mentions(message.mentions)), json.dumps(message.attachments) ) session.add(msg) session.commit() - def get_message_author(self, message): - author = message.author - obj = { - "username": author.name, - "discriminator": author.discriminator, - "bot": author.bot, - "id": author.id, - "avatar": author.avatar - } - return obj - - def get_message_mentions(self, mentions): - ments = [] - for author in mentions: - ments.append({ - "username": author.name, - "discriminator": author.discriminator, - "bot": author.bot, - "id": author.id, - "avatar": author.avatar - }) - return ments - async def update_message(self, message): if message.server: async with threadpool(): @@ -98,10 +79,11 @@ class DatabaseInterface(object): .filter(Messages.message_id == message.id).first() if msg: msg.content = message.content + msg.timestamp = message.timestamp msg.edited_timestamp = message.edited_timestamp - msg.mentions = json.dumps(self.get_message_mentions(message.mentions)) + msg.mentions = json.dumps(get_message_mentions(message.mentions)) msg.attachments = json.dumps(message.attachments) - msg.author = json.dumps(self.get_message_author(message)) + msg.author = json.dumps(get_message_author(message)) session.commit() async def delete_message(self, message): @@ -117,6 +99,10 @@ class DatabaseInterface(object): session.commit() async def update_guild(self, guild): + if guild.me.server_permissions.manage_webhooks: + server_webhooks = await self.bot.get_server_webhooks(guild) + else: + server_webhooks = [] async with threadpool(): with self.get_session() as session: gui = session.query(Guilds).filter(Guilds.guild_id == guild.id).first() @@ -124,77 +110,23 @@ class DatabaseInterface(object): gui = Guilds( guild.id, guild.name, - json.dumps(self.get_roles_list(guild.roles)), - json.dumps(self.get_channels_list(guild.channels)), - json.dumps(self.get_emojis_list(guild.emojis)), + json.dumps(get_roles_list(guild.roles)), + json.dumps(get_channels_list(guild.channels)), + json.dumps(get_webhooks_list(server_webhooks)), + json.dumps(get_emojis_list(guild.emojis)), guild.owner_id, guild.icon ) session.add(gui) else: gui.name = guild.name - gui.roles = json.dumps(self.get_roles_list(guild.roles)) - gui.channels = json.dumps(self.get_channels_list(guild.channels)) - gui.emojis = json.dumps(self.get_emojis_list(guild.emojis)) + gui.roles = json.dumps(get_roles_list(guild.roles)) + gui.channels = json.dumps(get_channels_list(guild.channels)) + gui.webhooks = json.dumps(get_webhooks_list(server_webhooks)) + gui.emojis = json.dumps(get_emojis_list(guild.emojis)) gui.owner_id = guild.owner_id gui.icon = guild.icon session.commit() - - def get_emojis_list(self, guildemojis): - emojis = [] - for emote in guildemojis: - emojis.append({ - "id": emote.id, - "name": emote.name, - "require_colons": emote.require_colons, - "managed": emote.managed, - "roles": self.list_role_ids(emote.roles), - "url": emote.url - }) - return emojis - - def get_roles_list(self, guildroles): - roles = [] - for role in guildroles: - roles.append({ - "id": role.id, - "name": role.name, - "color": role.color.value, - "hoist": role.hoist, - "position": role.position, - "permissions": role.permissions.value - }) - return roles - - def get_channels_list(self, guildchannels): - channels = [] - for channel in guildchannels: - if str(channel.type) == "text": - overwrites = [] - for target, overwrite in channel.overwrites: - if isinstance(target, discord.Role): - type = "role" - else: - type = "member" - allow, deny = overwrite.pair() - allow = allow.value - deny = deny.value - overwrites.append({ - "id": target.id, - "type": type, - "allow": allow, - "deny": deny, - }) - - channels.append({ - "id": channel.id, - "name": channel.name, - "topic": channel.topic, - "position": channel.position, - "type": str(channel.type), - "permission_overwrites": overwrites - }) - return channels async def remove_unused_guilds(self, guilds): async with threadpool(): @@ -239,7 +171,7 @@ class DatabaseInterface(object): member.avatar, active, banned, - json.dumps(self.list_role_ids(member.roles)) + json.dumps(list_role_ids(member.roles)) ) session.add(dbmember) else: @@ -247,9 +179,9 @@ class DatabaseInterface(object): dbmember.active = active dbmember.username = member.name dbmember.discriminator = member.discriminator - dbmember.nick = member.nick + dbmember.nickname = member.nick dbmember.avatar = member.avatar - dbmember.roles = json.dumps(self.list_role_ids(member.roles)) + dbmember.roles = json.dumps(list_role_ids(member.roles)) session.commit() async def unban_server_user(self, user, server): @@ -277,12 +209,6 @@ class DatabaseInterface(object): if changed: session.commit() - def list_role_ids(self, usr_roles): - ids = [] - for role in usr_roles: - ids.append(role.id) - return ids - async def flag_unactive_bans(self, guild_id, guildbans): async with threadpool(): with self.get_session() as session: @@ -363,3 +289,15 @@ class DatabaseInterface(object): dbuser.revoked = True session.commit() return "Successfully kicked **{}#{}**!".format(dbuser.username, dbuser.discriminator) + + async def send_webserver_heartbeat(self): + async with threadpool(): + with self.get_session() as session: + key = "bot_heartbeat" + q = session.query(KeyValueProperties).filter(KeyValueProperties.key == key) + if q.count() == 0: + session.add(KeyValueProperties(key=key, value=time.time())) + else: + firstobj = q.first() + firstobj.value = time.time() + session.commit() diff --git a/discordbot/titanembeds/database/guilds.py b/discordbot/titanembeds/database/guilds.py index a73ccb0..233cd0b 100644 --- a/discordbot/titanembeds/database/guilds.py +++ b/discordbot/titanembeds/database/guilds.py @@ -6,25 +6,31 @@ class Guilds(Base): guild_id = db.Column(db.String(255)) # Discord guild id name = db.Column(db.String(255)) # Name unauth_users = db.Column(db.Boolean()) # If allowed unauth users + visitor_view = db.Column(db.Boolean()) # If users are automatically "signed in" and can view chat + webhook_messages = db.Column(db.Boolean()) # Use webhooks to send messages instead of the bot chat_links = db.Column(db.Boolean()) # If users can post links bracket_links = db.Column(db.Boolean()) # If appending brackets to links to prevent embed mentions_limit = db.Column(db.Integer) # If there is a limit on the number of mentions in a msg - roles = db.Column(db.Text()) # Guild Roles - channels = db.Column(db.Text()) # Guild channels - emojis = db.Column(db.Text()) # Guild Emojis + roles = db.Column(db.Text().with_variant(db.Text(length=4294967295), 'mysql')) # Guild Roles + channels = db.Column(db.Text().with_variant(db.Text(length=4294967295), 'mysql'))# Guild channels + webhooks = db.Column(db.Text().with_variant(db.Text(length=4294967295), 'mysql'))# Guild webhooks + emojis = db.Column(db.Text().with_variant(db.Text(length=4294967295), 'mysql')) # Guild Emojis owner_id = db.Column(db.String(255)) # Snowflake of the owner icon = db.Column(db.String(255)) # The icon string, null if none discordio = db.Column(db.String(255)) # Custom Discord.io Invite Link - def __init__(self, guild_id, name, roles, channels, emojis, owner_id, icon): + def __init__(self, guild_id, name, roles, channels, webhooks, emojis, owner_id, icon): self.guild_id = guild_id self.name = name self.unauth_users = True # defaults to true + self.visitor_view = False + self.webhook_messages = False self.chat_links = True self.bracket_links = True self.mentions_limit = -1 # -1 = unlimited mentions self.roles = roles self.channels = channels + self.webhooks = webhooks self.emojis = emojis self.owner_id = owner_id self.icon = icon diff --git a/discordbot/titanembeds/database/keyvalue_properties.py b/discordbot/titanembeds/database/keyvalue_properties.py new file mode 100644 index 0000000..32ec8d7 --- /dev/null +++ b/discordbot/titanembeds/database/keyvalue_properties.py @@ -0,0 +1,17 @@ +from titanembeds.database import db, Base +import datetime + +class KeyValueProperties(Base): + __tablename__ = "keyvalue_properties" + id = db.Column(db.Integer, primary_key=True) # Auto incremented id + key = db.Column(db.String(255), nullable=False) # 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 \ No newline at end of file diff --git a/discordbot/titanembeds/socketio/__init__.py b/discordbot/titanembeds/socketio/__init__.py new file mode 100644 index 0000000..89ea000 --- /dev/null +++ b/discordbot/titanembeds/socketio/__init__.py @@ -0,0 +1 @@ +from .socketiointerface import SocketIOInterface \ No newline at end of file diff --git a/discordbot/titanembeds/socketio/socketiointerface.py b/discordbot/titanembeds/socketio/socketiointerface.py new file mode 100644 index 0000000..1dc3ab4 --- /dev/null +++ b/discordbot/titanembeds/socketio/socketiointerface.py @@ -0,0 +1,174 @@ +import socketio +from titanembeds.utils import get_message_author, get_message_mentions, get_roles_list +import time +from email import utils as emailutils + +class SocketIOInterface: + def __init__(self, bot, redis_uri): + self.io = socketio.AsyncRedisManager(redis_uri, write_only=True, channel='flask-socketio') + self.bot = bot + + def format_datetime(self, datetimeobj): + return emailutils.formatdate(time.mktime(datetimeobj.timetuple())) # https://stackoverflow.com/questions/3453177/convert-python-datetime-to-rfc-2822 + + def get_formatted_message(self, message): + edit_ts = message.edited_timestamp + if not edit_ts: + edit_ts = None + else: + edit_ts = self.format_datetime(edit_ts) + msg = { + "id": message.id, + "channel_id": message.channel.id, + "content": message.content, + "author": get_message_author(message), + "timestamp": self.format_datetime(message.timestamp), + "edited_timestamp": edit_ts, + "mentions": get_message_mentions(message.mentions), + "attachments": message.attachments, + } + nickname = None + if hasattr(message.author, 'nick') and message.author.nick: + nickname = message.author.nick + msg["author"]["nickname"] = nickname + for mention in msg["mentions"]: + mention["nickname"] = None + member = message.server.get_member(mention["id"]) + if member: + mention["nickname"] = member.nick + return msg + + async def on_message(self, message): + if message.server: + msg = self.get_formatted_message(message) + await self.io.emit('MESSAGE_CREATE', data=msg, room=str("CHANNEL_"+message.channel.id), namespace='/gateway') + + async def on_message_delete(self, message): + if message.server: + msg = self.get_formatted_message(message) + await self.io.emit('MESSAGE_DELETE', data=msg, room=str("CHANNEL_"+message.channel.id), namespace='/gateway') + + async def on_message_update(self, message): + if message.server: + msg = self.get_formatted_message(message) + await self.io.emit('MESSAGE_UPDATE', data=msg, room=str("CHANNEL_"+message.channel.id), namespace='/gateway') + + def get_formatted_user(self, user): + userobj = { + "avatar": user.avatar, + "avatar_url": user.avatar_url, + "color": str(user.color)[1:], + "discriminator": user.discriminator, + "game": None, + "hoist-role": None, + "id": user.id, + "status": str(user.status), + "username": user.name, + "nick": None, + } + if userobj["color"] == "000000": + userobj["color"] = None + # if userobj["avatar_url"][len(userobj["avatar_url"])-15:] != ".jpg": + # userobj["avatar_url"] = userobj["avatar_url"][:len(userobj["avatar_url"])-14] + ".jpg" + if user.nick: + userobj["nick"] = user.nick + if user.game: + userobj["game"] = { + "name": user.game.name + } + roles = sorted(user.roles, key=lambda k: k.position, reverse=True) + for role in roles: + if role.hoist: + userobj["hoist-role"] = { + "id": role.id, + "name": role.name, + "position": role.position, + } + break + return userobj + + async def on_guild_member_add(self, member): + user = self.get_formatted_user(member) + await self.io.emit('GUILD_MEMBER_ADD', data=user, room=str("GUILD_"+member.server.id), namespace='/gateway') + + async def on_guild_member_remove(self, member): + user = self.get_formatted_user(member) + await self.io.emit('GUILD_MEMBER_REMOVE', data=user, room=str("GUILD_"+member.server.id), namespace='/gateway') + + async def on_guild_member_update(self, member): + user = self.get_formatted_user(member) + await self.io.emit('GUILD_MEMBER_UPDATE', data=user, room=str("GUILD_"+member.server.id), namespace='/gateway') + + def get_formatted_emojis(self, emojis): + emotes = [] + for emo in emojis: + emotes.append({ + "id": emo.id, + "managed": emo.managed, + "name": emo.name, + "require_colons": emo.require_colons, + "roles": get_roles_list(emo.roles), + "url": emo.url, + }) + return emotes + + async def on_guild_emojis_update(self, emojis): + emotes = self.get_formatted_emojis(emojis) + await self.io.emit('GUILD_EMOJIS_UPDATE', data=emotes, room=str("GUILD_"+emojis[0].server.id), namespace='/gateway') + + def get_formatted_guild(self, guild): + guil = { + "id": guild.id, + "name": guild.name, + "icon": guild.icon, + "icon_url": guild.icon_url, + } + return guil + + async def on_guild_update(self, guild): + guildobj = self.get_formatted_guild(guild) + await self.io.emit('GUILD_UPDATE', data=guildobj, room=str("GUILD_"+guild.id), namespace='/gateway') + + def get_formatted_channel(self, channel): + chan = { + "id": channel.id, + "guild_id": channel.server.id, + } + return chan + + async def on_channel_delete(self, channel): + if str(channel.type) != "text": + return + chan = self.get_formatted_channel(channel) + await self.io.emit('CHANNEL_DELETE', data=chan, room=str("GUILD_"+channel.server.id), namespace='/gateway') + + async def on_channel_create(self, channel): + if str(channel.type) != "text": + return + chan = self.get_formatted_channel(channel) + await self.io.emit('CHANNEL_CREATE', data=chan, room=str("GUILD_"+channel.server.id), namespace='/gateway') + + async def on_channel_update(self, channel): + if str(channel.type) != "text": + return + chan = self.get_formatted_channel(channel) + await self.io.emit('CHANNEL_UPDATE', data=chan, room=str("GUILD_"+channel.server.id), namespace='/gateway') + + def get_formatted_role(self, role): + rol = { + "id": role.id, + "guild_id": role.server.id, + } + return rol + + async def on_guild_role_create(self, role): + rol = self.get_formatted_role(role) + await self.io.emit('GUILD_ROLE_CREATE', data=rol, room=str("GUILD_"+role.server.id), namespace='/gateway') + + async def on_guild_role_update(self, role): + rol = self.get_formatted_role(role) + await self.io.emit('GUILD_ROLE_UPDATE', data=rol, room=str("GUILD_"+role.server.id), namespace='/gateway') + + async def on_guild_role_delete(self, role): + rol = self.get_formatted_role(role) + await self.io.emit('GUILD_ROLE_DELETE', data=rol, room=str("GUILD_"+role.server.id), namespace='/gateway') \ No newline at end of file diff --git a/discordbot/titanembeds/utils.py b/discordbot/titanembeds/utils.py new file mode 100644 index 0000000..cee65f2 --- /dev/null +++ b/discordbot/titanembeds/utils.py @@ -0,0 +1,98 @@ +import discord + +def get_message_author(message): + author = message.author + obj = { + "username": author.name, + "discriminator": author.discriminator, + "bot": author.bot, + "id": author.id, + "avatar": author.avatar + } + return obj + +def get_message_mentions(mentions): + ments = [] + for author in mentions: + ments.append({ + "username": author.name, + "discriminator": author.discriminator, + "bot": author.bot, + "id": author.id, + "avatar": author.avatar + }) + return ments + +def get_webhooks_list(guild_webhooks): + webhooks = [] + for webhook in guild_webhooks: + webhooks.append({ + "id": webhook.id, + "guild_id": webhook.server.id, + "channel_id": webhook.channel.id, + "name": webhook.name, + "token": webhook.token, + }) + return webhooks + +def get_emojis_list(guildemojis): + emojis = [] + for emote in guildemojis: + emojis.append({ + "id": emote.id, + "name": emote.name, + "require_colons": emote.require_colons, + "managed": emote.managed, + "roles": list_role_ids(emote.roles), + "url": emote.url + }) + return emojis + +def get_roles_list(guildroles): + roles = [] + for role in guildroles: + roles.append({ + "id": role.id, + "name": role.name, + "color": role.color.value, + "hoist": role.hoist, + "position": role.position, + "permissions": role.permissions.value + }) + return roles + +def get_channels_list(guildchannels): + channels = [] + for channel in guildchannels: + if str(channel.type) == "text": + overwrites = [] + for target, overwrite in channel.overwrites: + if isinstance(target, discord.Role): + type = "role" + else: + type = "member" + allow, deny = overwrite.pair() + allow = allow.value + deny = deny.value + overwrites.append({ + "id": target.id, + "type": type, + "allow": allow, + "deny": deny, + }) + + channels.append({ + "id": channel.id, + "name": channel.name, + "topic": channel.topic, + "position": channel.position, + "type": str(channel.type), + "permission_overwrites": overwrites + }) + return channels + +def list_role_ids(usr_roles): + ids = [] + for role in usr_roles: + ids.append(role.id) + return ids \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..12e75ca --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +flask +flask-sqlalchemy +flask_limiter +requests_oauthlib +Flask-SSLify +flask_socketio +paypalrestsdk +git+https://github.com/TitanEmbeds/discord.py +asyncio_extras +kombu +redis +aioredis diff --git a/webapp/README.md b/webapp/README.md index 8fb833b..8369dbe 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -2,7 +2,7 @@ The webapp portion handles the frontend (it's what the users see). The webapp highly depends on the discordbot to push websockets data to the database. # Installation -1. Clone the repo (make sure you have **Python 2.7** installed on your system. This webapp portion depends on that specific python version) +1. Clone the repo (make sure you have **Python 3.5** installed on your system. This webapp portion depends on that specific python version) 2. Install the pip requirements `pip install -r requirements.txt` 3. Clone `config.example.py` and rename it to `config.py`. Edit the file to your standards 4. Run the development web via `python run.py` -- Though we suggest to use a better server software (look into gunicorn, nginx, uwsgi, etc) \ No newline at end of file diff --git a/webapp/alembic/env.py b/webapp/alembic/env.py index 8060c76..4415561 100644 --- a/webapp/alembic/env.py +++ b/webapp/alembic/env.py @@ -64,7 +64,8 @@ def run_migrations_online(): with connectable.connect() as connection: context.configure( connection=connection, - target_metadata=target_metadata + target_metadata=target_metadata, + compare_type=True ) with context.begin_transaction(): diff --git a/webapp/alembic/versions/2a2f32ac91d6_added_titan_tokens.py b/webapp/alembic/versions/2a2f32ac91d6_added_titan_tokens.py new file mode 100644 index 0000000..cad565c --- /dev/null +++ b/webapp/alembic/versions/2a2f32ac91d6_added_titan_tokens.py @@ -0,0 +1,160 @@ +"""Added Titan Tokens + +Revision ID: 2a2f32ac91d6 +Revises: 6fe130518448 +Create Date: 2017-08-13 22:44:15.996936 + +""" + +# revision identifiers, used by Alembic. +revision = '2a2f32ac91d6' +down_revision = '6fe130518448' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('titan_tokens', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.String(length=255), nullable=False), + sa.Column('tokens', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('token_transactions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.String(length=255), nullable=False), + sa.Column('timestamp', sa.TIMESTAMP(), nullable=False), + sa.Column('action', sa.String(length=255), nullable=False), + sa.Column('net_tokens', sa.Integer(), nullable=False), + sa.Column('start_tokens', sa.Integer(), nullable=False), + sa.Column('end_tokens', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.alter_column(u'cosmetics', 'css', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False) + op.alter_column(u'guild_members', 'active', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guild_members', 'banned', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'0'")) + op.alter_column(u'guilds', 'bracket_links', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guilds', 'channels', + existing_type=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column(u'guilds', 'chat_links', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guilds', 'emojis', + existing_type=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column(u'guilds', 'roles', + existing_type=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column(u'guilds', 'unauth_users', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guilds', 'visitor_view', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False) + op.alter_column(u'guilds', 'webhooks', + existing_type=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column(u'unauthenticated_users', 'revoked', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'0'")) + op.alter_column(u'user_css', 'css', + existing_type=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column(u'user_css', 'css', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=True) + op.alter_column(u'unauthenticated_users', 'revoked', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'0'")) + op.alter_column(u'guilds', 'webhooks', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column(u'guilds', 'visitor_view', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False) + op.alter_column(u'guilds', 'unauth_users', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guilds', 'roles', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column(u'guilds', 'emojis', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column(u'guilds', 'chat_links', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guilds', 'channels', + existing_typesa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column(u'guilds', 'bracket_links', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guild_members', 'banned', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'0'")) + op.alter_column(u'guild_members', 'active', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'cosmetics', 'css', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False) + op.drop_table('token_transactions') + op.drop_table('titan_tokens') + # ### end Alembic commands ### diff --git a/webapp/alembic/versions/40cbd3e0f22d_moved_some_text_columns_to_longtext_type.py b/webapp/alembic/versions/40cbd3e0f22d_moved_some_text_columns_to_longtext_type.py new file mode 100644 index 0000000..6f1559d --- /dev/null +++ b/webapp/alembic/versions/40cbd3e0f22d_moved_some_text_columns_to_longtext_type.py @@ -0,0 +1,142 @@ +"""Moved some text columns to longtext type + +Revision ID: 40cbd3e0f22d +Revises: 9bf2adbc33e8 +Create Date: 2017-07-10 00:01:53.940034 + +""" + +# revision identifiers, used by Alembic. +revision = '40cbd3e0f22d' +down_revision = '9bf2adbc33e8' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('cosmetics', 'css', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False) + op.alter_column('guild_members', 'active', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column('guild_members', 'banned', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'0'")) + op.alter_column('guilds', 'bracket_links', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column('guilds', 'channels', + existing_type=mysql.MEDIUMTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column('guilds', 'chat_links', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column('guilds', 'emojis', + existing_type=mysql.TEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column('guilds', 'roles', + existing_type=mysql.MEDIUMTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column('guilds', 'unauth_users', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column('guilds', 'visitor_view', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False) + op.alter_column('guilds', 'webhooks', + existing_type=mysql.TEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column('unauthenticated_users', 'revoked', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'0'")) + op.alter_column('user_css', 'css', + existing_type=mysql.MEDIUMTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('user_css', 'css', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.MEDIUMTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=True) + op.alter_column('unauthenticated_users', 'revoked', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'0'")) + op.alter_column('guilds', 'webhooks', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.TEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column('guilds', 'visitor_view', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False) + op.alter_column('guilds', 'unauth_users', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column('guilds', 'roles', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.MEDIUMTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column('guilds', 'emojis', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.TEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column('guilds', 'chat_links', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column('guilds', 'channels', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.MEDIUMTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column('guilds', 'bracket_links', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column('guild_members', 'banned', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'0'")) + op.alter_column('guild_members', 'active', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column('cosmetics', 'css', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False) + # ### end Alembic commands ### diff --git a/webapp/alembic/versions/6fe130518448_create_administrators_table.py b/webapp/alembic/versions/6fe130518448_create_administrators_table.py new file mode 100644 index 0000000..a143f12 --- /dev/null +++ b/webapp/alembic/versions/6fe130518448_create_administrators_table.py @@ -0,0 +1,148 @@ +"""Create administrators table + +Revision ID: 6fe130518448 +Revises: 40cbd3e0f22d +Create Date: 2017-07-22 02:00:35.317471 + +""" + +# revision identifiers, used by Alembic. +revision = '6fe130518448' +down_revision = '40cbd3e0f22d' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('administrators', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.alter_column(u'cosmetics', 'css', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False) + op.alter_column(u'guild_members', 'active', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guild_members', 'banned', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'0'")) + op.alter_column(u'guilds', 'bracket_links', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guilds', 'channels', + existing_type=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column(u'guilds', 'chat_links', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guilds', 'emojis', + existing_type=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column(u'guilds', 'roles', + existing_type=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column(u'guilds', 'unauth_users', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guilds', 'visitor_view', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False) + op.alter_column(u'guilds', 'webhooks', + existing_type=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column(u'unauthenticated_users', 'revoked', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text(u"'0'")) + op.alter_column(u'user_css', 'css', + existing_type=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column(u'user_css', 'css', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=True) + op.alter_column(u'unauthenticated_users', 'revoked', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'0'")) + op.alter_column(u'guilds', 'webhooks', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column(u'guilds', 'visitor_view', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False) + op.alter_column(u'guilds', 'unauth_users', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guilds', 'roles', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column(u'guilds', 'emojis', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column(u'guilds', 'chat_links', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guilds', 'channels', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation=u'utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column(u'guilds', 'bracket_links', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'guild_members', 'banned', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'0'")) + op.alter_column(u'guild_members', 'active', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text(u"'1'")) + op.alter_column(u'cosmetics', 'css', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False) + op.drop_table('administrators') + # ### end Alembic commands ### diff --git a/webapp/alembic/versions/9bf2adbc33e8_added_webhooks_column_to_guild_table.py b/webapp/alembic/versions/9bf2adbc33e8_added_webhooks_column_to_guild_table.py new file mode 100644 index 0000000..4cafcdf --- /dev/null +++ b/webapp/alembic/versions/9bf2adbc33e8_added_webhooks_column_to_guild_table.py @@ -0,0 +1,28 @@ +"""Added webhooks column to guild table + +Revision ID: 9bf2adbc33e8 +Revises: b1124468bb2e +Create Date: 2017-06-30 07:24:10.700408 + +""" + +# revision identifiers, used by Alembic. +revision = '9bf2adbc33e8' +down_revision = 'b1124468bb2e' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('guilds', sa.Column('webhooks', sa.Text(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('guilds', 'webhooks') + # ### end Alembic commands ### diff --git a/webapp/alembic/versions/b1124468bb2e_added_visitor_view_column_to_guilds_.py b/webapp/alembic/versions/b1124468bb2e_added_visitor_view_column_to_guilds_.py new file mode 100644 index 0000000..f0f4d69 --- /dev/null +++ b/webapp/alembic/versions/b1124468bb2e_added_visitor_view_column_to_guilds_.py @@ -0,0 +1,28 @@ +"""Added visitor view column to guilds table + +Revision ID: b1124468bb2e +Revises: 95ab6a63135d +Create Date: 2017-06-08 06:31:28.953304 + +""" + +# revision identifiers, used by Alembic. +revision = 'b1124468bb2e' +down_revision = '95ab6a63135d' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('guilds', sa.Column('visitor_view', sa.Boolean(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('guilds', 'visitor_view') + # ### end Alembic commands ### diff --git a/webapp/alembic/versions/dadcb876cdd9_added_webhook_messages_boolean_column_.py b/webapp/alembic/versions/dadcb876cdd9_added_webhook_messages_boolean_column_.py new file mode 100644 index 0000000..c3b4503 --- /dev/null +++ b/webapp/alembic/versions/dadcb876cdd9_added_webhook_messages_boolean_column_.py @@ -0,0 +1,144 @@ +"""Added webhook messages boolean column to guilds + +Revision ID: dadcb876cdd9 +Revises: 2a2f32ac91d6 +Create Date: 2017-08-27 20:01:30.874376 + +""" + +# revision identifiers, used by Alembic. +revision = 'dadcb876cdd9' +down_revision = '2a2f32ac91d6' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('cosmetics', 'css', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False) + op.alter_column('guild_members', 'active', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text("'1'")) + op.alter_column('guild_members', 'banned', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text("'0'")) + op.add_column('guilds', sa.Column('webhook_messages', sa.Boolean(), nullable=False)) + op.alter_column('guilds', 'bracket_links', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text("'1'")) + op.alter_column('guilds', 'channels', + existing_type=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column('guilds', 'chat_links', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text("'1'")) + op.alter_column('guilds', 'emojis', + existing_type=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column('guilds', 'roles', + existing_type=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column('guilds', 'unauth_users', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text("'1'")) + op.alter_column('guilds', 'visitor_view', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False) + op.alter_column('guilds', 'webhooks', + existing_type=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=False) + op.alter_column('unauthenticated_users', 'revoked', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Boolean(), + existing_nullable=False, + existing_server_default=sa.text("'0'")) + op.alter_column('user_css', 'css', + existing_type=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + type_=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('user_css', 'css', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + existing_nullable=True) + op.alter_column('unauthenticated_users', 'revoked', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text("'0'")) + op.alter_column('guilds', 'webhooks', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column('guilds', 'visitor_view', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False) + op.alter_column('guilds', 'unauth_users', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text("'1'")) + op.alter_column('guilds', 'roles', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column('guilds', 'emojis', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column('guilds', 'chat_links', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text("'1'")) + op.alter_column('guilds', 'channels', + existing_type=sa.Text().with_variant(sa.Text(length=4294967295), 'mysql'), + type_=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + existing_nullable=False) + op.alter_column('guilds', 'bracket_links', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text("'1'")) + op.drop_column('guilds', 'webhook_messages') + op.alter_column('guild_members', 'banned', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text("'0'")) + op.alter_column('guild_members', 'active', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False, + existing_server_default=sa.text("'1'")) + op.alter_column('cosmetics', 'css', + existing_type=sa.Boolean(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False) + # ### end Alembic commands ### diff --git a/webapp/config.example.py b/webapp/config.example.py index 786e614..fe8ae22 100644 --- a/webapp/config.example.py +++ b/webapp/config.example.py @@ -4,9 +4,15 @@ config = { 'client-id': "Your app client id", 'client-secret': "Your discord client secret", 'bot-token': "Discord bot token", + + # Rest API in https://developer.paypal.com/developer/applications + 'paypal-client-id': "Paypal client id", + 'paypal-client-secret': "Paypal client secret", 'app-location': "/var/www/Titan/webapp/", 'app-secret': "Type something random here, go wild.", 'database-uri': "driver://username:password@host:port/database", + 'redis-uri': "redis://", + 'websockets-mode': "LITTERALLY None or eventlet or gevent", } diff --git a/webapp/requirements.txt b/webapp/requirements.txt deleted file mode 100644 index 402aba3..0000000 --- a/webapp/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -flask -flask-sqlalchemy -flask_limiter -requests_oauthlib -Flask-SSLify diff --git a/webapp/run.py b/webapp/run.py index 4cb2554..7f2f6f3 100644 --- a/webapp/run.py +++ b/webapp/run.py @@ -1,5 +1,6 @@ #!/usr/bin/env python2 -from titanembeds.app import app +from titanembeds.app import app, socketio +import subprocess def init_debug(): import os @@ -30,6 +31,14 @@ def init_debug(): decoded = None return jsonify(session_cookie=decoded) + @app.route("/github-update", methods=["POST"]) + def github_update(): + try: + subprocess.Popen("git pull", shell=True).wait() + except OSError: + return "ERROR" + + return "OK" if __name__ == "__main__": init_debug() - app.run(host="0.0.0.0",port=3000,debug=True,processes=3) + socketio.run(app, host="0.0.0.0",port=3000,debug=True) diff --git a/webapp/run_c9.py b/webapp/run_c9.py index 68a161e..c934894 100644 --- a/webapp/run_c9.py +++ b/webapp/run_c9.py @@ -1,6 +1,6 @@ -from run import app, init_debug +from run import app, socketio, init_debug import os if __name__ == "__main__": init_debug() - app.run(host=os.getenv('IP', '0.0.0.0'), port=int(os.getenv('PORT', 8080)), debug=True, processes=3) \ No newline at end of file + socketio.run(app, host=os.getenv('IP', '0.0.0.0'), port=int(os.getenv('PORT', 8080)), debug=True) \ No newline at end of file diff --git a/webapp/titanembeds/app.py b/webapp/titanembeds/app.py index 76b9a20..72bb0cc 100644 --- a/webapp/titanembeds/app.py +++ b/webapp/titanembeds/app.py @@ -1,13 +1,23 @@ from config import config -from database import db +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, discord_api -import blueprints.api -import blueprints.user -import blueprints.embed +from titanembeds.utils import rate_limiter, discord_api, bot_alive, socketio +from .blueprints import api, user, admin, embed, gateway import os +from titanembeds.database import get_administrators_list +try: + import uwsgi + from gevent import monkey + monkey.patch_all() +except: + if config.get("websockets-mode", None) == "eventlet": + import eventlet + eventlet.monkey_patch() + elif config.get("websockets-mode", None) == "gevent": + from gevent import monkey + monkey.patch_all() os.chdir(config['app-location']) app = Flask(__name__, static_folder="static") @@ -21,10 +31,13 @@ app.secret_key = config['app-secret'] db.init_app(app) rate_limiter.init_app(app) sslify = SSLify(app, permanent=True) +socketio.init_app(app, message_queue=config["redis-uri"], path='gateway', async_mode=config.get("websockets-mode", None)) -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.register_blueprint(api.api, url_prefix="/api", template_folder="/templates") +app.register_blueprint(admin.admin, url_prefix="/admin", template_folder="/templates") +app.register_blueprint(user.user, url_prefix="/user", template_folder="/templates") +app.register_blueprint(embed.embed, url_prefix="/embed", template_folder="/templates") +socketio.on_namespace(gateway.Gateway('/gateway')) @app.route("/") def index(): @@ -36,5 +49,9 @@ def about(): @app.before_request def before_request(): - db.create_all() discord_api.init_discordrest() + +@app.context_processor +def context_processor(): + bot_status = bot_alive() + return {"bot_status": bot_status, "devs": get_administrators_list()} \ No newline at end of file diff --git a/webapp/titanembeds/blueprints/admin/__init__.py b/webapp/titanembeds/blueprints/admin/__init__.py new file mode 100644 index 0000000..e3741e5 --- /dev/null +++ b/webapp/titanembeds/blueprints/admin/__init__.py @@ -0,0 +1 @@ +from .admin import admin \ No newline at end of file diff --git a/webapp/titanembeds/blueprints/admin/admin.py b/webapp/titanembeds/blueprints/admin/admin.py new file mode 100644 index 0000000..ff28728 --- /dev/null +++ b/webapp/titanembeds/blueprints/admin/admin.py @@ -0,0 +1,227 @@ +from flask import Blueprint, url_for, redirect, session, render_template, abort, request, jsonify +from functools import wraps +from titanembeds.database import db, get_administrators_list, Cosmetics, Guilds, UnauthenticatedUsers, UnauthenticatedBans, TitanTokens, TokenTransactions, get_titan_token, set_titan_token +from titanembeds.oauth import generate_guild_icon_url +import datetime + +admin = Blueprint("admin", __name__) + +def is_admin(f): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + return redirect(url_for("index")) + if session['user_id'] not in get_administrators_list(): + return redirect(url_for("index")) + return f(*args, **kwargs) + return decorated_function + return decorator(f) + +@admin.route("/") +@is_admin +def index(): + return render_template("admin_index.html.j2") + +@admin.route("/cosmetics", methods=["GET"]) +@is_admin +def cosmetics(): + entries = db.session.query(Cosmetics).all() + return render_template("admin_cosmetics.html.j2", cosmetics=entries) + +@admin.route("/cosmetics", methods=["POST"]) +@is_admin +def cosmetics_post(): + user_id = request.form.get("user_id", None) + if not user_id: + abort(400) + css = request.form.get("css", None) + entry = db.session.query(Cosmetics).filter(Cosmetics.user_id == user_id).first() + if entry: + abort(409) + user = Cosmetics(user_id) + if css: + css = css.lower() == "true" + user.css = css + db.session.add(user) + db.session.commit() + return ('', 204) + +@admin.route("/cosmetics", methods=["DELETE"]) +@is_admin +def cosmetics_delete(): + user_id = request.form.get("user_id", None) + if not user_id: + abort(400) + entry = db.session.query(Cosmetics).filter(Cosmetics.user_id == user_id).first() + if not entry: + abort(409) + db.session.delete(entry) + db.session.commit() + return ('', 204) + +@admin.route("/cosmetics", methods=["PATCH"]) +@is_admin +def cosmetics_patch(): + user_id = request.form.get("user_id", None) + if not user_id: + abort(400) + css = request.form.get("css", None) + entry = db.session.query(Cosmetics).filter(Cosmetics.user_id == user_id).first() + if not entry: + abort(409) + if css: + css = css.lower() == "true" + entry.css = css + db.session.commit() + return ('', 204) +def prepare_guild_members_list(members, bans): + all_users = [] + ip_pool = [] + members = sorted(members, key=lambda k: datetime.datetime.strptime(str(k.last_timestamp.replace(tzinfo=None, microsecond=0)), "%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 + +@admin.route("/administrate_guild/", methods=["GET"]) +@is_admin +def administrate_guild(guild_id): + db_guild = db.session.query(Guilds).filter(Guilds.guild_id == guild_id).first() + if not db_guild: + abort(500) + return + session["redirect"] = None + permissions=[] + permissions.append("Manage Embed Settings") + permissions.append("Ban Members") + 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 = { + "id": db_guild.guild_id, + "name": db_guild.name, + "unauth_users": db_guild.unauth_users, + "visitor_view": db_guild.visitor_view, + "webhook_messages": db_guild.webhook_messages, + "chat_links": db_guild.chat_links, + "bracket_links": db_guild.bracket_links, + "mentions_limit": db_guild.mentions_limit, + "icon": db_guild.icon, + "discordio": db_guild.discordio if db_guild.discordio != None else "" + } + return render_template("administrate_guild.html.j2", guild=dbguild_dict, members=users, permissions=permissions) + +@admin.route("/administrate_guild/", methods=["POST"]) +@is_admin +def update_administrate_guild(guild_id): + db_guild = db.session.query(Guilds).filter(Guilds.guild_id == guild_id).first() + db_guild.unauth_users = request.form.get("unauth_users", db_guild.unauth_users) in ["true", True] + db_guild.visitor_view = request.form.get("visitor_view", db_guild.visitor_view) in ["true", True] + db_guild.webhook_messages = request.form.get("webhook_messages", db_guild.webhook_messages) in ["true", True] + db_guild.chat_links = request.form.get("chat_links", db_guild.chat_links) in ["true", True] + db_guild.bracket_links = request.form.get("bracket_links", db_guild.bracket_links) in ["true", True] + db_guild.mentions_limit = request.form.get("mentions_limit", db_guild.mentions_limit) + discordio = request.form.get("discordio", db_guild.discordio) + if discordio and discordio.strip() == "": + discordio = None + db_guild.discordio = discordio + db.session.commit() + return jsonify( + id=db_guild.id, + guild_id=db_guild.guild_id, + unauth_users=db_guild.unauth_users, + visitor_view=db_guild.visitor_view, + webhook_messages=db_guild.webhook_messages, + chat_links=db_guild.chat_links, + bracket_links=db_guild.bracket_links, + mentions_limit=db_guild.mentions_limit, + discordio=db_guild.discordio, + ) + +@admin.route("/guilds") +@is_admin +def guilds(): + guilds = db.session.query(Guilds).all() + return render_template("admin_guilds.html.j2", servers=guilds, icon_generate=generate_guild_icon_url) + +@admin.route("/tokens", methods=["GET"]) +@is_admin +def manage_titan_tokens(): + tokeners = db.session.query(TitanTokens).all() + donators = [] + for usr in tokeners: + row = { + "user_id": usr.user_id, + "tokens": usr.tokens, + "transactions": [] + } + transact = db.session.query(TokenTransactions).filter(TokenTransactions.user_id == usr.user_id).all() + for tr in transact: + row["transactions"].append({ + "id": tr.id, + "user_id": tr.user_id, + "timestamp": tr.timestamp, + "action": tr.action, + "net_tokens": tr.net_tokens, + "start_tokens": tr.start_tokens, + "end_tokens": tr.end_tokens + }) + donators.append(row) + return render_template("admin_token_transactions.html.j2", donators=donators) + +@admin.route("/tokens", methods=["POST"]) +@is_admin +def post_titan_tokens(): + user_id = request.form.get("user_id", None) + amount = request.form.get("amount", None, type=int) + if not user_id or not amount: + abort(400) + if get_titan_token(user_id) != -1: + abort(409) + set_titan_token(user_id, amount, "NEW VIA ADMIN") + return ('', 204) + +@admin.route("/tokens", methods=["PATCH"]) +@is_admin +def patch_titan_tokens(): + user_id = request.form.get("user_id", None) + amount = request.form.get("amount", None, type=int) + if not user_id or not amount: + abort(400) + if get_titan_token(user_id) == -1: + abort(409) + set_titan_token(user_id, amount, "MODIFY VIA ADMIN") + return ('', 204) \ No newline at end of file diff --git a/webapp/titanembeds/blueprints/api/__init__.py b/webapp/titanembeds/blueprints/api/__init__.py index 2b132fc..faf54f8 100644 --- a/webapp/titanembeds/blueprints/api/__init__.py +++ b/webapp/titanembeds/blueprints/api/__init__.py @@ -1 +1 @@ -from api import api +from .api import api diff --git a/webapp/titanembeds/blueprints/api/api.py b/webapp/titanembeds/blueprints/api/api.py index 847a613..9d773e3 100644 --- a/webapp/titanembeds/blueprints/api/api.py +++ b/webapp/titanembeds/blueprints/api/api.py @@ -1,11 +1,12 @@ -from titanembeds.database import db, Guilds, UnauthenticatedUsers, UnauthenticatedBans, AuthenticatedUsers, KeyValueProperties, GuildMembers, Messages, get_channel_messages, list_all_guild_members +from titanembeds.database import db, Guilds, UnauthenticatedUsers, UnauthenticatedBans, AuthenticatedUsers, KeyValueProperties, GuildMembers, Messages, get_channel_messages, list_all_guild_members, get_guild_member, get_administrators_list 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.utils import check_guild_existance, guild_accepts_visitors, 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 titanembeds.userbookkeeping import user_unauthenticated, checkUserRevoke, checkUserBanned, update_user_status, check_user_in_guild, get_guild_channels, guild_webhooks_enabled +from flask import Blueprint, abort, jsonify, session, request, url_for +from flask_socketio import emit from sqlalchemy import and_ import random -import requests import json import datetime import re @@ -13,107 +14,16 @@ 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 - dbUser = GuildMembers.query.filter(GuildMembers.guild_id == guild_id).filter(GuildMembers.user_id == session["user_id"]).first() - revoked = not dbUser or not dbUser.active - 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 - dbUser = GuildMembers.query.filter(GuildMembers.guild_id == guild_id).filter(GuildMembers.user_id == session["user_id"]).first() - if not dbUser: - banned = False - else: - banned = dbUser.banned - 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 dbUser is not None and not checkUserRevoke(guild_id) - def parse_emoji(textToParse, guild_id): - emojis = [] - emojis = re.findall(":(.*?):", textToParse) guild_emojis = get_guild_emojis(guild_id) - newText = textToParse for gemoji in guild_emojis: emoji_name = gemoji["name"] emoji_id = gemoji["id"] - for usremote in emojis: - if usremote == emoji_name: - newText = newText.replace(":{}:".format(emoji_name), "<:{}:{}>".format(emoji_name, emoji_id)) - return newText + textToParse = textToParse.replace(":{}:".format(emoji_name), "<:{}:{}>".format(emoji_name, emoji_id)) + return textToParse -def format_post_content(guild_id, message): +def format_post_content(guild_id, channel_id, message, dbUser): illegal_post = False illegal_reasons = [] message = message.replace("<", "\<") @@ -139,11 +49,16 @@ def format_post_content(guild_id, message): for match in all_mentions: mention = "<@" + match[2: len(match) - 1] + ">" message = message.replace(match, mention, 1) - - if (session['unauthenticated']): - message = u"**[{}#{}]** {}".format(session['username'], session['user_id'], message) - else: - message = u"**<{}#{}>** {}".format(session['username'], session['discriminator'], message) # I would like to do a @ mention, but i am worried about notif spam + + if not guild_webhooks_enabled(guild_id): + if (session['unauthenticated']): + message = u"**[{}#{}]** {}".format(session['username'], session['user_id'], message) + else: + username = session['username'] + if dbUser: + if dbUser.nickname: + username = dbUser.nickname + message = u"**<{}#{}>** {}".format(username, session['discriminator'], message) # I would like to do a @ mention, but i am worried about notify spam return (message, illegal_post, illegal_reasons) def format_everyone_mention(channel, content): @@ -154,94 +69,8 @@ def format_everyone_mention(channel, content): content = content.replace("@here", u"@\u200Bhere") return content -def get_member_roles(guild_id, user_id): - q = db.session.query(GuildMembers).filter(GuildMembers.guild_id == guild_id).filter(GuildMembers.user_id == user_id).first() - return json.loads(q.roles) - -def get_dbguild_channels(guild_id): - q = db.session.query(Guilds).filter(Guilds.guild_id == guild_id).first() - return json.loads(q.channels) - -def get_guild_channels(guild_id): - if user_unauthenticated(): - member_roles = [guild_id] #equivilant to @everyone role - else: - member_roles = get_member_roles(guild_id, session['user_id']) - if guild_id not in member_roles: - member_roles.append(guild_id) - dbguild = db.session.query(Guilds).filter(Guilds.guild_id == guild_id).first() - guild_channels = json.loads(dbguild.channels) - guild_roles = json.loads(dbguild.roles) - guild_owner = str(dbguild.owner_id) - result_channels = [] - for channel in guild_channels: - if channel['type'] == "text": - result = {"channel": channel, "read": False, "write": False, "mention_everyone": False} - if guild_owner == session['user_id']: - result["read"] = True - result["write"] = True - result["mention_everyone"] = 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["mention_everyone"] = 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) - result["mention_everyone"] = user_has_permission(channel_perm, 17) - - # 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 - result["mention_everyone"] = False - - 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) +def filter_guild_channel(guild_id, channel_id, force_everyone=False): + channels = get_guild_channels(guild_id, force_everyone) for chan in channels: if chan["channel"]["id"] == channel_id: return chan @@ -261,10 +90,18 @@ def get_online_discord_users(guild_id, embed): member["hoist-role"] = None member["color"] = None if apimem: - for roleid in reversed(apimem["roles"]): - role = guildroles_filtered[roleid] + mem_roles = [] + for roleid in apimem["roles"]: + role = guildroles_filtered.get(roleid) + if not role: + continue + mem_roles.append(role) + mem_roles = sorted(mem_roles, key=lambda k: k['position']) + for role in mem_roles: if role["color"] != 0: member["color"] = '{0:02x}'.format(role["color"]) #int to hex + while len(member["color"]) < 6: + member["color"] = "0" + member["color"] if role["hoist"]: member["hoist-role"] = {} member["hoist-role"]["name"] = role["name"] @@ -273,7 +110,7 @@ def get_online_discord_users(guild_id, embed): 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') + time_past = (datetime.datetime.now() - datetime.timedelta(seconds = 15)).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':[]} @@ -289,6 +126,7 @@ def get_online_embed_users(guild_id): meta = { 'id': usrdb.user_id, 'username': usrdb.username, + 'nickname': usrdb.nickname, 'discriminator': usrdb.discriminator, 'avatar_url': generate_avatar_url(usrdb.user_id, usrdb.avatar), } @@ -299,6 +137,26 @@ def get_guild_emojis(guild_id): dbguild = db.session.query(Guilds).filter(Guilds.guild_id == guild_id).first() return json.loads(dbguild.emojis) +# Returns webhook url if exists and can post w/webhooks, otherwise None +def get_channel_webhook_url(guild_id, channel_id): + if not guild_webhooks_enabled(guild_id): + return None + dbguild = db.session.query(Guilds).filter(Guilds.guild_id == guild_id).first() + guild_webhooks = json.loads(dbguild.webhooks) + name = "[Titan] " + if user_unauthenticated(): + name = name + session["username"] + "#" + str(session["user_id"]) + else: + name = name + session["username"] + "#" + str(session["discriminator"]) + for webhook in guild_webhooks: + if channel_id == webhook["channel_id"] and webhook["name"] == name: + return { + "id": webhook["id"], + "token": webhook["token"] + } + webhook = discord_api.create_webhook(channel_id, name) + return webhook["content"] + @api.route("/fetch", methods=["GET"]) @valid_session_required(api=True) @rate_limiter.limit("2 per 2 second", key_func = channel_ratelimit_key) @@ -316,29 +174,57 @@ def fetch(): status_code = 403 if user_unauthenticated(): session['user_keys'].pop(guild_id, None) + session.modified = True else: chan = filter_guild_channel(guild_id, channel_id) + if not chan: + abort(404) if not chan.get("read"): status_code = 401 else: - messages = get_channel_messages(channel_id, after_snowflake) + messages = get_channel_messages(guild_id, channel_id, after_snowflake) status_code = 200 response = jsonify(messages=messages, status=status) response.status_code = status_code return response +@api.route("/fetch_visitor", methods=["GET"]) +@rate_limiter.limit("2 per 2 second", key_func = channel_ratelimit_key) +def fetch_visitor(): + guild_id = request.args.get("guild_id") + channel_id = request.args.get('channel_id') + after_snowflake = request.args.get('after', None, type=int) + if not guild_accepts_visitors(guild_id): + abort(403) + messages = {} + chan = filter_guild_channel(guild_id, channel_id, True) + if not chan: + abort(404) + if not chan.get("read"): + status_code = 401 + else: + messages = get_channel_messages(guild_id, channel_id, after_snowflake) + status_code = 200 + response = jsonify(messages=messages) + 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) +@rate_limiter.limit("1 per 5 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, illegal_post, illegal_reasons = format_post_content(guild_id, content) + if "user_id" in session: + dbUser = GuildMembers.query.filter(GuildMembers.guild_id == guild_id).filter(GuildMembers.user_id == session['user_id']).first() + else: + dbUser = None if user_unauthenticated(): key = session['user_keys'][guild_id] else: key = None + content, illegal_post, illegal_reasons = format_post_content(guild_id, channel_id, content, dbUser) status = update_user_status(guild_id, session['username'], key) message = {} if illegal_post: @@ -350,8 +236,30 @@ def post(): if not chan.get("write"): status_code = 401 elif not illegal_post: + userid = session["user_id"] content = format_everyone_mention(chan, content) - message = discord_api.create_message(channel_id, content) + webhook = get_channel_webhook_url(guild_id, channel_id) + if userid in get_administrators_list(): + oldcontent = content + content = "(Titan Dev) " + oldcontent + if webhook: + if (session['unauthenticated']): + username = session["username"] + "#" + str(session["user_id"]) + avatar = url_for('static', filename='img/titanembeds_round.png', _external=True) + else: + username = session["username"] + if dbUser: + if dbUser.nickname: + username = dbUser.nickname + if content.startswith("(Titan Dev) "): + content = content[12:] + username = "(Titan Dev) " + username + else: + username = username + "#" + str(session['discriminator']) + avatar = session['avatar'] + message = discord_api.execute_webhook(webhook.get("id"), webhook.get("token"), username, avatar, content) + else: + message = discord_api.create_message(channel_id, content) status_code = message['code'] response = jsonify(message=message.get('content', message), status=status, illegal_reasons=illegal_reasons) response.status_code = status_code @@ -393,21 +301,75 @@ def create_unauthenticated_user(): response.status_code = 403 return response +@api.route("/change_unauthenticated_username", methods=["POST"]) +@rate_limiter.limit("1 per 15 minute", key_func=guild_ratelimit_key) +def change_unauthenticated_username(): + 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): + if 'user_keys' not in session or guild_id not in session['user_keys'] or not session['unauthenticated']: + abort(424) + emitmsg = {"unauthenticated": True, "username": session["username"], "discriminator": session["user_id"]} + 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 + session['user_keys'][guild_id] = key + status = update_user_status(guild_id, username, key) + emit("embed_user_disconnect", emitmsg, room="GUILD_"+guild_id, namespace="/gateway") + return jsonify(status=status) + else: + status = {'banned': True} + response = jsonify(status=status) + response.status_code = 403 + return response + +def process_query_guild(guild_id, visitor=False): + widget = discord_api.get_widget(guild_id) + channels = get_guild_channels(guild_id, visitor) + if widget.get("success", True): + discordmembers = get_online_discord_users(guild_id, widget) + else: + discordmembers = [{"id": 0, "color": "FFD6D6", "status": "dnd", "username": "Discord Server Widget is Currently Disabled"}] + embedmembers = get_online_embed_users(guild_id) + emojis = get_guild_emojis(guild_id) + if visitor: + for channel in channels: + channel["write"] = False + return jsonify(channels=channels, discordmembers=discordmembers, embedmembers=embedmembers, emojis=emojis, instant_invite=widget.get("instant_invite", None)) + @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): - widget = discord_api.get_widget(guild_id) - channels = get_guild_channels(guild_id) - discordmembers = get_online_discord_users(guild_id, widget) - embedmembers = get_online_embed_users(guild_id) - emojis = get_guild_emojis(guild_id) - return jsonify(channels=channels, discordmembers=discordmembers, embedmembers=embedmembers, emojis=emojis, instant_invite=widget.get("instant_invite")) + return process_query_guild(guild_id) abort(403) abort(404) +@api.route("/query_guild_visitor", methods=["GET"]) +def query_guild_visitor(): + guild_id = request.args.get('guild_id') + if check_guild_existance(guild_id): + if not guild_accepts_visitors(guild_id): + abort(403) + return process_query_guild(guild_id, True) + abort(404) + @api.route("/create_authenticated_user", methods=["POST"]) @discord_users_only(api=True) def create_authenticated_user(): @@ -435,24 +397,32 @@ def create_authenticated_user(): response.status_code = 403 return response +def canCleanupDB(): + canclean = False + if request.form.get("secret", None) == config['app-secret']: + canclean = True + if 'user_id' in session: + if session['user_id'] in get_administrators_list(): + canclean = True + return canclean @api.route("/cleanup-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) + if canCleanupDB(): + db.session.query(KeyValueProperties).filter(KeyValueProperties.expiration < datetime.datetime.now()).delete() + db.session.commit() guilds = Guilds.query.all() for guild in guilds: - channelsjson = json.loads(guild.channels) + try: + channelsjson = json.loads(guild.channels) + except: + continue for channel in channelsjson: chanid = channel["id"] - dbmsg = Messages.query.filter(Messages.channel_id == chanid).all() - for idx, val in enumerate(dbmsg): - if len(dbmsg) - idx > 50: - db.session.delete(val) - else: - continue - db.session.commit() + msgs = db.session.query(Messages).filter(Messages.channel_id == chanid).order_by(Messages.timestamp.desc()).offset(50).all() + for msg in msgs: + db.session.delete(msg) + db.session.commit() + return ('', 204) abort(401) diff --git a/webapp/titanembeds/blueprints/embed/__init__.py b/webapp/titanembeds/blueprints/embed/__init__.py index 34cd6de..0487374 100644 --- a/webapp/titanembeds/blueprints/embed/__init__.py +++ b/webapp/titanembeds/blueprints/embed/__init__.py @@ -1 +1 @@ -from embed import embed +from .embed import embed diff --git a/webapp/titanembeds/blueprints/embed/embed.py b/webapp/titanembeds/blueprints/embed/embed.py index 79c982b..086e8ae 100644 --- a/webapp/titanembeds/blueprints/embed/embed.py +++ b/webapp/titanembeds/blueprints/embed/embed.py @@ -1,5 +1,5 @@ from flask import Blueprint, render_template, abort, redirect, url_for, session, request -from titanembeds.utils import check_guild_existance, guild_query_unauth_users_bool +from titanembeds.utils import check_guild_existance, guild_query_unauth_users_bool, guild_accepts_visitors from titanembeds.oauth import generate_guild_icon_url, generate_avatar_url from titanembeds.database import db, Guilds, UserCSS from config import config @@ -66,6 +66,7 @@ def guild_embed(guild_id): guild_id=guild_id, guild=guild_dict, generate_guild_icon=generate_guild_icon_url, unauth_enabled=guild_query_unauth_users_bool(guild_id), + visitors_enabled=guild_accepts_visitors(guild_id), client_id=config['client-id'], css=customcss, cssvariables=parse_css_variable(customcss) diff --git a/webapp/titanembeds/blueprints/gateway/__init__.py b/webapp/titanembeds/blueprints/gateway/__init__.py new file mode 100644 index 0000000..0abc29c --- /dev/null +++ b/webapp/titanembeds/blueprints/gateway/__init__.py @@ -0,0 +1 @@ +from .gateway import Gateway diff --git a/webapp/titanembeds/blueprints/gateway/gateway.py b/webapp/titanembeds/blueprints/gateway/gateway.py new file mode 100644 index 0000000..6afd737 --- /dev/null +++ b/webapp/titanembeds/blueprints/gateway/gateway.py @@ -0,0 +1,106 @@ +from titanembeds.utils import socketio, guild_accepts_visitors, get_client_ipaddr, discord_api +from titanembeds.userbookkeeping import check_user_in_guild, get_guild_channels, update_user_status, guild_webhooks_enabled +from titanembeds.database import db, GuildMembers, get_guild_member, Guilds +from flask_socketio import Namespace, emit, disconnect, join_room, leave_room +import functools +from flask import request, session +import time +import json + +class Gateway(Namespace): + def on_connect(self): + emit('hello') + + def on_identify(self, data): + guild_id = data["guild_id"] + if not guild_accepts_visitors(guild_id) and not check_user_in_guild(guild_id): + disconnect() + return + session["socket_guild_id"] = guild_id + channels = [] + if guild_accepts_visitors(guild_id) and not check_user_in_guild(guild_id): + channels = get_guild_channels(guild_id, force_everyone=True) + else: + channels = get_guild_channels(guild_id) + join_room("GUILD_"+guild_id) + for chan in channels: + if chan["read"]: + join_room("CHANNEL_"+chan["channel"]["id"]) + if session.get("unauthenticated", True) and guild_id in session.get("user_keys", {}): + join_room("IP_"+get_client_ipaddr()) + elif not session.get("unauthenticated", True): + join_room("USER_"+session["user_id"]) + visitor_mode = data["visitor_mode"] + if not visitor_mode: + if session["unauthenticated"]: + emit("embed_user_connect", {"unauthenticated": True, "username": session["username"], "discriminator": session["user_id"]}, room="GUILD_"+guild_id) + else: + nickname = db.session.query(GuildMembers).filter(GuildMembers.guild_id == guild_id, GuildMembers.user_id == session["user_id"]).first().nickname + emit("embed_user_connect", {"unauthenticated": False, "id": session["user_id"], "nickname": nickname, "username": session["username"],"discriminator": session["discriminator"], "avatar_url": session["avatar"]}, room="GUILD_"+guild_id) + emit("identified") + + def on_disconnect(self): + if "user_keys" not in session: + return + guild_id = session["socket_guild_id"] + msg = {} + if session["unauthenticated"]: + msg = {"unauthenticated": True, "username": session["username"], "discriminator": session["user_id"]} + else: + msg = {"unauthenticated": False, "id": session["user_id"]} + emit("embed_user_disconnect", msg, room="GUILD_"+guild_id) + if guild_webhooks_enabled(guild_id): # Delete webhooks + dbguild = db.session.query(Guilds).filter(Guilds.guild_id == guild_id).first() + guild_webhooks = json.loads(dbguild.webhooks) + name = "[Titan] " + if session["unauthenticated"]: + name = name + session["username"] + "#" + str(session["user_id"]) + else: + name = name + session["username"] + "#" + str(session["discriminator"]) + for webhook in guild_webhooks: + if webhook["name"] == name: + discord_api.delete_webhook(webhook["id"], webhook["token"]) + + def on_heartbeat(self, data): + guild_id = data["guild_id"] + visitor_mode = data["visitor_mode"] + if not visitor_mode: + key = None + if session["unauthenticated"]: + key = session["user_keys"][guild_id] + status = update_user_status(guild_id, session["username"], key) + if status["revoked"] or status["banned"]: + emit("revoke") + time.sleep(1000) + disconnect() + else: + if not guild_accepts_visitors(guild_id): + disconnect() + + def on_channel_list(self, data): + guild_id = data["guild_id"] + visitor_mode = data["visitor_mode"] + channels = None + if visitor_mode or session.get("unauthenticated", True): + channels = get_guild_channels(guild_id, True) + else: + channels = get_guild_channels(guild_id) + for chan in channels: + if chan["read"]: + join_room("CHANNEL_"+chan["channel"]["id"]) + else: + leave_room("CHANNEL_"+chan["channel"]["id"]) + emit("channel_list", channels) + + def on_current_user_info(self, data): + guild_id = data["guild_id"] + if "user_keys" in session and not session["unauthenticated"]: + dbMember = get_guild_member(guild_id, session["user_id"]) + usr = { + 'avatar': session["avatar"], + 'username': dbMember.username, + 'nickname': dbMember.nickname, + 'discriminator': dbMember.discriminator, + 'user_id': session['user_id'], + } + emit("current_user_info", usr) \ No newline at end of file diff --git a/webapp/titanembeds/blueprints/user/__init__.py b/webapp/titanembeds/blueprints/user/__init__.py index 7645840..a4b1c01 100644 --- a/webapp/titanembeds/blueprints/user/__init__.py +++ b/webapp/titanembeds/blueprints/user/__init__.py @@ -1 +1 @@ -from user import user +from .user import user diff --git a/webapp/titanembeds/blueprints/user/user.py b/webapp/titanembeds/blueprints/user/user.py index bea781c..a6822f9 100644 --- a/webapp/titanembeds/blueprints/user/user.py +++ b/webapp/titanembeds/blueprints/user/user.py @@ -1,10 +1,12 @@ from flask import Blueprint, request, redirect, jsonify, abort, session, url_for, render_template +from flask import current_app as app from config import config from titanembeds.decorators import discord_users_only -from titanembeds.database import db, Guilds, UnauthenticatedUsers, UnauthenticatedBans, Cosmetics, UserCSS +from titanembeds.database import db, Guilds, UnauthenticatedUsers, UnauthenticatedBans, Cosmetics, UserCSS, set_titan_token, get_titan_token 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 +import paypalrestsdk import json user = Blueprint("user", __name__) @@ -40,6 +42,9 @@ def callback(): session['username'] = user['username'] session['discriminator'] = user['discriminator'] session['avatar'] = generate_avatar_url(user['id'], user['avatar']) + session["tokens"] = get_titan_token(session["user_id"]) + if session["tokens"] == -1: + session["tokens"] = 0 if session["redirect"]: redir = session["redirect"] session['redirect'] = None @@ -59,9 +64,6 @@ def logout(): @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'] @@ -179,6 +181,8 @@ def administrate_guild(guild_id): "id": db_guild.guild_id, "name": db_guild.name, "unauth_users": db_guild.unauth_users, + "visitor_view": db_guild.visitor_view, + "webhook_messages": db_guild.webhook_messages, "chat_links": db_guild.chat_links, "bracket_links": db_guild.bracket_links, "mentions_limit": db_guild.mentions_limit, @@ -196,12 +200,14 @@ def update_administrate_guild(guild_id): if not db_guild: abort(400) db_guild.unauth_users = request.form.get("unauth_users", db_guild.unauth_users) in ["true", True] + db_guild.visitor_view = request.form.get("visitor_view", db_guild.visitor_view) in ["true", True] + db_guild.webhook_messages = request.form.get("webhook_messages", db_guild.webhook_messages) in ["true", True] db_guild.chat_links = request.form.get("chat_links", db_guild.chat_links) in ["true", True] db_guild.bracket_links = request.form.get("bracket_links", db_guild.bracket_links) in ["true", True] db_guild.mentions_limit = request.form.get("mentions_limit", db_guild.mentions_limit) discordio = request.form.get("discordio", db_guild.discordio) - if discordio.strip() == "": + if discordio and discordio.strip() == "": discordio = None db_guild.discordio = discordio db.session.commit() @@ -209,6 +215,8 @@ def update_administrate_guild(guild_id): id=db_guild.id, guild_id=db_guild.guild_id, unauth_users=db_guild.unauth_users, + visitor_view=db_guild.visitor_view, + webhook_messages=db_guild.webhook_messages, chat_links=db_guild.chat_links, bracket_links=db_guild.bracket_links, mentions_limit=db_guild.mentions_limit, @@ -224,7 +232,7 @@ def add_bot(guild_id): 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) + members = sorted(members, key=lambda k: datetime.datetime.strptime(str(k.last_timestamp.replace(tzinfo=None, microsecond=0)), "%Y-%m-%d %H:%M:%S"), reverse=True) for member in members: user = { "id": member.id, @@ -324,3 +332,70 @@ def revoke_unauthenticated_user(): abort(409) db_user.revokeUser() return ('', 204) + +@user.route('/donate', methods=["GET"]) +@discord_users_only() +def donate_get(): + return render_template('donate.html.j2') + +def get_paypal_api(): + return paypalrestsdk.Api({ + 'mode': 'sandbox' if app.config["DEBUG"] else 'live', + 'client_id': config["paypal-client-id"], + 'client_secret': config["paypal-client-secret"]}) + +@user.route('/donate', methods=['POST']) +@discord_users_only() +def donate_post(): + donation_amount = request.form.get('amount') + if not donation_amount: + abort(402) + + donation_amount = "{0:.2f}".format(float(donation_amount)) + payer = {"payment_method": "paypal"} + items = [{"name": "TitanEmbeds Donation", + "price": donation_amount, + "currency": "USD", + "quantity": "1"}] + amount = {"total": donation_amount, + "currency": "USD"} + description = "Donate and support TitanEmbeds development." + redirect_urls = {"return_url": url_for('user.donate_confirm', success="true", _external=True), + "cancel_url": url_for('index', _external=True)} + payment = paypalrestsdk.Payment({"intent": "sale", + "payer": payer, + "redirect_urls": redirect_urls, + "transactions": [{"item_list": {"items": + items}, + "amount": amount, + "description": + description}]}, api=get_paypal_api()) + if payment.create(): + for link in payment.links: + if link['method'] == "REDIRECT": + return redirect(link["href"]) + return redirect(url_for('index')) + +@user.route("/donate/confirm") +@discord_users_only() +def donate_confirm(): + if not request.args.get('success'): + return redirect(url_for('index')) + payment = paypalrestsdk.Payment.find(request.args.get('paymentId'), api=get_paypal_api()) + if payment.execute({"payer_id": request.args.get('PayerID')}): + trans_id = str(payment.transactions[0]["related_resources"][0]["sale"]["id"]) + amount = float(payment.transactions[0]["amount"]["total"]) + tokens = int(amount * 100) + action = "PAYPAL {}".format(trans_id) + set_titan_token(session["user_id"], tokens, action) + session["tokens"] = get_titan_token(session["user_id"]) + return redirect(url_for('user.donate_thanks', transaction=trans_id)) + else: + return redirect(url_for('index')) + +@user.route("/donate/thanks") +@discord_users_only() +def donate_thanks(): + tokens = get_titan_token(session["user_id"]) + transaction = request.args.get("transaction") + return render_template("donate_thanks.html.j2", tokens=tokens, transaction=transaction) \ No newline at end of file diff --git a/webapp/titanembeds/database/__init__.py b/webapp/titanembeds/database/__init__.py index 0450efc..0374ccb 100644 --- a/webapp/titanembeds/database/__init__.py +++ b/webapp/titanembeds/database/__init__.py @@ -2,12 +2,34 @@ 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 guild_members import GuildMembers, list_all_guild_members -from keyvalue_properties import KeyValueProperties, set_keyvalproperty, get_keyvalproperty, getexpir_keyvalproperty, setexpir_keyvalproperty, ifexists_keyvalproperty, delete_keyvalproperty -from messages import Messages, get_channel_messages -from cosmetics import Cosmetics -from user_css import UserCSS \ No newline at end of file +from .guilds import Guilds +from .unauthenticated_users import UnauthenticatedUsers +from .unauthenticated_bans import UnauthenticatedBans +from .authenticated_users import AuthenticatedUsers +from .guild_members import GuildMembers, list_all_guild_members, get_guild_member +from .keyvalue_properties import KeyValueProperties, set_keyvalproperty, get_keyvalproperty, getexpir_keyvalproperty, setexpir_keyvalproperty, ifexists_keyvalproperty, delete_keyvalproperty +from .messages import Messages, get_channel_messages +from .cosmetics import Cosmetics +from .user_css import UserCSS +from .administrators import Administrators, get_administrators_list +from .titan_tokens import TitanTokens, get_titan_token +from .token_transactions import TokenTransactions + +def set_titan_token(user_id, amt_change, action): + token_count = get_titan_token(user_id) + if token_count >= 0: + token_usr = db.session.query(TitanTokens).filter(TitanTokens.user_id == user_id).first() + else: + token_count = 0 + token_usr = TitanTokens(user_id, 0) + db.session.add(token_usr) + db.session.commit() + new_token_count = token_count + amt_change + if new_token_count < 0: + return False + transact = TokenTransactions(user_id, action, amt_change, token_count, new_token_count) + db.session.add(transact) + token_usr.tokens = new_token_count + db.session.add(token_usr) + db.session.commit() + return True \ No newline at end of file diff --git a/webapp/titanembeds/database/administrators.py b/webapp/titanembeds/database/administrators.py new file mode 100644 index 0000000..41cf3a5 --- /dev/null +++ b/webapp/titanembeds/database/administrators.py @@ -0,0 +1,13 @@ +from titanembeds.database import db + +class Administrators(db.Model): + __tablename__ = "administrators" + id = db.Column(db.Integer, primary_key=True) # Auto increment id + user_id = db.Column(db.String(255), nullable=False) # Discord user id of user of an administrator + +def get_administrators_list(): + q = db.session.query(Administrators).all() + their_ids = [] + for admin in q: + their_ids.append(admin.user_id) + return their_ids \ No newline at end of file diff --git a/webapp/titanembeds/database/cosmetics.py b/webapp/titanembeds/database/cosmetics.py index a7a5295..a442c0c 100644 --- a/webapp/titanembeds/database/cosmetics.py +++ b/webapp/titanembeds/database/cosmetics.py @@ -4,4 +4,12 @@ class Cosmetics(db.Model): __tablename__ = "cosmetics" id = db.Column(db.Integer, primary_key=True) # Auto increment id user_id = db.Column(db.String(255), nullable=False) # Discord user id of user of cosmetics - css = db.Column(db.Boolean(), nullable=False) # If they can create/edit custom CSS \ No newline at end of file + css = db.Column(db.Boolean(), nullable=False) # If they can create/edit custom CSS + + def __init__(self, user_id, **kwargs): + self.user_id = user_id + + if "css" in kwargs: + self.css = kwargs["css"] + else: + self.css = False diff --git a/webapp/titanembeds/database/guild_members.py b/webapp/titanembeds/database/guild_members.py index 1ea147f..1f735a1 100644 --- a/webapp/titanembeds/database/guild_members.py +++ b/webapp/titanembeds/database/guild_members.py @@ -43,3 +43,6 @@ def list_all_guild_members(guild_id): "nickname": member.nickname, }) return memlist + +def get_guild_member(guild_id, member_id): + return db.session.query(GuildMembers).filter(GuildMembers.guild_id == guild_id, GuildMembers.user_id == member_id).first() diff --git a/webapp/titanembeds/database/guilds.py b/webapp/titanembeds/database/guilds.py index 934bf6a..bf34715 100644 --- a/webapp/titanembeds/database/guilds.py +++ b/webapp/titanembeds/database/guilds.py @@ -6,25 +6,31 @@ class Guilds(db.Model): guild_id = db.Column(db.String(255), nullable=False) # Discord guild id name = db.Column(db.String(255), nullable=False) # Name unauth_users = db.Column(db.Boolean(), nullable=False, default=1) # If allowed unauth users + visitor_view = db.Column(db.Boolean(), nullable=False, default=0) # If users are automatically "signed in" and can view chat + webhook_messages = db.Column(db.Boolean(), nullable=False, default=0) # Use webhooks to send messages instead of the bot chat_links = db.Column(db.Boolean(), nullable=False, default=1) # If users can post links bracket_links = db.Column(db.Boolean(), nullable=False, default=1) # If appending brackets to links to prevent embed mentions_limit = db.Column(db.Integer, nullable=False, default=11) # If there is a limit on the number of mentions in a msg - roles = db.Column(db.Text(), nullable=False) # Guild Roles - channels = db.Column(db.Text(), nullable=False) # Guild channels - emojis = db.Column(db.Text(), nullable=False) # Guild Emojis + roles = db.Column(db.Text().with_variant(db.Text(4294967295), 'mysql'), nullable=False) # Guild Roles + channels = db.Column(db.Text().with_variant(db.Text(4294967295), 'mysql'), nullable=False) # Guild channels + webhooks = db.Column(db.Text().with_variant(db.Text(4294967295), 'mysql'), nullable=False) # Guild webhooks + emojis = db.Column(db.Text().with_variant(db.Text(4294967295), 'mysql'), nullable=False) # Guild Emojis owner_id = db.Column(db.String(255), nullable=False) # Snowflake of the owner icon = db.Column(db.String(255)) # The icon string, null if none discordio = db.Column(db.String(255)) # Custom Discord.io Invite Link - def __init__(self, guild_id, name, roles, channels, emojis, owner_id, icon): + def __init__(self, guild_id, name, roles, channels, webhooks, emojis, owner_id, icon): self.guild_id = guild_id self.name = name self.unauth_users = True # defaults to true + self.visitor_view = False + self.webhook_messages = False self.chat_links = True self.bracket_links = True self.mentions_limit = -1 # -1 = unlimited mentions self.roles = roles self.channels = channels + self.webhooks = webhooks self.emojis = emojis self.owner_id = owner_id self.icon = icon diff --git a/webapp/titanembeds/database/keyvalue_properties.py b/webapp/titanembeds/database/keyvalue_properties.py index 8cfaa07..64db514 100644 --- a/webapp/titanembeds/database/keyvalue_properties.py +++ b/webapp/titanembeds/database/keyvalue_properties.py @@ -20,14 +20,14 @@ def set_keyvalproperty(key, value, expiration=None): 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): + if q.count() > 0 and (q.first().expiration is None or q.first().expiration.replace(tzinfo=None) > now.replace(tzinfo=None)): 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): + if q.count() > 0 and (q.first().expiration is not None and q.first().expiration.replace(tzinfo=None) > now.replace(tzinfo=None)): return int(q.first().expiration.strftime('%s')) return 0 diff --git a/webapp/titanembeds/database/messages.py b/webapp/titanembeds/database/messages.py index fd7d131..fbe497f 100644 --- a/webapp/titanembeds/database/messages.py +++ b/webapp/titanembeds/database/messages.py @@ -1,4 +1,4 @@ -from titanembeds.database import db +from titanembeds.database import db, get_guild_member from sqlalchemy import cast import json @@ -29,14 +29,18 @@ class Messages(db.Model): def __repr__(self): return ''.format(self.id, self.guild_id, self.guild_id, self.channel_id, self.message_id) -def get_channel_messages(channel_id, after_snowflake=None): +def get_channel_messages(guild_id, channel_id, after_snowflake=None): if not after_snowflake: - q = db.session.query(Messages).filter(Messages.channel_id == channel_id).order_by(Messages.id.desc()).limit(50) + q = db.session.query(Messages).filter(Messages.channel_id == channel_id).order_by(Messages.timestamp.desc()).limit(50) else: - q = db.session.query(Messages).filter(cast(Messages.channel_id, db.Integer) == int(channel_id)).filter(Messages.message_id > after_snowflake).order_by(Messages.id.desc()).limit(50) + q = db.session.query(Messages).filter(cast(Messages.channel_id, db.Integer) == int(channel_id)).filter(Messages.message_id > after_snowflake).order_by(Messages.timestamp.desc()).limit(50) msgs = [] + snowflakes = [] for x in q: - msgs.append({ + if x.message_id in snowflakes: + continue + snowflakes.append(x.message_id) + message = { "attachments": json.loads(x.attachments), "timestamp": x.timestamp, "id": x.message_id, @@ -45,5 +49,15 @@ def get_channel_messages(channel_id, after_snowflake=None): "content": x.content, "channel_id": x.channel_id, "mentions": json.loads(x.mentions) - }) + } + member = get_guild_member(guild_id, message["author"]["id"]) + message["author"]["nickname"] = None + if member: + message["author"]["nickname"] = member.nickname + for mention in message["mentions"]: + author = get_guild_member(guild_id, mention["id"]) + mention["nickname"] = None + if author: + mention["nickname"] = author.nickname + msgs.append(message) return msgs diff --git a/webapp/titanembeds/database/titan_tokens.py b/webapp/titanembeds/database/titan_tokens.py new file mode 100644 index 0000000..3ea2bff --- /dev/null +++ b/webapp/titanembeds/database/titan_tokens.py @@ -0,0 +1,18 @@ +from titanembeds.database import db + +class TitanTokens(db.Model): + __tablename__ = "titan_tokens" + id = db.Column(db.Integer, primary_key=True) # Auto increment id + user_id = db.Column(db.String(255), nullable=False) # Discord user id of user + tokens = db.Column(db.Integer, nullable=False, default=0) # Token amount + + def __init__(self, user_id, tokens): + self.user_id = user_id + self.tokens = tokens + +def get_titan_token(user_id): + q = db.session.query(TitanTokens).filter(TitanTokens.user_id == user_id).first() + if q: + return q.tokens + else: + return -1 \ No newline at end of file diff --git a/webapp/titanembeds/database/token_transactions.py b/webapp/titanembeds/database/token_transactions.py new file mode 100644 index 0000000..7f184f5 --- /dev/null +++ b/webapp/titanembeds/database/token_transactions.py @@ -0,0 +1,21 @@ +from titanembeds.database import db +import datetime +import time + +class TokenTransactions(db.Model): + __tablename__ = "token_transactions" + id = db.Column(db.Integer, primary_key=True) # Auto increment id + user_id = db.Column(db.String(255), nullable=False) # Discord user id of user + timestamp = db.Column(db.TIMESTAMP, nullable=False) # The timestamp of when the action took place + action = db.Column(db.String(255), nullable=False) # Very short description of the action + net_tokens = db.Column(db.Integer, nullable=False) # Net change of the token amount + start_tokens = db.Column(db.Integer, nullable=False) # Token amount before transaction + end_tokens = db.Column(db.Integer, nullable=False) # Tokens after transaction + + def __init__(self, user_id, action, net_tokens, start_tokens, end_tokens): + self.user_id = user_id + self.timestamp = datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S') + self.action = action + self.net_tokens = net_tokens + self.start_tokens = start_tokens + self.end_tokens = end_tokens diff --git a/webapp/titanembeds/database/user_css.py b/webapp/titanembeds/database/user_css.py index 06cc8e1..36f3c4f 100644 --- a/webapp/titanembeds/database/user_css.py +++ b/webapp/titanembeds/database/user_css.py @@ -6,7 +6,7 @@ class UserCSS(db.Model): name = db.Column(db.String(255), nullable=False) # CSS Name user_id = db.Column(db.String(255), nullable=False) # Discord client ID of the owner of the css (can edit) css_variables = db.Column(db.Text()) # Customizeable CSS Variables - css = db.Column(db.Text()) # CSS contents + css = db.Column(db.Text().with_variant(db.Text(4294967295), 'mysql')) # CSS contents def __init__(self, name, user_id, css_variables=None, css=None): self.name = name diff --git a/webapp/titanembeds/discordrest.py b/webapp/titanembeds/discordrest.py index 5414203..c89d54f 100644 --- a/webapp/titanembeds/discordrest.py +++ b/webapp/titanembeds/discordrest.py @@ -17,7 +17,7 @@ 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__) + self.user_agent = "TitanEmbeds (https://github.com/TitanEmbeds/Titan) Python/{} requests/{}".format(sys.version_info, requests.__version__) def init_discordrest(self): if not self._bucket_contains("global_limited"): @@ -135,7 +135,41 @@ class DiscordREST: 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.get("success", True): + return {"success": False} if not embed['content']['enabled']: self.modify_guild_embed(guild_id, enabled=True, channel_id=guild_id) widget = requests.get(_endpoint).json() return widget + + + ##################### + # Webhook + ##################### + + def create_webhook(self, channel_id, name, avatar=None): + _endpoint = "/channels/{channel_id}/webhooks".format(channel_id=channel_id) + payload = { + "name": name, + } + if avatar: + payload["avatar"] = avatar + r = self.request("POST", _endpoint, data=payload, json=True) + return r + + def execute_webhook(self, webhook_id, webhook_token, username, avatar, content, wait=True): + _endpoint = "/webhooks/{id}/{token}".format(id=webhook_id, token=webhook_token) + if wait: + _endpoint += "?wait=true" + payload = { + 'content': content, + 'avatar_url': avatar, + 'username': username + } + r = self.request("POST", _endpoint, data=payload) + return r + + def delete_webhook(self, webhook_id, webhook_token): + _endpoint = "/webhooks/{id}/{token}".format(id=webhook_id, token=webhook_token) + r = self.request("DELETE", _endpoint) + return r \ No newline at end of file diff --git a/webapp/titanembeds/oauth.py b/webapp/titanembeds/oauth.py index 389d32e..e0bfab9 100644 --- a/webapp/titanembeds/oauth.py +++ b/webapp/titanembeds/oauth.py @@ -45,14 +45,14 @@ def user_has_permission(permission, index): return bool((int(permission) >> index) & 1) def get_user_guilds(): - cache = get_keyvalproperty("OAUTH/USERGUILDS/"+make_user_cache_key()) + cache = get_keyvalproperty("OAUTH/USERGUILDS/"+str(make_user_cache_key())) if cache: return cache req = discordrest_from_user("/users/@me/guilds") if req.status_code != 200: abort(req.status_code) req = json.dumps(req.json()) - set_keyvalproperty("OAUTH/USERGUILDS/"+make_user_cache_key(), req, 250) + set_keyvalproperty("OAUTH/USERGUILDS/"+str(make_user_cache_key()), req, 250) return req def get_user_managed_servers(): @@ -96,5 +96,5 @@ 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={}".format(config['client-id'], '536083583', guild_id) + url = "https://discordapp.com/oauth2/authorize?&client_id={}&scope=bot&permissions={}&guild_id={}".format(config['client-id'], '537349164', guild_id) return url diff --git a/webapp/titanembeds/static/css/embedstyle.css b/webapp/titanembeds/static/css/embedstyle.css index 85d2382..439bf6f 100644 --- a/webapp/titanembeds/static/css/embedstyle.css +++ b/webapp/titanembeds/static/css/embedstyle.css @@ -1,54 +1,121 @@ +@font-face { + font-family: Whitney; + font-style: light; + font-weight: 300; + src: url("/static/fonts/whitney_light.woff") format("woff"); +} + +@font-face { + font-family: Whitney; + font-style: normal; + font-weight: 500; + src: url("/static/fonts/whitney_normal.woff") format("woff"); +} + +@font-face { + font-family: Whitney; + font-style: medium; + font-weight: 600; + src: url("/static/fonts/whitney_medium.woff") format("woff"); +} + +@font-face { + font-family: Whitney; + font-style: bold; + font-weight: 700; + src: url("/static/fonts/whitney_bold.woff") format("woff"); +} + html { -background-color: #455a64; -color: white; + background-color: #455a64; + color: white; + font-family: Whitney, Helvetica Neue, Helvetica, Arial, sans-serif; } main { -min-height: calc(100vh - 80px); -overflow-x: hidden; + min-height: calc(100vh - 80px); + overflow-x: hidden; } footer { -position: fixed; -bottom: 0; -left: 0; -right: 0; -height: 50px; -background-color: #37474f; + 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; + 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; + font-size: 1.5rem; +} + +@media only screen and (min-width: 601px) { + nav a.button-collapse { + display: block; + } +} + +body > div.navbar-fixed > nav > div { + background: #263238 background: -webkit-linear-gradient(#263238, #37474f, #455a64); + /* For Safari 5.1 to 6.0 */ + + background: -o-linear-gradient(#263238, #37474f, #455a64); + /* For Opera 11.1 to 12.0 */ + + background: -moz-linear-gradient(#263238, #37474f, #455a64); + /* For Firefox 3.6 to 15 */ + + background: linear-gradient(#263238, #37474f, #455a64); + /* Standard syntax */ } @media only screen and (min-width: 993px) { -.container { - width: 85%; -} + .container { + width: 85%; + } } .side-nav { -color: white; -background-color: #607d8b; + color: white; + background-color: #607d8b; + max-width: 95%; } .side-nav .userView .name { -font-size: 20px; + font-size: 20px; } .side-nav li>a { -color: #eceff1; + color: #eceff1; } .side-nav .subheader { -color: #cfd8dc; -font-variant: small-caps; + color: #cfd8dc; + font-variant: small-caps; +} + +.side-nav div.divider { + background-color: #90a4ae; + margin-left: 10px; + margin-right: 10px; +} + +#members-btn > i { + visibility: hidden; +} + +#members-btn { + visibility: visible; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNiIgaGVpZ2h0PSIyNiI+CiAgPGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgIDxwYXRoIGQ9Ik0xIDFoMjR2MjRIMVYxeiIvPgogICAgPHBhdGggZmlsbD0iI0ZGRiIgZD0iTTE2Ljc3MjA3NTQgMTQuNjc5MTc3OGMtLjIyNDkwOTctLjIyNTY4ODYtLjQ3NDE1NzMtLjQzNTE2ODEtLjc0MjA3NTQtLjYyOTE3NzguMzUtLjAzLjY4LS4wNS45Ny0uMDUgMi4zMyAwIDcgMS4xNyA3IDMuNVYyMGgtNHYtMWMwLTEuODgxNjkxNC0xLjQwNzcwNTYtMy4zMjMwMTQzLTMuMjI3OTI0Ni00LjMyMDgyMjJ6bS0xLjQzODU3MzUtOC4xNzU4NTM0QzE1LjgwOTgwODIgNi4xODUyNTE3MyAxNi4zODI3ODQ1IDYgMTcgNmMxLjY2IDAgMi45OSAxLjM0IDIuOTkgM3MtMS4zMyAzLTIuOTkgM2MtLjYxNzE5MTQgMC0xLjE5MDE0NzEtLjE4NTIzNzMtMS42NjY0NDIzLS41MDMyODcyQzE1Ljc1NzQ4MzIgMTAuNzYyMTQwOTUgMTYgOS45MDk1NTYgMTYgOS4wMDAwNjY5M2MwLS45MDk1Mjg5Ni0uMjQyNTM4MS0xLjc2MjE0ODgtLjY2NjQ5ODEtMi40OTY3NDI1M3pNMTAgMTNjMi4yMSAwIDQtMS43OSA0LTRzLTEuNzktNC00LTQtNCAxLjc5LTQgNCAxLjc5IDQgNCA0em0wIDJjLTIuNjcgMC04IDEuMzQtOCA0djJoMTZ2LTJjMC0yLjY2LTUuMzMtNC04LTR6Ii8+CiAgPC9nPgo8L3N2Zz4=); + background-repeat: no-repeat; + margin-top: 18px } .role-title { @@ -57,71 +124,118 @@ font-variant: small-caps; font-size: 80% !important; } -.divider { -background-color: #90a4ae; -} - .channel-hash { -font-size: 95%; -color: #b0bec5; + font-size: 95%; + color: #b0bec5; } .membercircle { -margin-top: 5px; -height: 40px; + margin-top: 5px; + height: 40px; } .membername { -position: absolute; -padding-left: 10px; + 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; +#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 { +#curuser_discrim, +#curuser_name { display: block; } + +#chatcontent > p { + display: table; + width: 90%; +} + +#chatcontent > p > span { + display: table-row +} + +::-webkit-input-placeholder { + color: rgb(99, 99, 99); +} + +:-moz-placeholder { + color: rgb(99, 99, 99); +} + +::-moz-placeholder { + color: rgb(99, 99, 99); +} + +:-ms-input-placeholder { + color: rgb(99, 99, 99); +} + +::-ms-input-placeholder { + color: rgb(99, 99, 99); +} + +#discord-members > li > a.subheader, +#members-nav > li:nth-child(1) > a, +#discord-members-count, +#embed-discord-members-count, +#members-nav > li:nth-child(4) > a, +#guest-members-count, +#members-nav > li:nth-child(6) > a { + text-transform: uppercase; +} + +.circle:hover { + border-radius: 20px; + background: linear-gradient(to right, #f9f9f9 90%, #fff); +} + +#channels-list > li:hover { + -webkit-filter: brightness(150%); } .chatusername { -font-weight: bold; -color: #eceff1; + font-weight: bold; + color: #eceff1; } .chattimestamp { -font-size: 10px; -color: #90a4ae; -margin-right: 3px; + font-size: 10px; + color: #90a4ae; + margin-right: 3px; } .footercontainer { -width: 100%; -position: relative; -margin: 10px; -white-space: nowrap; -overflow: hidden; + width: 100%; + position: relative; + margin: 10px; + white-space: nowrap; + overflow: hidden; + border-radius: 20px; + border: 1px solid rgb(99, 99, 99); + margin-left: -0px; + padding-left: -4px; } #messageboxouter { @@ -130,35 +244,53 @@ overflow: hidden; } .currentuserchip { -display: inline-block; -position: relative; -top: -6px; -padding: 6px; -padding-right: 9px; -background-color: #455a64; + display: inline-block; + position: relative; + top: -6px; + padding: 6px; + padding-right: 9px; + background-color: #455a64; } .currentuserimage { -width: 30px; + width: 30px; } .currentusername { -position: relative; -top: 7px; -left: 5px; + position: relative; + top: 7px; + left: 5px; +} + +#curuser_discrim { + font-size: 50%; +} + +#curuser_discrim, +#curuser_name { + margin-top: -2px; +} + +#currentuserimage { + margin-top: 4px; + margin-right: 4px; } .input-field { -position: relative; -top: -19px; + position: relative; + top: -19px; } .left { -float: left; + float: left; } .modal { -background-color: #546e7a; + background-color: #546e7a; +} + +.modal-overlay { + height: auto; } .betatag { @@ -174,6 +306,10 @@ background-color: #546e7a; font-size: 85%; } +.input-field label { + color: white; +} + a { color: #82b1ff; } @@ -225,112 +361,124 @@ a { display: -ms-flexbox; display: flex; -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; } } #nameplate { cursor: pointer; -} -@font-face { - font-family: Whitney; - font-style: light; - font-weight: 300; - src: url("../fonts/whitney_light.woff") format("woff") + background: transparent; + margin-left: 10px; } -@font-face { - font-family: Whitney; - font-style: normal; - font-weight: 500; - src: url("../fonts/whitney_normal.woff") format("woff") +#visitor_mode_message { + margin-right: auto; + margin-left: auto; + display: block; + width: 305px; } -@font-face { - font-family: Whitney; - font-style: medium; - font-weight: 600; - src: url("../fonts/whitney_medium.woff") format("woff") +#visitor_mode_message_note { + display: none; } -@font-face { - font-family: Whitney; - font-style: bold; - font-weight: 700; - src: url("../fonts/whitney_bold.woff") format("woff") +@media only screen and (min-width: 400px) { + #visitor_mode_message_note { + display: inline; + } } -* { -font-family: Whitney, Helvetica Neue, Helvetica, Arial, sans-serif; + +#focusmodal { + background-color: rgba(84, 110, 122, 0.58); + text-shadow: 1px 1px 2px black, 0 0 1em #607d8b, 0 0 0.2em #b0bec5; } -#footercontainer { -border-radius:20px; -border: 1px solid rgb(99,99,99); -margin-left:-0px!important; -padding-left:-4px!important; + +.message_emoji { + height: 20px; } -#nameplate { -background:transparent!important; -margin-left:10px + +.message_emoji:hover { + cursor: pointer; } -#chatcontent > p > span.chatusername, -#curuser_discrim, -#curuser_name { -display:block + +#chatcontent .message_emoji:hover { + height: 30px; } -#curuser_discrim { -font-size:50%; + +.chatusername { + display: table-header-group; } -#curuser_discrim, -#curuser_name { -margin-top:-2px +.chatmessage { + display: inline; + color: rgb(195, 196, 197); } -#currentuserimage { -margin-top:4px; -margin-right:4px + +p.mentioned { + font-weight: bold; + font-style: italic; } -#chatcontent > p { display: table; } -#chatcontent > p > span { display: table-row } -#chatcontent > p > span.chatusername { display: table-header-group } -#chatcontent > p > span.chatmessage { display: table-footer-group;display:inline-block!important;color:rgb(195,196,197) } -::-webkit-input-placeholder { color:rgb(99,99,99) } -:-moz-placeholder { color:rgb(99,99,99) } -::-moz-placeholder { color:rgb(99,99,99) } -:-ms-input-placeholder { color:rgb(99,99,99) } -::-ms-input-placeholder { color:rgb(99,99,99) } -body > div.navbar-fixed > nav > div { -background:#263238 - background: -webkit-linear-gradient(#263238, #37474f, #455a64); /* For Safari 5.1 to 6.0 */ - background: -o-linear-gradient(#263238, #37474f, #455a64); /* For Opera 11.1 to 12.0 */ - background: -moz-linear-gradient(#263238, #37474f, #455a64); /* For Firefox 3.6 to 15 */ - background: linear-gradient(#263238, #37474f, #455a64); /* Standard syntax */ + +p.mentioned span.chatmessage { + color: #ff5252; } -div.divider { -margin-left:10px; -margin-right:10px; + +.chatmessage code { + background-color: gray; + color: lightgray; + border-radius: 5px; + padding: 2px; } -#discord-members > li > a.subheader, -#members-nav > li:nth-child(1) > a, -#discord-members-count, -#embed-discord-members-count, -#members-nav > li:nth-child(4) > a, -#guest-members-count, -#members-nav > li:nth-child(6) > a { -text-transform: uppercase; + +.chatmessage code.blockcode { + width: 100%; + display: inline-block; + white-space: pre-wrap; + line-height: 15px; + padding: 5px; } -#members-btn > i { visibility:hidden } -#members-btn { -visibility:visible; -background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNiIgaGVpZ2h0PSIyNiI+CiAgPGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgIDxwYXRoIGQ9Ik0xIDFoMjR2MjRIMVYxeiIvPgogICAgPHBhdGggZmlsbD0iI0ZGRiIgZD0iTTE2Ljc3MjA3NTQgMTQuNjc5MTc3OGMtLjIyNDkwOTctLjIyNTY4ODYtLjQ3NDE1NzMtLjQzNTE2ODEtLjc0MjA3NTQtLjYyOTE3NzguMzUtLjAzLjY4LS4wNS45Ny0uMDUgMi4zMyAwIDcgMS4xNyA3IDMuNVYyMGgtNHYtMWMwLTEuODgxNjkxNC0xLjQwNzcwNTYtMy4zMjMwMTQzLTMuMjI3OTI0Ni00LjMyMDgyMjJ6bS0xLjQzODU3MzUtOC4xNzU4NTM0QzE1LjgwOTgwODIgNi4xODUyNTE3MyAxNi4zODI3ODQ1IDYgMTcgNmMxLjY2IDAgMi45OSAxLjM0IDIuOTkgM3MtMS4zMyAzLTIuOTkgM2MtLjYxNzE5MTQgMC0xLjE5MDE0NzEtLjE4NTIzNzMtMS42NjY0NDIzLS41MDMyODcyQzE1Ljc1NzQ4MzIgMTAuNzYyMTQwOTUgMTYgOS45MDk1NTYgMTYgOS4wMDAwNjY5M2MwLS45MDk1Mjg5Ni0uMjQyNTM4MS0xLjc2MjE0ODgtLjY2NjQ5ODEtMi40OTY3NDI1M3pNMTAgMTNjMi4yMSAwIDQtMS43OSA0LTRzLTEuNzktNC00LTQtNCAxLjc5LTQgNCAxLjc5IDQgNCA0em0wIDJjLTIuNjcgMC04IDEuMzQtOCA0djJoMTZ2LTJjMC0yLjY2LTUuMzMtNC04LTR6Ii8+CiAgPC9nPgo8L3N2Zz4=); -background-repeat:no-repeat; -margin-top:18px + +#emoji-picker { + color: black; + position: fixed; + bottom: 12%; + right: 1%; + z-index: 500; + width: 350px; + height: 110px; + max-width: 80%; + max-height: 80%; + background-color: #eceff1; + border-radius: 5px; + display: none; } -.circle:hover { -border-radius:20px; -background: linear-gradient(to right, #f9f9f9 90%, #fff); + +#emoji-picker-content { + overflow: auto; + height: 100%; + padding: 5px; + padding-top: 0; } -#channels-list > li:hover { --webkit-filter: brightness(150%); + +#emoji-picker h6 { + font-weight: bold; } -#loginmodal { + +#emoji-tray-toggle { + position: absolute; + width: 10px; + height: 10px; + top: 14px; + right: 30px; } + +#emoji-tray-toggle > .btn-floating { + width: 30px; + height: 30px; +} + +#emoji-tray-toggle > .btn-floating > i { + line-height: 0; + position: relative; + top: -5px; +} \ No newline at end of file diff --git a/webapp/titanembeds/static/img/partners/everybot.jpg b/webapp/titanembeds/static/img/partners/everybot.jpg new file mode 100644 index 0000000..1e598d3 Binary files /dev/null and b/webapp/titanembeds/static/img/partners/everybot.jpg differ diff --git a/webapp/titanembeds/static/img/partners/lgbtq_lounge.jpg b/webapp/titanembeds/static/img/partners/lgbtq_lounge.jpg new file mode 100644 index 0000000..0e32a59 Binary files /dev/null and b/webapp/titanembeds/static/img/partners/lgbtq_lounge.jpg differ diff --git a/webapp/titanembeds/static/img/partners/ping_salar_emote.png b/webapp/titanembeds/static/img/partners/ping_salar_emote.png new file mode 100644 index 0000000..dffbd9e Binary files /dev/null and b/webapp/titanembeds/static/img/partners/ping_salar_emote.png differ diff --git a/webapp/titanembeds/static/img/partners/reddit_tech.png b/webapp/titanembeds/static/img/partners/reddit_tech.png deleted file mode 100644 index 8e738d8..0000000 Binary files a/webapp/titanembeds/static/img/partners/reddit_tech.png and /dev/null differ diff --git a/webapp/titanembeds/static/img/partners/streamers_connected.png b/webapp/titanembeds/static/img/partners/streamers_connected.png new file mode 100644 index 0000000..e9c4bbd Binary files /dev/null and b/webapp/titanembeds/static/img/partners/streamers_connected.png differ diff --git a/webapp/titanembeds/static/img/people/appledash.png b/webapp/titanembeds/static/img/people/appledash.png new file mode 100644 index 0000000..f20dc1f Binary files /dev/null and b/webapp/titanembeds/static/img/people/appledash.png differ diff --git a/webapp/titanembeds/static/img/people/dotjs.jpg b/webapp/titanembeds/static/img/people/dotjs.jpg new file mode 100644 index 0000000..15c2de9 Binary files /dev/null and b/webapp/titanembeds/static/img/people/dotjs.jpg differ diff --git a/webapp/titanembeds/static/img/people/js.png b/webapp/titanembeds/static/img/people/js.png deleted file mode 100644 index 2235f94..0000000 Binary files a/webapp/titanembeds/static/img/people/js.png and /dev/null differ diff --git a/webapp/titanembeds/static/img/people/selina.png b/webapp/titanembeds/static/img/people/selina.png new file mode 100644 index 0000000..2ae3a16 Binary files /dev/null and b/webapp/titanembeds/static/img/people/selina.png differ diff --git a/webapp/titanembeds/static/img/webhook_comparison.png b/webapp/titanembeds/static/img/webhook_comparison.png new file mode 100644 index 0000000..332edcc Binary files /dev/null and b/webapp/titanembeds/static/img/webhook_comparison.png differ diff --git a/webapp/titanembeds/static/js/admin_cosmetics.js b/webapp/titanembeds/static/js/admin_cosmetics.js new file mode 100644 index 0000000..79a2e1a --- /dev/null +++ b/webapp/titanembeds/static/js/admin_cosmetics.js @@ -0,0 +1,82 @@ +/* global $, Materialize, location */ + +function postForm(user_id, css) { + var funct = $.ajax({ + dataType: "json", + method: "POST", + data: {"user_id": user_id, "css": css} + }); + return funct.promise(); +} + +function deleteForm(user_id) { + var funct = $.ajax({ + dataType: "json", + method: "DELETE", + data: {"user_id": user_id} + }); + return funct.promise(); +} + +function patchForm(user_id, css) { + var funct = $.ajax({ + dataType: "json", + method: "PATCH", + data: {"user_id": user_id, "css": css} + }); + return funct.promise(); +} + +$(function() { + $("#new_submit").click(function () { + var user_id = $("#new_user_id").val(); + if (user_id.length < 1) { + Materialize.toast("The user ID field can't be blank!", 2000); + return; + } + var css_checked = $("#new_css_switch").is(':checked'); + var formPost = postForm(user_id, css_checked); + formPost.done(function (data) { + location.reload(); + }); + formPost.fail(function (data) { + if (data.status == 409) { + Materialize.toast('This user id already exists!', 10000); + } else { + Materialize.toast('Oh no! Something has failed submitting a new entry!', 10000); + } + }); + }); +}); + +function delete_user(user_id) { + var confirmation = confirm("Are you sure that you want to delete user?"); + if (confirmation) { + var formDelete = deleteForm(user_id); + formDelete.done(function (data) { + location.reload(); + }); + formDelete.fail(function (data) { + if (data.status == 409) { + Materialize.toast('This user id does not exists!', 10000); + } else { + Materialize.toast('Oh no! Something has failed deleting this user entry!', 10000); + } + }); + } +} + +function update_css_switch(user_id, element) { + var css_checked = $(element).is(':checked'); + var formPatch = patchForm(user_id, css_checked); + formPatch.done(function (data) { + Materialize.toast('CSS updated!', 10000); + }); + formPatch.fail(function (data) { + if (data.status == 409) { + Materialize.toast('This user id does not exists!', 10000); + } else { + Materialize.toast('Oh no! Something has failed changing the css toggle!', 10000); + } + }); +} \ No newline at end of file diff --git a/webapp/titanembeds/static/js/admin_index.js b/webapp/titanembeds/static/js/admin_index.js new file mode 100644 index 0000000..1a1f11c --- /dev/null +++ b/webapp/titanembeds/static/js/admin_index.js @@ -0,0 +1,30 @@ +/* global $ */ +/* global Materialize */ + +(function () { + function cleanup_database() { + var funct = $.ajax({ + method: "DELETE", + url: "/api/cleanup-db", + }); + return funct.promise(); + } + + $(function(){ + $("#db_cleanup_btn").click(run_cleanup_db); + }); + + function run_cleanup_db() { + $("#db_cleanup_btn").attr("disabled",true); + Materialize.toast('Please wait for the cleanup database task to finish...', 10000); + var cleanupdb = cleanup_database(); + cleanupdb.done(function () { + $("#db_cleanup_btn").attr("disabled",false); + Materialize.toast('Successfully cleaned up the database!', 10000); + }); + cleanupdb.fail(function () { + $("#db_cleanup_btn").attr("disabled",false); + Materialize.toast('Database cleanup failiure.', 10000); + }); + } +})(); \ No newline at end of file diff --git a/webapp/titanembeds/static/js/admin_token_transactions.js b/webapp/titanembeds/static/js/admin_token_transactions.js new file mode 100644 index 0000000..c44c46d --- /dev/null +++ b/webapp/titanembeds/static/js/admin_token_transactions.js @@ -0,0 +1,56 @@ +/* global $, Materialize, location */ + +function postForm(user_id, amount) { + var funct = $.ajax({ + dataType: "json", + method: "POST", + data: {"user_id": user_id, "amount": amount} + }); + return funct.promise(); +} + +function patchForm(user_id, amount) { + var funct = $.ajax({ + dataType: "json", + method: "PATCH", + data: {"user_id": user_id, "amount": amount} + }); + return funct.promise(); +} + +$(function() { + $("#new_submit").click(function () { + var user_id = $("#new_user_id").val(); + var user_token = $("#new_user_token").val(); + if (user_id.length < 1 || user_token.length < 1) { + Materialize.toast("The user ID or balance field can't be blank!", 2000); + return; + } + var formPost = postForm(user_id, user_token); + formPost.done(function (data) { + location.reload(); + }); + formPost.fail(function (data) { + if (data.status == 409) { + Materialize.toast('This user id already exists!', 10000); + } else { + Materialize.toast('Oh no! Something has failed submitting a new entry!', 10000); + } + }); + }); +}); + +function submit_modify_user(user_id) { + var amount = $("#input_"+user_id).val(); + var formPatch = patchForm(user_id, amount); + formPatch.done(function (data) { + location.reload(); + }); + formPatch.fail(function (data) { + if (data.status == 409) { + Materialize.toast('This user id does not exists!', 10000); + } else { + Materialize.toast('Oh no! Something has failed changing the css toggle!', 10000); + } + }); +} \ No newline at end of file diff --git a/webapp/titanembeds/static/js/administrate_guild.js b/webapp/titanembeds/static/js/administrate_guild.js index d689ceb..c189827 100644 --- a/webapp/titanembeds/static/js/administrate_guild.js +++ b/webapp/titanembeds/static/js/administrate_guild.js @@ -7,6 +7,24 @@ $('#unauth_users').change(function() { }); }); +$('#visitor_view').change(function() { + var pathname = window.location.pathname; + var checked = $(this).is(':checked') + var payload = {"visitor_view": checked} + $.post(pathname, payload, function(data) { + Materialize.toast('Updated visitor mode setting!', 2000) + }); +}); + +$('#webhook_messages').change(function() { + var pathname = window.location.pathname; + var checked = $(this).is(':checked') + var payload = {"webhook_messages": checked} + $.post(pathname, payload, function(data) { + Materialize.toast('Updated webhook messages setting!', 2000) + }); +}); + $('#chat_links').change(function() { var pathname = window.location.pathname; var checked = $(this).is(':checked') diff --git a/webapp/titanembeds/static/js/donate.js b/webapp/titanembeds/static/js/donate.js new file mode 100644 index 0000000..4e170b7 --- /dev/null +++ b/webapp/titanembeds/static/js/donate.js @@ -0,0 +1,19 @@ +/* global $ */ +(function () { + $('#token-slider').on('input', function(){ + var slider_value = $("#token-slider").val(); + var multiplier = 100; + + $("#money-display").text(slider_value); + $("#token-display").text(slider_value * multiplier); + }); + + $("#donate-btn").click(function () { + var slider_value = $("#token-slider").val(); + var form = $('
' + + '' + + '
'); + $(document.body).append(form); + form.submit(); + }); +})(); \ No newline at end of file diff --git a/webapp/titanembeds/static/js/embed.js b/webapp/titanembeds/static/js/embed.js index 47cf570..44d5a31 100644 --- a/webapp/titanembeds/static/js/embed.js +++ b/webapp/titanembeds/static/js/embed.js @@ -5,22 +5,31 @@ /* global bot_client_id */ /* global moment */ /* global localStorage */ +/* global visitors_enabled */ +/* global cheet */ +/* global location */ +/* global io */ +/* global twemoji */ (function () { const theme_options = ["DiscordDark", "BetterTitan"]; // All the avaliable theming names var user_def_css; // Saves the user defined css - var has_already_been_focused = false; // keep track of if the embed has initially been focused. + var has_already_been_initially_resized = false; // keep track if the embed initially been resized 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 selected_channel = null; // user selected 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 - var priority_query_guild = false; // So you have selected a channel? Let's populate it. + var emoji_store = []; // all server emojis var current_username_discrim; // Current username/discrim pair, eg EndenDraogn#4151 + var current_user_discord_id; // Current user discord snowflake id, eg mine is 140252024666062848 + var visitor_mode = false; // Keep track of if using the visitor mode or authenticate mode + var socket = null; // Socket.io object + var authenticated_users_list = []; // List of all authenticated users + var unauthenticated_users_list = []; // List of all guest users + var discord_users_list = []; // List of all discord users that are probably online + var guild_channels_list = []; // guild channels, but as a list of them + var shift_pressed = false; // Track down if shift pressed on messagebox function element_in_view(element, fullyInView) { var pageTop = $(window).scrollTop(); @@ -34,11 +43,19 @@ return ((elementTop <= pageBottom) && (elementBottom >= pageTop)); } } + + String.prototype.replaceAll = function(target, replacement) { + return this.split(target).join(replacement); + }; function query_guild() { + var url = "/api/query_guild"; + if (visitor_mode) { + url = url += "_visitor"; + } var funct = $.ajax({ dataType: "json", - url: "/api/query_guild", + url: url, data: {"guild_id": guild_id} }); return funct.promise(); @@ -63,12 +80,26 @@ }); return funct.promise(); } + + function change_unauthenticated_username(username) { + var funct = $.ajax({ + method: "POST", + dataType: "json", + url: "/api/change_unauthenticated_username", + data: {"username": username, "guild_id": guild_id} + }); + return funct.promise(); + } function fetch(channel_id, after=null) { + var url = "/api/fetch"; + if (visitor_mode) { + url += "_visitor"; + } var funct = $.ajax({ method: "GET", dataType: "json", - url: "/api/fetch", + url: url, data: {"guild_id": guild_id,"channel_id": channel_id, "after": after} }); return funct.promise(); @@ -98,15 +129,17 @@ } $('select').material_select(); - $("#focusmodal").modal({ - dismissible: true, - opacity: .5, - inDuration: 400, - outDuration: 400, - startingTop: "4%", - endingTop: "10%", - }); - $("#focusmodal").modal("open"); + + $("#loginmodal").modal({ + dismissible: visitors_enabled, // 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'); $("#userembedmodal").modal({ dismissible: true, opacity: .5, @@ -118,14 +151,58 @@ $("#userembedmodal").modal("open"); }); - $( "#theme-selector" ).change(function() { + $("#visitor_login_btn").click(function () { + $("#loginmodal").modal("open"); + }); + + $("#emoji-tray-toggle").click(function () { + $("#emoji-picker").fadeToggle(); + var offset = $("#emoji-tray-toggle").offset().top; + $("#emoji-picker").offset({"top": offset-120}); + $("#emoji-picker-emojis").html(""); + var template = $('#mustache_message_emoji').html(); + Mustache.parse(template); + for (var i = 0; i < emoji_store.length; i++) { + var emoji = emoji_store[i]; + var rendered = Mustache.render(template, {"id": emoji.id, "name": emoji.name}).trim(); + var jqueryed = $(rendered); + jqueryed.click(function () { + var emote_name = $(this).attr("data-tooltip"); + place_emoji(emote_name); + }); + $("#emoji-picker-emojis").append(jqueryed); + } + $('.tooltipped').tooltip(); + }); + + $("#chatcontent").click(function () { + var emojipck_display = $('#emoji-picker').css('display'); + if (emojipck_display != "none") { + $("#emoji-picker").fadeToggle(); + } + }); + + $("#messagebox").click(function () { + var emojipck_display = $('#emoji-picker').css('display'); + if (emojipck_display != "none") { + $("#emoji-picker").fadeToggle(); + } + }); + + $( "#theme-selector" ).change(function () { var theme = $("#theme-selector option:selected").val(); - changeTheme(theme); + var keep_custom_css = $("#overwrite_theme_custom_css_checkbox").is(':checked'); + changeTheme(theme, keep_custom_css); + }); + + $("#overwrite_theme_custom_css_checkbox").change(function () { + var keep_custom_css = $("#overwrite_theme_custom_css_checkbox").is(':checked'); + changeTheme(null, keep_custom_css); }); var themeparam = getParameterByName('theme'); var localstore_theme = localStorage.getItem("theme"); - if ((getParameterByName("css") == null) && ((themeparam && $.inArray(themeparam, theme_options) != -1) || (localstore_theme))) { + if ((themeparam && $.inArray(themeparam, theme_options) != -1) || (localstore_theme)) { var theme; if (themeparam) { theme = themeparam; @@ -137,27 +214,42 @@ $("#theme-selector option[value=" + theme + "]").attr('selected', 'selected'); $('select').material_select(); } - - if (document.hasFocus()) { - primeEmbed(); - } - $(window).focus(function() { - if (!has_already_been_focused) { - primeEmbed(); + var dembed = discord_embed(); + dembed.done(function (data) { + $("#modal_invite_btn").attr("href", data.instant_invite); + }); + + $(window).resize(function(){ + // For those who decides to hide the embed at first load (display: none), resulting in the messages being not scrolled down. + if (!has_already_been_initially_resized) { + has_already_been_initially_resized = true; + $("html, body").animate({ scrollTop: $(document).height() }, "fast"); } }); + + primeEmbed(); + setInterval(send_socket_heartbeat, 5000); + if (getParameterByName("username")) { + $("#custom_username_field").val(getParameterByName("username")); + } }); - function changeTheme(theme) { + function changeTheme(theme=null, keep_custom_css=true) { if (theme == "") { $("#css-theme").attr("href", ""); $("#user-defined-css").text(user_def_css); localStorage.removeItem("theme"); - } else if ($.inArray(theme, theme_options) != -1) { - $("#user-defined-css").text(""); - $("#css-theme").attr("href", "/static/themes/" + theme + "/css/style.css"); - localStorage.setItem("theme", theme); + } else if ($.inArray(theme, theme_options) != -1 || theme == null) { + if (!keep_custom_css) { + $("#user-defined-css").text(""); + } else { + $("#user-defined-css").text(user_def_css); + } + if (theme) { + $("#css-theme").attr("href", "/static/themes/" + theme + "/css/style.css"); + localStorage.setItem("theme", theme); + } } } @@ -171,31 +263,39 @@ if (!results[2]) return ''; return decodeURIComponent(results[2].replace(/\+/g, " ")); } + + function setVisitorMode(enabled) { + if (!visitors_enabled) { + return; + } + visitor_mode = enabled; + if (visitor_mode) { + $("#visitor_mode_message").show(); + $("#messagebox").hide(); + $("#emoji-tray-toggle").hide(); + } else { + $("#visitor_mode_message").hide(); + $("#messagebox").show(); + $("#emoji-tray-toggle").show(); + } + } function primeEmbed() { - $("#focusmodal").modal("close"); - has_already_been_focused = true; - - var dembed = discord_embed(); - dembed.done(function (data) { - $("#modal_invite_btn").attr("href", data.instant_invite); - }); - - $("#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(); + if (visitors_enabled) { + setVisitorMode(true); + var guild2 = query_guild(); + guild2.done(function(data) { + initialize_embed(data); + }); + guild2.fail(function() { + setVisitorMode(false); + }); + } }); guild.done(function(data) { @@ -220,33 +320,59 @@ } function initialize_embed(guildobj) { + if (socket) { + socket.disconnect(); + socket = null; + } if (guildobj === undefined) { var guild = query_guild(); guild.done(function(data) { + switch_to_default_channel(data.channels); prepare_guild(data); $('#loginmodal').modal('close'); unlock_login_fields(); }); } else { + switch_to_default_channel(guildobj.channels); prepare_guild(guildobj); $('#loginmodal').modal('close'); unlock_login_fields(); } } + + function switch_to_default_channel(guildchannels) { + var defaultChannel = getParameterByName("defaultchannel"); + if (!defaultChannel) { + return; + } + for (var i = 0; i < guildchannels.length; i++) { + if (guildchannels[i].channel.id == defaultChannel) { + if (!guildchannels[i].read) { + return; + } + selected_channel = defaultChannel; + return; + } + } + } function prepare_guild(guildobj) { + emoji_store = guildobj.emojis; fill_channels(guildobj.channels); fill_discord_members(guildobj.discordmembers); fill_authenticated_users(guildobj.embedmembers.authenticated); fill_unauthenticated_users(guildobj.embedmembers.unauthenticated); $("#instant-inv").attr("href", guildobj.instant_invite); run_fetch_routine(); + initiate_websockets(); } function fill_channels(channels) { + guild_channels_list = channels; var template = $('#mustache_channellistings').html(); Mustache.parse(template); $("#channels-list").empty(); + var curr_default_channel = selected_channel; for (var i = 0; i < channels.length; i++) { var chan = channels[i]; guild_channels[chan.channel.id] = chan; @@ -256,18 +382,23 @@ $("#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); + if (!selected_channel && (!curr_default_channel || chan.channel.position < curr_default_channel.channel.position)) { + curr_default_channel = chan; } } } + if (typeof curr_default_channel == "object") { + selected_channel = curr_default_channel.channel.id; + } + var this_channel = guild_channels[selected_channel]; + if (this_channel.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(this_channel.channel.topic); $("#channel-"+selected_channel).parent().addClass("active"); } @@ -278,8 +409,20 @@ $("#messagebox").focus(); } } + + function place_emoji(emoji_name) { + if (!$('#messagebox').prop('disabled')) { + $('#messagebox').val( $('#messagebox').val() + emoji_name + " " ); + $("#messagebox").focus(); + } + var emojipck_display = $('#emoji-picker').css('display'); + if (emojipck_display != "none") { + $("#emoji-picker").fadeToggle(); + } + } function fill_discord_members(discordmembers) { + discord_users_list = discordmembers; var template = $('#mustache_authedusers').html(); Mustache.parse(template); $("#discord-members").empty(); @@ -325,9 +468,28 @@ var rendered_role = Mustache.render(template_role, {"name": roleobj["name"] + " - " + roleobj["members"].length}); discordmembercnt += roleobj["members"].length; $("#discord-members").append(rendered_role); + roleobj.members.sort(function(a, b){ + var name_a = a.username; + var name_b = b.username; + if (a.nick) { + name_a = a.nick; + } + if (b.nick) { + name_b = b.nick; + } + name_a = name_a.toUpperCase(); + name_b = name_b.toUpperCase(); + if(name_a < name_b) return -1; + if(name_a > name_b) return 1; + return 0; + }); 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}); + var member_name = member.nick; + if (!member_name) { + member_name = member.username; + } + var rendered_user = Mustache.render(template_user, {"id": member.id.toString() + "d", "username": member_name, "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); @@ -347,12 +509,17 @@ $("#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}); + var username = member.username; + if (member.nickname) { + username = member.nickname; + } + var rendered = Mustache.render(template, {"id": member.id.toString() + "a", "username": 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); }); } + authenticated_users_list = users; } function fill_unauthenticated_users(users) { @@ -365,6 +532,7 @@ var rendered = Mustache.render(template, {"username": member.username, "discriminator": member.discriminator}); $("#embed-unauth-users").append(rendered); } + unauthenticated_users_list = users; } function wait_for_discord_login() { @@ -375,12 +543,14 @@ setTimeout(function() { var usr = create_authenticated_user(); usr.done(function(data) { + setVisitorMode(false); initialize_embed(); return; }); usr.fail(function(data) { if (data.status == 403) { Materialize.toast('Authentication error! You have been banned.', 10000); + setVisitorMode(true); } else if (index < 10) { _wait_for_discord_login(index + 1); } @@ -394,8 +564,6 @@ last_message_id = null; $("#channels-list > li.active").removeClass("active"); $("#channel-"+selected_channel).parent().addClass("active"); - priority_query_guild = true; - clearTimeout(fetchtimeout); run_fetch_routine(); } } @@ -404,8 +572,12 @@ 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); + var username = mention.username; + if (mention.nickname) { + username = mention.nickname; + } + message.content = message.content.replace(new RegExp("<@" + mention.id + ">", 'g'), "@" + username + "#" + mention.discriminator); + message.content = message.content.replace(new RegExp("<@!" + mention.id + ">", 'g'), "@" + username + "#" + mention.discriminator); message.content = message.content.replace("<@&" + guild_id + ">", "@everyone"); } return message; @@ -418,9 +590,22 @@ 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); + if (message.content.startsWith("(Titan Dev) ")) { + message.content = message.content.substring(usernamefield.length + 18); + } else { + message.content = message.content.substring(usernamefield.length + 7); + } message.author.username = usernamefield.split("#")[0]; message.author.discriminator = usernamefield.split("#")[1]; + } else if (message.author.bot && message.author.discriminator == "0000" && message.author.username.substring(message.author.username.length-5, message.author.username.length-4) == "#") { + var namestr = message.author.username; + if (message.content.startsWith("(Titan Dev) ")) { + message.author.username = "(Titan Dev) " + namestr.substring(0,namestr.length-5); + message.content = message.content.substring(11); + } else { + message.author.username = namestr.substring(0,namestr.length-5); + } + message.author.discriminator = namestr.substring(namestr.length-4); } return message; } @@ -449,9 +634,8 @@ var lastmsg = $("#chatcontent p:last-child"); var content = lastmsg.text().toLowerCase(); var username_discrim = current_username_discrim.toLowerCase(); - if (content.includes("@everyone") || content.includes("@" + username_discrim)) { - lastmsg.css( "color", "#ff5252" ); - lastmsg.css( "font-weight", "bold" ); + if (content.includes("@everyone") || content.includes("@here") || content.includes("@" + username_discrim)) { + lastmsg.addClass( "mentioned" ); } } @@ -477,8 +661,45 @@ } return message; } + + function parse_emoji_in_message(message) { + var template = $('#mustache_message_emoji').html(); + Mustache.parse(template); + for (var i = 0; i < emoji_store.length; i++) { + var emoji = emoji_store[i]; + var emoji_format = "<:" + emoji.name + ":" + emoji.id + ">"; + var rendered = Mustache.render(template, {"id": emoji.id, "name": emoji.name}).trim(); + message.content = message.content.replaceAll(emoji_format, rendered); + } + var rendered = Mustache.render(template, {"id": "$2", "name": "$1"}).trim(); + message.content = message.content.replace(/<:(.*?):(.*?)>/g, rendered); + message.content = twemoji.parse(message.content, { + className: "message_emoji", + callback: function(icon, options, variant) { // exclude special characters + switch (icon) { + case 'a9': // © copyright + case 'ae': // ® registered trademark + case '2122': // ™ trademark + return false; + } + return ''.concat(options.base, options.size, '/', icon, options.ext); + } + }); + return message; + } + + function parse_message_markdown(text) { + text = text.replace(/\*\*(.*?)\*\*/g, "$1"); + text = text.replace(/\*(.*?)\*/g, "$1"); + text = text.replace(/__(.*?)__/g, "$1"); + text = text.replace(/_(.*?)_/g, "$1"); + text = text.replace(/~~(.*?)~~/g, "$1"); + text = text.replace(/\`\`\`([^]+)\`\`\`/g, "$1"); + text = text.replace(/\`(.*?)\`/g, "$1"); + return text; + } - function fill_discord_messages(messages, jumpscroll) { + function fill_discord_messages(messages, jumpscroll, replace=null) { if (messages.length == 0) { return last_message_id; } @@ -492,24 +713,37 @@ 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); + message.content = message.content.replaceAll("\\<", "<"); + message.content = message.content.replaceAll("\\>", ">"); + message.content = escapeHtml(message.content); + message.content = parse_message_markdown(message.content); + message = parse_emoji_in_message(message); + var username = message.author.username; + if (message.author.nickname) { + username = message.author.nickname; + } + var rendered = Mustache.render(template, {"id": message.id, "full_timestamp": message.formatted_timestamp, "time": message.formatted_time, "username": username, "discriminator": message.author.discriminator, "content": nl2br(message.content)}); + if (replace == null) { + $("#chatcontent").append(rendered); + handle_last_message_mention(); + $("#chatcontent p:last-child").find(".blockcode").find("br").remove(); // Remove excessive breaks in codeblocks + } else { + replace.html($(rendered).html()); + replace.find(".blockcode").find("br").remove(); + } last = message.id; - handle_last_message_mention(); } - $("html, body").animate({ scrollTop: $(document).height() }, "slow"); + if (replace == null) { + $("html, body").animate({ scrollTop: $(document).height() }, "slow"); + } $('#chatcontent').linkify({ target: "_blank" }); + $('.tooltipped').tooltip(); 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; @@ -524,27 +758,28 @@ } fet.done(function(data) { var status = data.status; - update_embed_userchip(status.authenticated, status.avatar, status.username, status.user_id, status.discriminator); + if (visitor_mode) { + update_embed_userchip(false, null, "Titan", null, "0001", null); + update_change_username_modal(); + } else { + update_embed_userchip(status.authenticated, status.avatar, status.username, status.nickname, status.user_id, status.discriminator); + update_change_username_modal(status.authenticated, status.username); + current_user_discord_id = status.user_id; + } last_message_id = fill_discord_messages(data.messages, jumpscroll); - if (status.manage_embed) { + if (!visitor_mode && status.manage_embed) { $("#administrate_link").show(); } else { $("#administrate_link").hide(); } - if (times_fetched % 10 == 0 || priority_query_guild) { - var guild = query_guild(); - guild.done(function(guildobj) { - priority_query_guild = false; - fill_channels(guildobj.channels); - fill_discord_members(guildobj.discordmembers); - fill_authenticated_users(guildobj.embedmembers.authenticated); - fill_unauthenticated_users(guildobj.embedmembers.unauthenticated); - $("#instant-inv").attr("href", guildobj.instant_invite); - fetchtimeout = setTimeout(run_fetch_routine, 5000); - }); - } else { - fetchtimeout = setTimeout(run_fetch_routine, 5000); - } + 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); + $("#instant-inv").attr("href", guildobj.instant_invite); + }); }); fet.fail(function(data) { if (data.status == 403) { @@ -554,34 +789,44 @@ $('#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); - } + setVisitorMode(true); }); fet.always(function() { - currently_fetching = false; $("#fetching-indicator").fadeOut(800); }); } - function update_embed_userchip(authenticated, avatar, username, userid, discrim=null) { + function update_embed_userchip(authenticated, avatar, username, nickname, userid, discrim=null) { if (authenticated) { $("#currentuserimage").show(); $("#currentuserimage").attr("src", avatar); $("#curuser_name").text(username); $("#curuser_discrim").text("#" + discrim); - current_username_discrim = username + "#" + discrim; + current_username_discrim = "#" + discrim; } else { $("#currentuserimage").hide(); $("#curuser_name").text(username); $("#curuser_discrim").text("#" + userid); - current_username_discrim = username + "#" + userid; + current_username_discrim = "#" + userid; + } + if (nickname) { + $("#curuser_name").text(nickname); + current_username_discrim = nickname + current_username_discrim; + } else { + current_username_discrim = username + current_username_discrim; + } + } + + function update_change_username_modal(authenticated=false, username=null) { + if (!$("#change_username_field") || $("#change_username_field").is(":focus")) { + return; + } + if (authenticated || visitor_mode) { + $("#change_username_field").attr("disabled", true); + $("#change_username_field").val(""); + } else { + $("#change_username_field").attr("disabled", false); + $("#change_username_field").val(username); } } @@ -600,6 +845,7 @@ lock_login_fields(); var usr = create_unauthenticated_user($(this).val()); usr.done(function(data) { + setVisitorMode(false); initialize_embed(); }); usr.fail(function(data) { @@ -611,24 +857,65 @@ Materialize.toast('Illegal username provided! Only alphanumeric, spaces, dashes, and underscores allowed in usernames.', 10000); } unlock_login_fields(); + setVisitorMode(true); }); } } }); + + $("#change_username_field").keyup(function(event){ + if (event.keyCode == 13) { + $(this).blur(); + 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) && $("#curuser_name").text() != $(this).val()) { + var usr = change_unauthenticated_username($(this).val()); + usr.done(function(data) { + Materialize.toast('Username changed successfully!', 10000); + if (socket) { + run_fetch_routine(); + socket.disconnect(); + socket = null; + } + initiate_websockets(); + }); + usr.fail(function(data) { + if (data.status == 429) { + Materialize.toast('Sorry! You are allowed to change your username 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); + } else { + Materialize.toast('Something unexpected happened! Error code of ' + data.status, 10000); + } + }); + } + } + }); + + $("#messagebox").keyup(function (event) { + if (event.keyCode == 16) { + shift_pressed = false; + } + }); - $("#messagebox").keyup(function(event){ + $("#messagebox").keydown(function(event){ if ($(this).val().length == 1) { $(this).val($.trim($(this).val())); } - if(event.keyCode == 13 && $(this).val().length >= 1 && $(this).val().length <= 350) { + if (event.keyCode == 16) { + shift_pressed = true; + } + if(event.keyCode == 13 && !shift_pressed && $(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); @@ -640,11 +927,12 @@ }); funct.catch(function(data) { if (data.status == 429) { - Materialize.toast('You are sending messages too fast! 1 message per 10 seconds', 10000); + Materialize.toast('You are sending messages too fast! 1 message per 5 seconds', 10000); } }); funct.always(function() { $("#messagebox").attr('readonly', false); + $("#messagebox").focus(); }); } }); @@ -663,4 +951,208 @@ draggable: true // Choose whether you can drag to open on touch screens } ); + + // enter konami code into the embed page for some ponies action! + cheet('↑ ↑ ↓ ↓ ← → ← → b a', function () { + // basically copied and pasted of browser ponies bookmarklet + (function (srcs,cfg) { var cbcount = 1; var callback = function () { -- cbcount; if (cbcount === 0) { BrowserPonies.setBaseUrl(cfg.baseurl); if (!BrowserPoniesBaseConfig.loaded) { BrowserPonies.loadConfig(BrowserPoniesBaseConfig); BrowserPoniesBaseConfig.loaded = true; } BrowserPonies.loadConfig(cfg); if (!BrowserPonies.running()) BrowserPonies.start(); } }; if (typeof(BrowserPoniesConfig) === "undefined") { window.BrowserPoniesConfig = {}; } if (typeof(BrowserPoniesBaseConfig) === "undefined") { ++ cbcount; BrowserPoniesConfig.onbasecfg = callback; } if (typeof(BrowserPonies) === "undefined") { ++ cbcount; BrowserPoniesConfig.oninit = callback; } var node = (document.body || document.documentElement || document.getElementsByTagName('head')[0]); for (var id in srcs) { if (document.getElementById(id)) continue; if (node) { var s = document.createElement('script'); s.type = 'text/javascript'; s.id = id; s.src = srcs[id]; node.appendChild(s); } else { document.write('\u003cscript type="text/javscript" src="'+ srcs[id]+'" id="'+id+'"\u003e\u003c/script\u003e'); } } callback();})({"browser-ponies-script":"https://panzi.github.io/Browser-Ponies/browserponies.js","browser-ponies-config":"https://panzi.github.io/Browser-Ponies/basecfg.js"},{"baseurl":"https://panzi.github.io/Browser-Ponies/","fadeDuration":500,"volume":1,"fps":25,"speed":3,"audioEnabled":false,"showFps":false,"showLoadProgress":true,"speakProbability":0.1,"spawn":{"applejack":1,"fluttershy":1,"pinkie pie":1,"rainbow dash":1,"rarity":1,"twilight sparkle":1}}); + }); + + function initiate_websockets() { + if (socket) { + return; + } + + socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + "/gateway", {path: '/gateway', transports: ['websocket']}); + socket.on('connect', function () { + socket.emit('identify', {"guild_id": guild_id, "visitor_mode": visitor_mode}); + }); + + socket.on("disconnect", function () { + + }); + + socket.on("revoke", function () { + socket.disconnect(); + socket = null; + $('#loginmodal').modal('open'); + primeEmbed(); + Materialize.toast('Authentication error! You have been disconnected by the server.', 10000); + }); + + socket.on("embed_user_connect", function (msg) { + if (msg.unauthenticated) { + for (var i = 0; i < unauthenticated_users_list.length; i++) { + var item = unauthenticated_users_list[i]; + if (item.username == msg.username && item.discriminator == msg.discriminator) { + return; + } + } + unauthenticated_users_list.push(msg); + fill_unauthenticated_users(unauthenticated_users_list); + } else { + for (var i = 0; i < authenticated_users_list.length; i++) { + var item = authenticated_users_list[i]; + if (item.id == msg.id) { + return; + } + } + authenticated_users_list.push(msg); + fill_authenticated_users(authenticated_users_list); + } + }); + + socket.on("embed_user_disconnect", function (msg) { + if (msg.unauthenticated) { + for (var i = 0; i < unauthenticated_users_list.length; i++) { + var item = unauthenticated_users_list[i]; + if (item.username == msg.username && item.discriminator == msg.discriminator) { + unauthenticated_users_list.splice(i, 1); + fill_unauthenticated_users(unauthenticated_users_list); + return; + } + } + } else { + for (var i = 0; i < authenticated_users_list.length; i++) { + var item = authenticated_users_list[i]; + if (item.id == msg.id) { + authenticated_users_list.splice(i, 1); + fill_authenticated_users(authenticated_users_list); + return; + } + } + } + }); + + socket.on("MESSAGE_CREATE", function (msg) { + var thismsgchan = msg.channel_id; + if (selected_channel != thismsgchan) { + return; + } + var jumpscroll = element_in_view($('#discordmessage_'+last_message_id), true); + last_message_id = fill_discord_messages([msg], jumpscroll); + }); + + socket.on("MESSAGE_DELETE", function (msg) { + var msgchan = msg.channel_id; + if (selected_channel != msgchan) { + return; + } + $("#discordmessage_"+msg.id).parent().remove(); + last_message_id = $("#chatcontent").find("[id^=discordmessage_]").last().attr('id').substring(15); + }); + + socket.on("MESSAGE_UPDATE", function (msg) { + var msgelem = $("#discordmessage_"+msg.id); + if (msgelem.length == 0) { + return; + } + var msgelem_parent = msgelem.parent(); + fill_discord_messages([msg], false, msgelem_parent); + }); + + socket.on("GUILD_MEMBER_ADD", function (usr) { + if (usr.status != "offline") { + discord_users_list.push(usr); + fill_discord_members(discord_users_list); + } + }); + + socket.on("GUILD_MEMBER_UPDATE", function (usr) { + if (usr.id == current_user_discord_id) { + update_socket_channels(); + socket.emit("current_user_info", {"guild_id": guild_id}); + } + for (var i = 0; i < discord_users_list.length; i++) { + if (usr.id == discord_users_list[i].id) { + discord_users_list.splice(i, 1); + if (usr.status != "offline") { + discord_users_list.push(usr); + } + fill_discord_members(discord_users_list); + return; + } + } + discord_users_list.push(usr); + fill_discord_members(discord_users_list); + }); + + socket.on("GUILD_MEMBER_REMOVE", function (usr) { + for (var i = 0; i < discord_users_list.length; i++) { + if (usr.id == discord_users_list[i].id) { + discord_users_list.splice(i, 1); + fill_discord_members(discord_users_list); + return; + } + } + }); + + socket.on("GUILD_EMOJIS_UPDATE", function (emo) { + emoji_store = emo; + }); + + socket.on("GUILD_UPDATE", function (guil) { + $("#guild_name").text(guil.name); + if (guil.icon) { + $("#guild_icon").attr("src", guil.icon_url); + $("#guild_icon").show(); + } else { + $("#guild_icon").hide(); + } + }); + + socket.on("CHANNEL_DELETE", function (chan) { + for (var i = 0; i < guild_channels_list.length; i++) { + var thatchannel = guild_channels_list[i]; + if (thatchannel.channel.id == chan.id) { + guild_channels_list.splice(i, 1); + fill_channels(guild_channels_list); + return; + } + } + }); + + socket.on("CHANNEL_UPDATE", function (chan) { + update_socket_channels(); + }); + + socket.on("CHANNEL_CREATE", function (chan) { + update_socket_channels(); + }); + + socket.on("GUILD_ROLE_UPDATE", function (chan) { + update_socket_channels(); + }); + + socket.on("GUILD_ROLE_DELETE", function (chan) { + update_socket_channels(); + }); + + socket.on("channel_list", function (chans) { + fill_channels(chans); + for (var i = 0; i < chans.length; i++) { + var thischan = chans[i]; + if (thischan.channel.id == selected_channel) { + $("#channeltopic").text(thischan.channel.topic); + } + } + }); + + socket.on("current_user_info", function (usr) { + update_embed_userchip(true, usr.avatar, usr.username, usr.nickname, usr.userid, usr.discriminator); + }); + } + + function update_socket_channels() { + if (!socket) { + return; + } + socket.emit("channel_list", {"guild_id": guild_id, "visitor_mode": visitor_mode}); + } + + function send_socket_heartbeat() { + if (socket) { + socket.emit("heartbeat", {"guild_id": guild_id, "visitor_mode": visitor_mode}); + } + } })(); diff --git a/webapp/titanembeds/templates/about.html.j2 b/webapp/titanembeds/templates/about.html.j2 index 161ed24..492e682 100644 --- a/webapp/titanembeds/templates/about.html.j2 +++ b/webapp/titanembeds/templates/about.html.j2 @@ -1,5 +1,5 @@ {% extends 'site_layout.html.j2' %} -{% block title %}About Titan{% endblock %} +{% set title="About Titan" %} {% block content %}

In the beginning, there was silence.

@@ -40,34 +40,19 @@ used to replace Discord itself. (that's what the mobile apps are for!) It is used in conjunction for a quick and dirty Discord embed for websites. Some uses include forum shoutboxes, etc.

-

Commands

-
-
-
-
-
-

All commands start by mentioning the bot user, @Titan.

-

Guest User Moderation

-

All guest users are denoted by square brackets around their username in the Discord channel, when sending messages.

-
    -
  • ban <username-query>[#<discriminator>]
    Bans the user by the username. The username does not need to be the full string. The discriminator is optional.
    Eg: ban Titan#0001
  • -
  • kick <username-query>[#<discriminator>]
    Kicks the user by the username. The username does not need to be the full string. The discriminator is optional.
    Eg: kick Titan#0001
  • -
-
-
-
-
-
+{% include 'card_commands.html.j2' %} + +{% include 'card_queryparams.html.j2' %}

Chat with us!

- +

Cool People

Keep in mind, this project is not complete without these awesome people!

-
-
+
+
@@ -82,7 +67,7 @@ etc.

-
+
@@ -90,44 +75,74 @@ etc.

iAmMaffie_
-

Developer

-

A mysterious guy, he helped out Titan with his Python coding skills, he helped primarely with the Emoji parsers and the Discord Bot commands system.

+

Head Developer

+

Have you heard of JustMaffie? He has done quite some developing for Titan.

-
+
- Riva + AppleDash +
+
+
AppleDash
+

Server Hosting

+

From the shadows of Poniverse, AppleDash swiftly jumps in and offered his server to make websockets possible!

+
+
+
+
+ +
+
+
+
+ Selina +
+
+
Selina
+

Server Witch

+

This gal manages our server and figures out how to keep the hamsters in the basement in line! Without her they might all run away!

+
+
+
+
+ +
+
+
+
+ Riva
Riva
-

Bot Hosting

-

This guy hosts our lovely bot, without him Titan would probably not exist.

+

Previous Bot Hosting

+

This guy hosts our lovely bot, without him Titan would probably not been that great (ahem, indefinitely offline bot).

- -
+ +
- .JS + dotJS
-
.JS
-

Embed Theme Architect

-

.JS didn't make the designs in JavaScript, he helped out Titan with his CSS skillz.

+
dotJS
+

CSS Architect

+

Although been well known for his name of JavaScript, he helped us design the embeds with his CSS skillz.

- -
+ +
@@ -135,8 +150,8 @@ etc.

Semic
-

Logo/Asset Designer

-

Semic made most of our graphics such as the Logo and moooooore.

+

Logo Designer

+

From our friends over at ProCord, Semic made most of our awesome and heroic Titan logo.

@@ -181,22 +196,88 @@ etc.

-
- +
-
Reddit Tech
-

Reddit Technology Discussions

-

Are you interested in technology? Want to discuss your PC build with others, or anything else tech related? Just want to hang out with fellow PCMasterRace people? Then Reddit Tech is the place for you!

- Discord Server +
Everybot
+

Multipurpose Bot

+

Hey, thank you for reading this, Everybot is a Discord Bot created by JustMaffie, we aim to bring fun and moderation to Discord servers using our commands. + We also want to provide a nice community at our Discord Server.

+ Discord Server + Bot Invite +
+
+
+
+
+
+
+
+
+
+ +
+
+
Ping and Salar's Emote List
+

Discord Global Emojis Listing

+

Do you want to use global emotes, but can't afford Nitro? Well look no further! The Discord Universe contains a few servers, which have emotes you can use EVERYWHERE! We're a community about discovering those servers, and sharing them with you! All you have to do is click the link below and go to the #links channel! From there, you can select the servers you wish to get emotes from and use them on all the servers you want! AND IT'S FREE!

+ Discord Server +
+
+
+
+
+
+
+
+
+
+ +
+
+
Streamers Connected
+

Community for all Content Creators, Alike

+

Streamers Connected is a community for content creators across all platforms of any genre and size. We strive to provide a place for creators to network, grow, and find any resources they may need to produce the best content they can. +We want to see all of our members rise to their potential and find success in their passion to create and entertain. We work with large and small developers to bring our streamers games at discounted prices, and fund giveaways.

+ Website + Discord Server +
+
+
+
+
+
+
+
+
+
+ +
+
+
LGBTQ+
+

A Welcoming Community for People of All Sexuality

+

LGBTQ+ Lounge is a community that welcomes all, regardless of gender or sexuality, come to our server to enjoy yourself and make new friends and involve yourself with some of the events and activities that the staff will organise.

+ Discord Server
{% endblock %} +{% block script %} + + + +{% endblock %} diff --git a/webapp/titanembeds/templates/add_bot.html.j2 b/webapp/titanembeds/templates/add_bot.html.j2 index 7aad8e3..a51b16b 100644 --- a/webapp/titanembeds/templates/add_bot.html.j2 +++ b/webapp/titanembeds/templates/add_bot.html.j2 @@ -1,10 +1,10 @@ {% extends 'site_layout.html.j2' %} -{% block title %}Adding bot to server{% endblock %} +{% set title="Adding bot to server" %} {% block content %}

Would you like to invite Titan to your server?

-

Please keep in mind that Titan currently requires Administrator permissions to function.
-This is strictly enforced for the bot to send messages to all the channels, read role permissions, access banned users list, etc.

+

Please keep in mind that Titan works best with Administrator permissions to function.
+However this is not required. For those who do not want to give Titan Administrator, we've handpicked the permissions to give Titan for the best experience.

@@ -32,8 +32,7 @@ This is strictly enforced for the bot to send messages to all the channels, read

Step 2

Oops!
-

There seems to be a problem processing the invite.
- Please make sure that the bot is given Administrator permission in the server.

+

There seems to be a problem processing the invite.


You may try adding the bot to the server again, or report the bug on our Discord server.

diff --git a/webapp/titanembeds/templates/admin_cosmetics.html.j2 b/webapp/titanembeds/templates/admin_cosmetics.html.j2 new file mode 100644 index 0000000..96a68bd --- /dev/null +++ b/webapp/titanembeds/templates/admin_cosmetics.html.j2 @@ -0,0 +1,80 @@ +{% extends 'site_layout.html.j2' %} +{% set title="Editing User Cosmetics Privilages" %} + +{% block content %} +

Administrating User Cosmetics Privilages

+ +
+
+
+

New Entry

+ + + + + + + + + + + + + + + +
User IDCSSSubmit
+
+ +
+
+
+ +
+
+ Submit +
+
+
+ +
+
+ + + + + + + + + + {% for cosmetic in cosmetics %} + + + + + + {% endfor %} + +
RemoveUser IDCSS
Remove{{ cosmetic.user_id }} +
+ +
+
+
+
+
+{% endblock %} +{% block script %} + +{% endblock %} diff --git a/webapp/titanembeds/templates/admin_guilds.html.j2 b/webapp/titanembeds/templates/admin_guilds.html.j2 new file mode 100644 index 0000000..b9c0d1b --- /dev/null +++ b/webapp/titanembeds/templates/admin_guilds.html.j2 @@ -0,0 +1,32 @@ +{% extends 'site_layout.html.j2' %} +{% set title="Manage Guilds as Administrator" %} + +{% block content %} +

Manage Guilds (Admin)

+

Select a server to configure Titan Embeds. (Total server count: {{ servers|length }})

+
+ {% for server in servers %} +
+
+
+
+ {% if server.icon %} + + {% else %} + No icon :( + {% endif %} +
+
+ +

{{ server.name }}

+
+ Modify +
+
+
+
+
+ {% endfor %} +
+ +{% endblock %} diff --git a/webapp/titanembeds/templates/admin_index.html.j2 b/webapp/titanembeds/templates/admin_index.html.j2 new file mode 100644 index 0000000..55f76db --- /dev/null +++ b/webapp/titanembeds/templates/admin_index.html.j2 @@ -0,0 +1,41 @@ +{% extends 'site_layout.html.j2' %} +{% set title="Admin" %} + +{% block content %} +

Administrate Titan Embeds

+

Select an action.

+ +
+
+
+

Cosmetics

+

Give or revoke special cosmetics privilages for users.

+ Manage +
+
+
+
+

Guilds

+

Manage any guild in Titan, this will give you the normal dashboard.

+ Manage +
+
+
+
+

Titan Tokens

+

View transactions and modify Titan Tokens per user.

+ Manage +
+
+
+
+

Run a Database Cleanup

+

Clears the keyval caches and purges the old messages. (Hit once, and wait a minute)

+ Run DB Cleanup Task +
+
+
+{% endblock %} +{% block script %} + +{% endblock %} \ No newline at end of file diff --git a/webapp/titanembeds/templates/admin_token_transactions.html.j2 b/webapp/titanembeds/templates/admin_token_transactions.html.j2 new file mode 100644 index 0000000..c81d779 --- /dev/null +++ b/webapp/titanembeds/templates/admin_token_transactions.html.j2 @@ -0,0 +1,105 @@ +{% extends 'site_layout.html.j2' %} +{% set title="Editing User Titan Tokens" %} + +{% block content %} +

Administrating Titan Tokens

+ +
+
+
+

New Entry

+ + + + + + + + + + + + + + + +
User IDStarting BalanceSubmit
+
+ +
+
+
+ +
+
+ Submit +
+
+
+
+
+

View Transactions and Modify User Tokens

+
    + {% for don in donators %} +
  • +
    {{ don.user_id }}
    +
    + + + + + + + + + + + + + + +
    Modify AmountSubmit
    +
    + +
    +

    (Place a subtract sign in the front to remove tokens. Otherwise, it will add the amount)

    +
    + Submit +
    + +

    Balance: {{ don.tokens }} Tokens

    + + + + + + + + + + + + + {% for trans in don.transactions %} + + + + + + + + + {% endfor %} + +
    Trans #TimestampActionChangeStarting BalEnding Bal
    {{ trans.id }}{{ trans.timestamp }}{{ trans.action }}.{{ trans.net_tokens }}{{ trans.start_tokens }}{{ trans.end_tokens }}
    +
    +
  • + {% endfor %} +
+
+
+
+{% endblock %} +{% block script %} + +{% endblock %} diff --git a/webapp/titanembeds/templates/administrate_guild.html.j2 b/webapp/titanembeds/templates/administrate_guild.html.j2 index 438a08d..2adb547 100644 --- a/webapp/titanembeds/templates/administrate_guild.html.j2 +++ b/webapp/titanembeds/templates/administrate_guild.html.j2 @@ -1,5 +1,5 @@ {% extends 'site_layout.html.j2' %} -{% block title %}Administrate Guild: {{ guild['name'] }}{% endblock %} +{% set title="Administrate Guild: " + guild['name'] %} {% block additional_head_elements %} @@ -48,6 +48,32 @@ Enable
+ +
+ +

Toggle Visitor Mode

+

Visitors are able to view the channels that @everyone has access to. However, they are not able to send messages until they login using the usual methods.

+
+ +
+ +
+ +

Toggle Webhooks Messages

+

Instead of sending user messages directly from the Titan bot, webhook messages allows Titan to take advantage of the built-in webhooks to create messages that looks more real. Reading messages in Discord can be 20% more cooler!

+
+ +

@@ -160,6 +186,11 @@ {% endif %}
+

+{% include 'card_commands.html.j2' %} + +{% include 'card_queryparams.html.j2' %} + {% endblock %} {% block script %} diff --git a/webapp/titanembeds/templates/card_commands.html.j2 b/webapp/titanembeds/templates/card_commands.html.j2 new file mode 100644 index 0000000..3aab508 --- /dev/null +++ b/webapp/titanembeds/templates/card_commands.html.j2 @@ -0,0 +1,18 @@ +

Commands

+
+
+
+
+
+

All commands start by mentioning the bot user, @Titan.

+

Guest User Moderation

+

All guest users are denoted by square brackets (or Titan's logo as avatar if enabled Webhook Messages) around their username in the Discord channel, when sending messages.

+
    +
  • ban <username-query>[#<discriminator>]
    Bans the user by the username. The username does not need to be the full string. The discriminator is optional.
    Eg: ban Titan#0001
  • +
  • kick <username-query>[#<discriminator>]
    Kicks the user by the username. The username does not need to be the full string. The discriminator is optional.
    Eg: kick Titan#0001
  • +
+
+
+
+
+
\ No newline at end of file diff --git a/webapp/titanembeds/templates/card_queryparams.html.j2 b/webapp/titanembeds/templates/card_queryparams.html.j2 new file mode 100644 index 0000000..1387e0d --- /dev/null +++ b/webapp/titanembeds/templates/card_queryparams.html.j2 @@ -0,0 +1,43 @@ +

Query Parameters

+
+
+
+
+
+

Use query parameters to customize your individual embeds out of this world!

+

Query parameters are in the format of key-value pairs. They are appended after your embed url such that it would look like so:
https://titanembeds.com/embed/1234567890?css=1&defaultchannel=81387914189078528&theme=DiscordDark

+

Below is the reference of all the avaliable query parameters that may be used.

+
    +
  • + css=<integer>
    + Styles the embed's theme according to the unique custom CSS ID. Custom CSS may be managed from the user dashboard page.
    + Eg: css=1 +
  • +
  • + defaultchannel=<snowflake>
    + Instead of having the top channel as the first channel your users see, you may change it. Enable Discord's Developer mode in the Appearances tab of the User Settings and copy the channel ID.
    + Eg: defaultchannel=1234567890 +
  • +
  • + theme=<string>
    + Want your embed to use one of our premade themes? Look no further!
    +
    + Avaliable Options: +
      +
    • BetterTitan
    • +
    • DiscordDark
    • +
    +
    + Eg: theme=DiscordDark +
  • +
  • + username=<string>
    + Prefills the guest username field with the given username.
    + Eg: username=Rainbow%20Dash +
  • +
+
+
+
+
+
\ No newline at end of file diff --git a/webapp/titanembeds/templates/dashboard.html.j2 b/webapp/titanembeds/templates/dashboard.html.j2 index 06bc5fc..4246208 100644 --- a/webapp/titanembeds/templates/dashboard.html.j2 +++ b/webapp/titanembeds/templates/dashboard.html.j2 @@ -1,5 +1,5 @@ {% extends 'site_layout.html.j2' %} -{% block title %}Dashboard{% endblock %} +{% set title="Dashboard" %} {% block content %}

User Dashboard

@@ -60,4 +60,17 @@ {% endfor %}
{% endif %} -{% endblock %} + +
+ +
+
+
+

Donations!

+

Would you like to support the Titan Embeds project?

+

You currently have {{ session["tokens"] }} Titan Tokens.

+ Donate!! +
+
+
+{% endblock %} \ No newline at end of file diff --git a/webapp/titanembeds/templates/donate.html.j2 b/webapp/titanembeds/templates/donate.html.j2 new file mode 100644 index 0000000..d3f2093 --- /dev/null +++ b/webapp/titanembeds/templates/donate.html.j2 @@ -0,0 +1,51 @@ +{% extends 'site_layout.html.j2' %} +{% set title="Donate" %} + +{% block content %} +

Donate and Support Titan Embeds

+

Contributing to the Titan project has never been so easy! Donate to support our project development and hosting.

+ +
+
+
+

The Name-Your-Price Tool

+

Currently if you donate, we cannot give much back in return, yet. However, we do have some donatator features up our sleeves and will be implemented.

+

For now, you will receive Titan Tokens™ (to be spent on donator features) and a supporter role on our support server.

+

+ +

+

$5 for 500 tokens!

+ +
+
+
+{% endblock %} +{% block script %} + +{% endblock %} + +{% block additional_head_elements %} + +{% endblock %} \ No newline at end of file diff --git a/webapp/titanembeds/templates/donate_thanks.html.j2 b/webapp/titanembeds/templates/donate_thanks.html.j2 new file mode 100644 index 0000000..b32cb62 --- /dev/null +++ b/webapp/titanembeds/templates/donate_thanks.html.j2 @@ -0,0 +1,18 @@ +{% extends 'site_layout.html.j2' %} +{% set title="Thanks for Donating" %} + +{% block content %} +

Thank you for Donating and Supporting the Titan Embeds project!

+ +
+
+
+

You're officially one step closer to becoming a True Titan!

+

You now have {{ tokens }} tokens!

+

Please visit our support server and contact a True Titan (Administrators Role) to claim your Supporter role, if you haven't done so already. Mention the transaction ID of {{ transaction }}.

+ Support Server +

Have a nice day!

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/webapp/titanembeds/templates/embed.html.j2 b/webapp/titanembeds/templates/embed.html.j2 index 7212184..cacdc9f 100644 --- a/webapp/titanembeds/templates/embed.html.j2 +++ b/webapp/titanembeds/templates/embed.html.j2 @@ -1,5 +1,5 @@ - + @@ -12,6 +12,9 @@ {% include 'seo_meta.html.j2' %} + {% with title="Visit " + guild['name'] + " embed", description="Visit " + guild['name'] + " on Titan Embeds and chat with your friends from the comfort of your own website. This page is 100% embeddable, iFrameable and looks good on any webpages. Titan is hassle free and designed as easy to setup!", image=generate_guild_icon( guild['id'], guild['icon']) %} + {% include "opengraph_tags.html.j2" %} + {% endwith %} {{ guild['name'] }} - Embed - Titan Embeds for Discord {% include 'google_analytics.html.j2' %} @@ -21,6 +24,7 @@ {% endif %} + {% include 'nobot_header.html.j2' %}
@@ -80,7 +84,7 @@ will have CSS cosmetic privilages removed, if caught. Please don't, we check the

Edit your CSS code here

-
{% if new %}/* Enter your CSS code here! */{% else %}{{ css.css }}{% endif %}
+
{% if new %}/* Enter your CSS code here! */{% else %}{{ css.css|e }}{% endif %}

diff --git a/webapp/titanembeds/userbookkeeping.py b/webapp/titanembeds/userbookkeeping.py new file mode 100644 index 0000000..2588d1c --- /dev/null +++ b/webapp/titanembeds/userbookkeeping.py @@ -0,0 +1,216 @@ +from titanembeds.database import db, Guilds, UnauthenticatedUsers, UnauthenticatedBans, AuthenticatedUsers, GuildMembers, get_guild_member +from titanembeds.utils import guild_accepts_visitors, guild_query_unauth_users_bool, get_client_ipaddr +from titanembeds.oauth import check_user_can_administrate_guild, user_has_permission +from config import config +from flask import session +from sqlalchemy import and_ +import json + +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 = db.session.query(UnauthenticatedUsers).filter(UnauthenticatedUsers.guild_id == guild_id, UnauthenticatedUsers.user_key == user_key).first() + revoked = dbUser.isRevoked() + else: + banned = checkUserBanned(guild_id) + if banned: + return revoked + dbUser = GuildMembers.query.filter(GuildMembers.guild_id == guild_id).filter(GuildMembers.user_id == session["user_id"]).first() + revoked = not dbUser or not dbUser.active + 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 + dbUser = GuildMembers.query.filter(GuildMembers.guild_id == guild_id).filter(GuildMembers.user_id == session["user_id"]).first() + if not dbUser: + banned = False + else: + banned = dbUser.banned + 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, + 'nickname': None, + '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, + 'nickname': None, + '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 + dbMember = get_guild_member(guild_id, status["user_id"]) + if dbMember: + status["nickname"] = dbMember.nickname + 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.get("user_keys", {}) + else: + dbUser = db.session.query(AuthenticatedUsers).filter(and_(AuthenticatedUsers.guild_id == guild_id, AuthenticatedUsers.client_id == session['user_id'])).first() + return dbUser is not None and not checkUserRevoke(guild_id) + +def get_member_roles(guild_id, user_id): + q = db.session.query(GuildMembers).filter(GuildMembers.guild_id == guild_id).filter(GuildMembers.user_id == user_id).first() + return json.loads(q.roles) + +def get_guild_channels(guild_id, force_everyone=False): + if user_unauthenticated() or force_everyone: + member_roles = [guild_id] #equivilant to @everyone role + else: + member_roles = get_member_roles(guild_id, session['user_id']) + if guild_id not in member_roles: + member_roles.append(guild_id) + bot_member_roles = get_member_roles(guild_id, config["client-id"]) + if guild_id not in bot_member_roles: + bot_member_roles.append(guild_id) + dbguild = db.session.query(Guilds).filter(Guilds.guild_id == guild_id).first() + guild_channels = json.loads(dbguild.channels) + guild_roles = json.loads(dbguild.roles) + guild_owner = str(dbguild.owner_id) + result_channels = [] + for channel in guild_channels: + if channel['type'] == "text": + result = get_channel_permission(channel, guild_id, guild_owner, guild_roles, member_roles, session.get("user_id"), force_everyone) + bot_result = get_channel_permission(channel, guild_id, guild_owner, guild_roles, bot_member_roles, config["client-id"], False) + if not bot_result["read"]: + result["read"] = False + if not bot_result["write"]: + result["write"] = False + if not bot_result["mention_everyone"]: + result["mention_everyone"] = False + result_channels.append(result) + return sorted(result_channels, key=lambda k: k['channel']['position']) + +def get_channel_permission(channel, guild_id, guild_owner, guild_roles, member_roles, user_id=None, force_everyone=False): + result = {"channel": channel, "read": False, "write": False, "mention_everyone": False} + if not user_id: + user_id = session.get("user_id") + if guild_owner == user_id: + result["read"] = True + result["write"] = True + result["mention_everyone"] = True + return result + 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["mention_everyone"] = True + return result + + 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.get("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) + result["mention_everyone"] = user_has_permission(channel_perm, 17) + + # 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 + result["mention_everyone"] = False + return result + +def bot_can_create_webhooks(guild): + perm = 0 + guild_roles = json.loads(guild.roles) + # @everyone + for role in guild_roles: + if role["id"] == guild.guild_id: + perm |= role["permissions"] + continue + member_roles = get_member_roles(guild.guild_id, config["client-id"]) + # User Guild Roles + for m_role in member_roles: + for g_role in guild_roles: + if g_role["id"] == m_role: + perm |= g_role["permissions"] + continue + return user_has_permission(perm, 29) + +def guild_webhooks_enabled(guild_id): + dbguild = db.session.query(Guilds).filter(Guilds.guild_id == guild_id).first() + if not dbguild.webhook_messages: + return False + return bot_can_create_webhooks(dbguild) \ No newline at end of file diff --git a/webapp/titanembeds/utils.py b/webapp/titanembeds/utils.py index df2ad51..7c5f7a4 100644 --- a/webapp/titanembeds/utils.py +++ b/webapp/titanembeds/utils.py @@ -1,10 +1,12 @@ -from titanembeds.database import db, Guilds, KeyValueProperties +from titanembeds.database import db, Guilds, KeyValueProperties, get_keyvalproperty from flask import request, session from flask_limiter import Limiter +from flask_socketio import SocketIO from config import config import random import string import hashlib +import time from titanembeds.discordrest import DiscordREST @@ -15,12 +17,12 @@ def get_client_ipaddr(): ip = request.headers['X-Real-IP'] else: # general ip = request.remote_addr - return hashlib.sha512(config['app-secret'] + ip).hexdigest()[:15] + return hashlib.sha512((config['app-secret'] + ip).encode('utf-8')).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)]) + rand_str = lambda n: ''.join([random.choice(string.ascii_lowercase) for i in range(n)]) session['sessionunique'] = rand_str(25) sess = session['sessionunique'] return sess #Totally unique @@ -30,33 +32,33 @@ def make_cache_key(*args, **kwargs): args = str(hash(frozenset(request.args.items()))) ip = get_client_ipaddr() sess = generate_session_key() - return (path + args + sess + ip).encode('utf-8') + return (path + args + sess + ip) def make_user_cache_key(*args, **kwargs): ip = get_client_ipaddr() sess = generate_session_key() - return (sess + ip).encode('utf-8') + return (sess + ip) def make_guilds_cache_key(): sess = generate_session_key() ip = get_client_ipaddr() - return (sess + ip + "user_guilds").encode('utf-8') + return (sess + ip + "user_guilds") 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') + return (sess + ip + guild_id + "user_guild_channels") 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') + return (sess + channel_id) def guild_ratelimit_key(): ip = get_client_ipaddr() guild_id = request.values.get('guild_id', "0") - return (ip + guild_id).encode('utf-8') + return (ip + guild_id) def check_guild_existance(guild_id): dbGuild = Guilds.query.filter_by(guild_id=guild_id).first() @@ -65,8 +67,27 @@ def check_guild_existance(guild_id): else: return True +def guild_accepts_visitors(guild_id): + dbGuild = Guilds.query.filter_by(guild_id=guild_id).first() + return dbGuild.visitor_view + def guild_query_unauth_users_bool(guild_id): dbGuild = db.session.query(Guilds).filter(Guilds.guild_id==guild_id).first() return dbGuild.unauth_users + +def bot_alive(): + results = {"status": False, "formatted_utc": "Never", "epoch_seconds": None} + epoch = get_keyvalproperty("bot_heartbeat") + if not epoch: + return results + epoch = float(epoch) + utc = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(epoch)) + results["formatted_utc"] = utc + results["epoch_seconds"] = epoch + now = time.time() + if now - epoch < 60 * 5: + results["status"] = True + return results rate_limiter = Limiter(key_func=get_client_ipaddr) # Default limit by ip address +socketio = SocketIO() \ No newline at end of file