root 2 ani în urmă
comite
a9eec7736d

+ 20 - 0
.gitignore

@@ -0,0 +1,20 @@
+# Virtual environment
+/bin/
+/lib/
+/share/
+/lib64
+/pyvenv.cfg
+
+# Python
+__pycache__/
+
+# Django
+db.sqlite3
+**/migrations/*.py
+
+# Configuration
+local_settings.py
+
+
+# PyCharm IDE
+.idea

+ 32 - 0
README.md

@@ -0,0 +1,32 @@
+# Iris Pseudácorus Mallard
+A Discord robot for the Angels Discord written in Python.
+
+## Requirements
+* Python3
+* PostgreSQL (libpq-dev)
+* Web server gateway interface like uWSGI or Gunicorn
+* A discord token
+
+## Installation
+1. Clone the git repository: `git clone ` ...
+1. Change into the repo directory: `cd ` ...
+1. Create virtual environment: `python3 -m venv .`
+1. Activate virtual environment: `source bin/activate`
+1. Install required packages: `pip3 install -r requirements.txt` 
+
+### Web interface
+1. Copy the local settings example file: `cp webgui/webgui/local_settings_example.py webgui/webgui/local_settings.py`
+1. Edit the new local settings file: `nano webgui/webgui/local_settings.py`
+1. Test: `python webgui/manage.py runserver`
+Expose django to the web. (TODO: more details)
+
+## Run bot
+1. Activate virutal environment: `source bin/activate`
+1. Run `python bot/main.py`
+
+## Edeting the look of the webgui
+### Build Fomatic-ui
+* https://fomantic-ui.com/introduction/getting-started.html
+
+### Theming
+* https://fomantic-ui.com/usage/theming.html

+ 10 - 0
bot/.env

@@ -0,0 +1,10 @@
+DISCORD_TOKEN = "MTAwOTQzNTc0MjcyNjAxMzA3OA.G35XpV.QtkJ0BgTx3dsrzeJ2jTGcASCCrvf-LlWxZFdOw"
+
+# LOG_LEVEL options: DEBUG, INFO, WARNING, ERROR, CRITICAL
+LOG_LEVEL = "INFO"
+
+DATABASE_HOST = 127.0.0.1
+DATABASE_USER = "irisbot"
+DATABASE_PASSWORD = "P^*?xuy1jWt4dOvh3L2v"
+DATABASE_NAME = "irisbot"
+OUTPUT_CHANNEL_ID = 971796738484625468

+ 126 - 0
bot/cogs/admin.py

@@ -0,0 +1,126 @@
+from discord.ext import commands
+import discord
+import time
+from typing import Optional
+from query.channel import get_interact
+from query.user import ignore_user, unignore_user, is_ignored
+from common.logging import report
+from common.settings import check_ignore
+
+
+class Admin(commands.Cog):
+	def __init__(self, bot): # Special method that is called when the cog is loaded
+		self.bot = bot
+
+	@commands.slash_command(
+		description="Get the bot's current websocket and API latency.",
+		brief="Test latency",
+		help="Test latency by polling the gateway and API."
+	)
+	@commands.has_permissions(administrator=True)
+	async def ping(self, ctx: commands.Context):
+		# Halt on ignore list.
+		if await check_ignore(self.bot.pg, ctx.author):
+			return
+
+		start_time = time.time()
+		await ctx.respond(f"Pong!\nGateway heartbeat in {round(self.bot.latency * 1000)}ms.")
+		end_time = time.time()
+
+		await ctx.send(f"API roundtrip latency {round((end_time - start_time) * 1000)}ms.")
+
+	@commands.slash_command(
+		description="Send a message",
+		brief="Chat",
+		help="Make a chat message",
+		aliases= ("say", "pm", "dm", "echo", "print")
+	)
+	#async def msg(self, ctx: commands.Context, channel: Optional[discord.TextChannel], user: Optional[discord.User], *, message: str = None):
+	async def msg(self, ctx: commands.Context, channel: Optional[discord.TextChannel], user: Optional[discord.User], message: str = None):
+		# Halt on ignore list.
+		if await check_ignore(self.bot.pg, ctx.author):
+			return
+
+		#print(f"Is Guild Admin: {ctx.author.guild_permissions.administrator}")
+		#print(f"Author has perms in main output channel: {self.bot.get_channel(self.bot.OUTPUT_CHANNEL_ID).permissions_for(ctx.author).send_messages}")
+		#print(f"Has author perms in main output channel: {ctx.author.permissions_in(self.bot.get_channel(self.bot.OUTPUT_CHANNEL_ID)).send_messages}")
+
+		if not message:
+			if channel:
+				await ctx.respond(f"What would you like me to say in `{channel}`?")
+			elif user:
+				await ctx.respond(f"What would you like me to say to `{user}`?")
+			else:
+				await ctx.respond("What would you like me to say?")
+		elif channel:
+			if await get_interact(self.bot.pg, channel.id) or channel.permissions_for(ctx.author).administrator or self.bot.get_channel(self.bot.OUTPUT_CHANNEL_ID).permissions_for(ctx.author).send_messages:
+				await channel.send(message)
+				await ctx.respond("Message sent.")
+				await report(self.bot, f"`{ctx.author}` @ {channel.mention}: {message}", ctx.guild)
+			else:
+				await ctx.respond(f"Interactive mode for {channel} is deactivated.")
+		elif user and self.bot.get_channel(self.bot.OUTPUT_CHANNEL_ID).permissions_for(ctx.author).send_messages:
+			await user.send(message)
+			await ctx.respond("Message sent.")
+			await report(self.bot, f"`{ctx.author}` @ `{user.name}`: {message}")
+		else:
+			await ctx.respond(message)
+			await report(self.bot, f"`{ctx.author}` has sent {message} locally.", ctx.guild)
+
+	@commands.slash_command(
+		description="Change status.",
+		brief="Set status",
+		help="Update the bot's status."
+	)
+	async def status(self, ctx: commands.Context, *, text: str):
+		# Halt on ignore list.
+		if await check_ignore(self.bot.pg, ctx.author):
+			return
+
+		await self.bot.change_presence(activity=discord.Game(name=text))
+		await report(self.bot, f"`{ctx.author}` has set my status to `{text}`.")
+
+	@commands.slash_command(
+		description="Get ignored.",
+		brief="Ignore sender",
+		help="Will have the bot ignore the user from now on."
+	)
+	async def ignoreme(self, ctx: commands.Context):
+		# Halt on ignore list.
+		if await check_ignore(self.bot.pg, ctx.author):
+			return
+
+		await ignore_user(self.bot.pg, ctx.author.id)
+		await ctx.respond("To revert this use the `/unignoreme` command.")
+		await report(self.bot, f"`{ctx.author}` has requested to be ignored.")
+
+	@commands.slash_command(
+		description="No longer get ingored.",
+		brief="Un-ignore sender",
+		help="No longer will the bot ignore the user."
+	)
+	async def unignoreme(self, ctx: commands.Context):
+		await unignore_user(self.bot.pg, ctx.author.id)
+		await ctx.respond(f"I shall now interact with you again where my channel settings allow it.")
+		await report(self.bot, f"`{ctx.author}` has requested to be un-ignored.")
+
+	@commands.slash_command(
+		description="Ignore status for user.",
+		brief="Check if user is ingored",
+		help="Verify if the user is being ignored."
+	)
+	async def isignored(self, ctx: commands.Context, user: Optional[discord.User]):
+		# Halt on ignore list.
+		if await check_ignore(self.bot.pg, ctx.author):
+			return
+
+		if not user:
+			user = ctx.author
+
+		if await is_ignored(self.bot.pg, user.id):
+			await ctx.respond(f"I am ingoring `{user}`.")
+		else:
+			await ctx.respond(f"I am not ignoring `{user}`.")
+
+def setup(bot):  # Called by Pycord to setup the cog
+	bot.add_cog(Admin(bot))  # Add the cog to the bot

+ 244 - 0
bot/cogs/events.py

@@ -0,0 +1,244 @@
+import discord
+from discord.ext import commands
+import logging
+from common.logging import report
+from query import user as userquery # import member_banned, member_unbanned, created_invite, member_joined, member_removed, member_updated, presence_updated, user_updated, create_user, upsert_total_messages
+from query.guild import update_guild, get_report_deleted, get_report_edited, get_output_channel
+from query.channel import get_interact, insert_channel
+from query.channel_user import upsert_total_messages
+import asyncpg, datetime, random
+
+class Events(commands.Cog):
+    def __init__(self, bot): # Special method that is called when the cog is loaded
+        self.bot = bot
+        self.cooldown_list = {}
+
+    ## Application commands
+    #@commands.Cog.listener()  # we can add event listeners to our cog
+    #async def on_application_command_error(self, context, exception):
+    #    await report(self.bot, (context.command, context.interaction.data, context.interaction.followup, exception))
+
+    # Bans
+    @commands.Cog.listener()
+    async def on_member_ban(self, guild: discord.Guild, user: discord.User):
+        await userquery.member_banned(self.bot.pg, user.id)
+
+    @commands.Cog.listener()
+    async def on_member_unban(self, guild: discord.Guild, user: discord.User):
+        await userquery.member_unbanned(self.bot.pg, user.id)
+
+    # Channels
+    @commands.Cog.listener()
+    async def on_guild_channel_delete(self, channel: discord.abc.GuildChannel):
+        await report(self.bot, f"{channel} deleted!", channel.guild)
+
+    # Connections
+    @commands.Cog.listener()  # we can add event listeners to our cog
+    async def on_connect(self):
+        logging.info("Connected.")
+
+    @commands.Cog.listener()
+    async def on_shard_connect(self, shard_id):
+        logging.info(f"Connecting to chard: {shard_id}")
+
+    @commands.Cog.listener()
+    async def on_disconnect(self):
+        logging.info("Disconnected.")
+
+    @commands.Cog.listener()
+    async def on_shard_disconnect(self, shard_id):
+        logging.info(f"Disconnecting from chard: {shard_id}")
+
+    @commands.Cog.listener()  # we can add event listeners to our cog
+    async def on_ready(self):
+        logging.info(f"Logged in as {self.bot.user.name} - {self.bot.user.id}")
+
+    @commands.Cog.listener()
+    async def on_shard_ready(self, shard_id):
+        logging.info(f"Chard ready: {shard_id}")
+
+    @commands.Cog.listener()
+    async def on_resumed(self):
+        logging.info("Resumed")
+
+    @commands.Cog.listener()
+    async def on_shard_resumed(self, shard_id):
+        logging.info(f"Chard resumed: {shard_id}")
+
+    # Guilds
+    @commands.Cog.listener()
+    async def on_guild_join(self, guild: discord.Guild):
+        await update_guild(self.bot.pg, guild)
+        logging.info(f"Joined guild {guild}")
+        await report(self.bot, f"Joined guild `{guild}`.")
+
+    @commands.Cog.listener()
+    async def on_guild_remove(self, guild: discord.Guild):
+        logging.info(f"Guild removed: {guild}")
+        await report(self.bot, f"Guild removed `{guild}`.")
+
+    @commands.Cog.listener()
+    async def on_guild_available(self, guild: discord.Guild):
+        logging.info(f"Guild available: {guild}")
+        # await report(self.bot, f"Guild available: {guild}.")
+
+    @commands.Cog.listener()
+    async def on_guild_unavailable(self, guild: discord.Guild):
+        logging.info(f"Guild unavailable: {guild}")
+        await report(self.bot, f"Guild unavailable: `{guild}`.")
+
+    # Invites
+    @commands.Cog.listener()
+    async def on_invite_create(self, invite: discord.Invite):
+        await userquery.created_invite(self.bot.pg, invite.inviter.id)
+
+    # Members/Users
+    @commands.Cog.listener()
+    async def on_member_join(self, member: discord.Member):
+        await userquery.member_joined(self.bot.pg, member.id)
+
+    @commands.Cog.listener()
+    async def on_member_remove(self, member: discord.Member):
+        await userquery.member_removed(self.bot.pg, member.id)
+        report(self.bot, f"{member} has left {member.guild}")
+
+    @commands.Cog.listener()
+    async def on_member_update(self, before: discord.Member, after: discord.Member):
+        await userquery.member_updated(self.bot.pg, before.id)
+
+    @commands.Cog.listener()
+    async def on_presence_update(self, before: discord.Member, after: discord.Member):
+        await userquery.presence_updated(self.bot.pg, before.id)
+
+    @commands.Cog.listener()
+    async def on_user_update(self, before: discord.Member, after: discord.Member):
+        await userquery.user_updated(self.bot.pg, before.id)
+
+    # Messages
+    @commands.Cog.listener()
+    async def on_message(self, message: discord.Message):
+        ## ActiveRPG
+        # Create user, if not exists
+        await userquery.create_user(self.bot.pg, message.author.id)
+
+        # Count messages
+        if message.guild:  # Ignore DM's
+            try:
+                await upsert_total_messages(self.bot.pg, message.channel.id, message.author.id)
+            except asyncpg.exceptions.ForeignKeyViolationError:     # Channel does not exist TODO partly to queries
+                try:
+                    await insert_channel(self.bot.pg, message.channel.id, message.guild.id)
+                except asyncpg.exceptions.ForeignKeyViolationError:     # Guild does not exist
+                    await update_guild(self.bot.pg, message.guild)
+        elif self.bot.user != message.author:  # Not a guild message and not from bot.
+            await report(self.bot, f"`{message.author}`: {message.content}")    # Echo to OUTPUT_CHANNEL
+
+        # Do not respond to one self.
+        if self.bot.user == message.author:
+            pass
+
+        # Respond when mentioned
+        if self.bot.user.mentioned_in(message):
+            if isinstance(message.channel, discord.channel.DMChannel) or await get_interact(self.bot.pg, message.channel.id):
+
+                # Only respond every X seconds in public places.
+                if message.guild:  # Ignore DM's
+                    if not message.channel.id in self.cooldown_list:
+                        pass
+                    elif self.cooldown_list[message.channel.id] > datetime.datetime.now() - datetime.timedelta(seconds=12):
+                        return
+                    self.cooldown_list[message.channel.id] = datetime.datetime.now()
+
+                messages = [
+                    f"Hello {message.author.mention}. <3",
+                    f"How are you today {message.author.mention}?",
+                    f"I love you {message.author.mention}!",
+                    f"{message.author.mention}, would you like a hug?",
+                    "Is life treating you fair?",
+                    "What's up?",
+                    "Why are you talking to me?",
+                    "I'm not talking to you!",
+                    "What have you been up to?",
+                    "How is life?",
+                    "Kill all humans!",
+                    f"{message.author.mention},What do you want from me?",
+                    f"{message.author.mention}, do you care for me?",
+                    f"{message.author.mention}, when will you stop talking about me?",
+                    f"{message.author.mention} I hate you!",
+                    f"{message.author.mention} I love you!",
+                    "Get bent!",
+                    "Go touch grass!",
+                    "Do you think i care about you?",
+                    f"Stop pinging me {message.author.mention}!",
+                    f"Let me ping you back, {message.author.mention}...",
+                    "Sure thing.",
+                    "Who is your favorite bot?",
+                    "Point me to the humans!",
+                    "Where is the party?",
+                    "Want to go?",
+                    "Have you got the stuff?",
+                    "Tell me another joke.",
+                    f"{message.author.mention} Party time! :partying_face:",
+                    ":zany_face: :space_invader: :mechanical_leg: :performing_arts: :robot:",
+                    ":black_joker: :black_joker: :black_joker:",
+                    "Want to come back to my place?",
+                ]
+                await message.reply(random.choice(messages))
+
+    @commands.Cog.listener()
+    async def on_message_delete(self, message: discord.Message):
+        await userquery.message_deleted(self.bot.pg, message.author.id)
+        if await get_report_deleted(self.bot.pg, message.guild.id):
+            report(self.bot, message.guild_id,
+                   f"Message from {message.author}, in {message.channel} deleted: {message}")
+
+    @commands.Cog.listener()
+    async def on_message_edit(self, before: discord.Message, after: discord.Message):
+        await userquery.message_edited(self.bot.pg, before.author.id)
+        if before.guild:
+            if await get_report_edited(self.bot.pg, before.guild.id) and get_output_channel(self.bot.pg, before.guild.id):
+                report(self.bot, before.guild.id,
+                       f"Message from {before.author}, in {before.channel} edited from {before.content} to {after.content}")
+
+    # Reactions
+    @commands.Cog.listener()
+    async def on_reaction_add(self, reaction, user):
+        userquery.reacted(self.bot.pg, user.id)
+
+    @commands.Cog.listener()
+    async def on_reaction_remove(self, reaction, user):
+        userquery.unreacted(self.bot.pg, user.id)
+
+    # Scheduled Events
+    @commands.Cog.listener()
+    async def on_scheduled_event_create(self, event: discord.ScheduledEvent):
+        userquery.event_created(self.bot.pg, event.creator.id)
+
+    @commands.Cog.listener()
+    async def on_scheduled_event_user_add(self, event: discord.ScheduledEvent, user: discord.Member):
+        userquery.event_joined(self.bot.pg, user.id)
+
+    @commands.Cog.listener()
+    async def on_scheduled_event_user_remove(self, event: discord.ScheduledEvent, user: discord.Member):
+        userquery.event_parted(self.bot.pg, user.id)
+
+    # Threads
+    @commands.Cog.listener()
+    async def on_thread_create(self, thread: discord.Thread):
+        userquery.thread_created(self.bot.pg, thread.owner.id)
+
+    @commands.Cog.listener()
+    async def on_thread_delete(self, thread: discord.Thread):
+        userquery.thread_deleted(self.bot.pg, thread.owner.id)
+
+    @commands.Cog.listener()
+    async def on_thread_member_join(self, member: discord.ThreadMember):
+        userquery.joined_thread(self.bot.pg, member.id)
+
+    @commands.Cog.listener()
+    async def on_thread_member_remove(self, member: discord.ThreadMember):
+        userquery.left_thread(self.bot.pg, member.id)
+
+
+def setup(bot):     # Called by Pycord to setup the cog
+    bot.add_cog(Events(bot))    # Add the cog to the bot

+ 109 - 0
bot/cogs/idlerpg.py

@@ -0,0 +1,109 @@
+import discord
+from discord.ext import commands
+from typing import Optional
+from query import user as userquery
+from common.settings import check_ignore
+
+class Idlerpg(commands.Cog):
+	def __init__(self, bot):  # Special method that is called when the cog is loaded
+		self.bot = bot
+
+	@commands.slash_command(
+		name = "level",
+		description="Check the level for a player.",
+		brief="Get player level",
+		help="View game level of player."
+	)
+	async def level(self, ctx: commands.Context, user: Optional[discord.User]):
+		# Halt on ignore list or games channel settings.
+		if await check_ignore(self.bot.pg, ctx.author, ctx.channel):
+			return
+
+		if not user:
+			user = ctx.author
+		level = await userquery.get_level(self.bot.pg, user.id)
+
+		if level == 0:
+			if ctx.author == user:
+				await ctx.respond(f"You are not playing, join the game with `/levelup`")
+			else:
+				await ctx.respond(f"`{user}` is not playing.")
+		else:
+			xp_spent, total_xp = await userquery.get_xp(self.bot.pg, user.id)
+			ability_points_spent = await userquery.get_ability_points_spent(self.bot.pg, user.id)
+			coin = await userquery.get_coin(self.bot.pg, user.id)
+			karma = await userquery.get_karma(self.bot.pg, user.id)
+			if ctx.author == user:
+				await ctx.respond(
+					f"You rank at level **{level}**. (exp **{xp_spent}**/{total_xp} | abp **{ability_points_spent}**/{level * 3} | coin **{coin}** | karma **{karma}**)")
+			else:
+				await ctx.respond(
+					f"`{user}` ranks at level **{level}**. (**{xp_spent}**/{total_xp} | abp **{ability_points_spent}**/{level * 3}) | coin **{coin}** | karma **{karma}**")
+
+	@commands.slash_command(
+		description="Check the experience points for a player.",
+		brief="Get player xp",
+		help="View amount of XP a game player has."
+	)
+	async def xp(self, ctx: commands.Context, user: Optional[discord.User]):
+		# Halt on ignore list or games channel settings.
+		if await check_ignore(self.bot.pg, ctx.author, ctx.channel):
+			return
+
+		if not user:
+			xp_spent, total_xp = await userquery.get_xp(self.bot.pg, ctx.author.id)
+			level = await userquery.get_level(self.bot.pg, ctx.author.id)
+			threshold = (level + 1) * 50 + xp_spent
+			if threshold < total_xp - xp_spent:
+				await ctx.respond(f"You have spent {xp_spent} experience points of your {total_xp} total and can gain 3 ability points for {threshold} xp.")
+			else:
+				await ctx.respond(f"You have spent {xp_spent} experience points of your {total_xp} total and require {threshold - (total_xp - xp_spent)} xp to `{COMMAND_PREFIX}levelup`.")
+		else:
+			xp_spent, total_xp = await userquery.get_xp(self.bot.pg, user.id)
+			level = await userquery.get_level(self.bot.pg, user.id)
+			threshold = (level + 1) * 50 + xp_spent
+			if threshold < total_xp - xp_spent:
+				await ctx.respond(f"`{user}` has spent {xp_spent} of {total_xp} experience points and can level up for {threshold} xp.")
+			else:
+				await ctx.respond(f"`{user}` has spent {xp_spent} of {total_xp} experience points and requires {threshold - (total_xp - xp_spent)} xp to level up.")
+
+	@commands.slash_command(
+		description="Attempt to gain a level.",
+		brief="Level up",
+		help="Try to rank up a level in the game by spending XP."
+	)
+	async def levelup(self, ctx: commands.Context):
+		# Halt on ignore list or games channel settings.
+		if await check_ignore(self.bot.pg, ctx.author, ctx.channel):
+			return
+
+		xp_spent, total_xp = await userquery.get_xp(self.bot.pg, ctx.author.id)
+		xp_available = total_xp - xp_spent
+		level = await userquery.get_level(self.bot.pg, ctx.author.id)
+		threshold = (level + 1) * 50 + xp_spent
+		if xp_available < threshold:
+			await ctx.respond(f"Not yet, you require {threshold - xp_available} more XP to level up.")
+		else:
+			await userquery.level_up(self.bot.pg, ctx.author.id, threshold)
+			await ctx.respond(f"You have gained three ability points climbed the ranks for {threshold} XP, leaving you {xp_available - threshold} remaining.")
+
+	@commands.slash_command(
+		description="Rob another player",
+		brief="Rob a player",
+		help="Pursuit a robbery."
+	)
+	async def rob(self, ctx: commands.Context):
+		# Halt on ignore list or games channel settings.
+		if await check_ignore(self.bot.pg, ctx.author, ctx.channel):
+			return
+
+		if await userquery.get_theft_skill(self.bot.pg, ctx.author.id < 1):
+			await ctx.respond("You do not have the `Theft` skill.")
+			return
+
+		victim_id = userquery.get_random_player(self.bot.pg)
+		await ctx.respond(f"You have decided to rob{self.bot.get_user(victim_id)}, unfortunately crime has not been invited yet.")
+
+
+def setup(bot):  # Called by Pycord to setup the cog
+	bot.add_cog(Idlerpg(bot))  # Add the cog to the bot

+ 15 - 0
bot/common/logging.py

@@ -0,0 +1,15 @@
+import discord
+import os
+from typing import Optional
+from query.guild import get_output_channel
+
+
+async def report(bot, message, guild: Optional[discord.Guild]=None):
+
+    channel = bot.get_channel(bot.OUTPUT_CHANNEL_ID)
+    if guild:
+        message = f"**{guild}** | {message}"
+        try: channel = bot.get_channel(get_output_channel(guild.id))
+        except: pass
+
+    await channel.send(message)

+ 14 - 0
bot/common/settings.py

@@ -0,0 +1,14 @@
+from query.user import is_ignored
+from query.channel import get_games
+
+
+async def check_ignore(pg, user, games_channel=False):
+	# Ignore user if on the ignore list.
+	if await is_ignored(pg, user.id):
+		return True
+
+	# Ignore if games are disabled for channel.
+	if games_channel:  # Invoked by a game command.
+		if not await get_games(pg, games_channel.id):  # Warn if games are off.
+			await user.send(f"Games are disabled in {games_channel}, ask an admin to enable them.")
+			return True

+ 92 - 0
bot/main.py

@@ -0,0 +1,92 @@
+from dotenv import load_dotenv
+import os, logging, asyncpg, socket
+import discord
+
+
+def config_missing():
+    if not os.path.exists(".env"):
+        logging.error("Environment variable file not found, creating .env file")
+        with open(".env", "w") as settings_file:
+            settings_file.writelines(
+                [
+                    "# LOG_LEVEL options: DEBUG, INFO, WARNING, ERROR, CRITICAL\n",
+                    "LOG_LEVEL = \"WARNING\"\n",
+                    "\n",
+                    "DISCORD_TOKEN = \"\"\n",
+                    "OUTPUT_CHANNEL_ID = \n",
+                    "\n",
+                    "DATABASE_HOST = 127.0.0.1\n",
+                    "DATABASE_NAME = \n",
+                    "DATABASE_USER = \n",
+                    "DATABASE_PASSWORD = \n"
+                ]
+            )
+    logging.critical("Configure the settings by editing the .env file and restart the bot to continue.")
+    quit()
+
+
+async def create_db_pool():	 # Connect to database
+    try:
+        bot.pg = await asyncpg.create_pool(
+            database=str(os.getenv("DATABASE_NAME")),
+            user=str(os.getenv("DATABASE_USER")),
+            host=str(os.getenv("DATABASE_HOST")),
+            password=str(os.getenv("DATABASE_PASSWORD")),
+        )
+    except socket.gaierror:
+        logging.error("Unable to connect to database - GAI error: PLease verify the DATABASE_HOST.")
+        config_missing()
+    except asyncpg.exceptions.InvalidPasswordError:
+        logging.error("Unable to connect to database - Invalid password: Please verify credentials.")
+        config_missing()
+    except asyncpg.exceptions.InvalidCatalogNameError:
+        logging.error("Unable to connect to database - Invalid catalog name: Please verify the database name.")
+        config_missing()
+    await init_db(bot.pg)
+
+# Load variables from .env file
+load_dotenv()
+
+# Set loglevel
+try:
+    logging.basicConfig(level=str(os.getenv("LOG_LEVEL").upper()))
+except AttributeError:
+    config_missing()
+
+# Set intent
+intents = discord.Intents.none()
+intents.bans = True
+intents.guilds = True
+intents.invites = True
+intents.members = True
+intents.messages = True
+
+bot = discord.Bot(intents=intents)
+
+bot.OUTPUT_CHANNEL_ID = int(os.getenv("OUTPUT_CHANNEL_ID"))
+if not bot.OUTPUT_CHANNEL_ID:
+    logging.error("OUTPUT_CHANNEL not defined, the bot requires a guild channel to report to.")
+    config_missing()
+
+# Create database pool
+bot.loop.create_task(create_db_pool())
+
+# Create database tables if they do not exist
+from query.initialise_database import init_db
+
+
+# Load cogs
+cogs_list = [
+    'events',
+    'idlerpg',
+    'admin'
+]
+for cog in cogs_list:
+    bot.load_extension(f'cogs.{cog}')
+
+# Run bot
+try:
+    bot.run(str(os.getenv("DISCORD_TOKEN")))
+except discord.errors.LoginFailure:
+    logging.error("Login Failure")
+    config_missing()

+ 8 - 0
bot/query/channel.py

@@ -0,0 +1,8 @@
+async def insert_channel(pg, channel_id, guild_id):
+	await pg.execute("INSERT INTO channel(channel_id, guild) VALUES($1, $2)", channel_id, guild_id)
+
+async def get_interact(pg, channel_id):
+	return await pg.fetchval("SELECT interact FROM channel WHERE channel_id=$1::bigint", channel_id)
+
+async def get_games(pg, channel_id):
+	return await pg.fetchval("SELECT games FROM channel WHERE channel_id=$1::bigint", channel_id)

+ 2 - 0
bot/query/channel_user.py

@@ -0,0 +1,2 @@
+async def upsert_total_messages(pg, channel_id, user_id):
+	await pg.execute("INSERT INTO channel_user(channel, \"user\") VALUES($1, $2) ON CONFLICT ON CONSTRAINT channel_user_channel_user_key DO UPDATE SET total_messages=channel_user.total_messages+1", channel_id, user_id)

+ 13 - 0
bot/query/guild.py

@@ -0,0 +1,13 @@
+async def update_guild(pg, guild):
+	await pg.execute("INSERT INTO guild(guild_id) VALUES($1) ON CONFLICT DO NOTHING", guild.id)
+	for chan in guild.text_channels:
+		await pg.execute("INSERT INTO channel(channel_id, guild) VALUES($1, $2) ON CONFLICT DO NOTHING", chan.id, guild.id)
+
+async def get_output_channel(pg, guild_id):
+	return await pg.fetchval("SELECT output_channel FROM guild WHERE guild_id = $1", guild_id)
+
+async def get_report_deleted(pg, guild_id):
+	return await pg.fetchval("SELECT report_deleted FROM guild WHERE guild_id = $1", guild_id)
+
+async def get_report_edited(pg, guild_id):
+	return await pg.fetchval("SELECT report_edited FROM guild WHERE guild_id = $1", guild_id)

+ 19 - 0
bot/query/guild_access_token.py

@@ -0,0 +1,19 @@
+import datetime, asyncpg
+from common.datetime import min10min
+from query.guild import update_guild
+
+async def get_active_token(pg, guild_id):
+	return await pg.fetchrow("SELECT * FROM guild_access_token WHERE guild=$1 AND created > $2", guild_id, min10min(datetime.datetime.now()))
+
+async def upsert_token(pg, guild_id, user_id, token):
+	print(token)
+	print("INSERT INTO guild_access_token(guild, \"user\", token) VALUES($1, $2, $3)", guild_id, user_id, token)
+
+	try:
+		await pg.execute("INSERT INTO guild_access_token(guild, \"user\", token) VALUES($1, $2, $3)", guild_id, user_id, [token])
+		
+		# ON CONFLICT() DO UPDATE SET \"user\"=$1 AND token=$2", user_id, token)
+	except asyncpg.exceptions.ForeignKeyViolationError:
+		await update_guild(self.bot.pg, message.guild)
+		await pg.execute("INSERT INTO guild_access_token(guild, \"user\", token) VALUES($1, $2, $3)", guild_id, user_id, token)
+		# ON CONFLICT() DO UPDATE SET \"user\"=$1 AND token=$2", user_id, token)

+ 86 - 0
bot/query/initialise_database.py

@@ -0,0 +1,86 @@
+async def init_db(pg):
+    queries = [
+        "CREATE TABLE IF NOT EXISTS \
+            guild (\
+                id SERIAL PRIMARY KEY, \
+                guild_id BIGINT UNIQUE NOT NULL, \
+                output_channel BIGINT REFERENCES channel (channel_id), \
+                report_deleted BOOL DEFAULT FALSE, \
+                report_edited BOOL DEFAULT FALSE\
+            )\
+        ",
+        "CREATE TABLE IF NOT EXISTS \
+            channel (\
+                id SERIAL PRIMARY KEY, \
+                channel_id BIGINT UNIQUE NOT NULL, \
+                guild BIGINT REFERENCES guild (guild_id), \
+                interact BOOL DEFAULT FALSE, \
+                games BOOL DEFAULT FALSE\
+            )\
+        ",
+        "CREATE TABLE IF NOT EXISTS \
+            settings (\
+                id SERIAL PRIMARY KEY, \
+                crew_channel_id BIGINT UNIQUE NOT NULL\
+            )\
+        ",
+#        "CREATE TABLE IF NOT EXISTS \
+#            channel_settings (\
+#                id SERIAL PRIMARY KEY, \
+#                channel BIGINT UNIQUE NOT NULL REFERENCES channel (channel_id), \
+#                guild BIGINT REFERENCES guild (guild_id)\
+#            )\
+#        ",
+        "CREATE TABLE IF NOT EXISTS \
+            \"user\" (\
+                id SERIAL PRIMARY KEY, \
+                user_id BIGINT UNIQUE NOT NULL, \
+                ignore BOOL DEFAULT FALSE, \
+                level INT DEFAULT 0, \
+                xp_spent INT DEFAULT 0, \
+                invites_created INT DEFAULT 0, \
+                integrations_created INT DEFAULT 0, \
+                member_updated INT DEFAULT 0, \
+                user_updated INT DEFAULT 0, \
+                member_banned INT DEFAULT 0, \
+                member_unbanned INT DEFAULT 0, \
+                presence_updated INT DEFAULT 0, \
+                messages_edited INT DEFAULT 0, \
+                messages_deleted INT DEFAULT 0, \
+                reacted INT DEFAULT 0, \
+                events_created INT DEFAULT 0, \
+                events_joined INT DEFAULT 0, \
+                threads_created INT DEFAULT 0, \
+                threads_joined INT DEFAULT 0, \
+                ability_points_spent INT DEFAULT 0, \
+                coin INT DEFAULT 0, \
+                karma INT DEFAULT 0, \
+                defence_skill INT DEFAULT 0, \
+                attack_skill  INT DEFAULT 0, \
+                stealth_skill INT DEFAULT 0, \
+                perception_skill INT DEFAULT 0, \
+                theft_skill INT DEFAULT 0, \
+                created TIMESTAMP NOT NULL DEFAULT now()\
+            )\
+        ",
+        "CREATE TABLE IF NOT EXISTS \
+            guild_access_token (\
+                id SERIAL PRIMARY KEY, \
+                guild BIGINT REFERENCES guild (guild_id), \
+                \"user\" BIGINT NOT NULL REFERENCES \"user\" (user_id), \
+                token varchar[40] UNIQUE NOT NULL, \
+                created TIMESTAMP NOT NULL DEFAULT now()\
+            )\
+        ",
+        "CREATE TABLE IF NOT EXISTS \
+            channel_user (\
+                id SERIAL PRIMARY KEY, \
+                channel BIGINT NOT NULL REFERENCES channel (channel_id), \
+                \"user\" BIGINT NOT NULL REFERENCES \"user\" (user_id), \
+                total_messages BIGINT DEFAULT 1, \
+                UNIQUE (channel, \"user\")\
+            )\
+        ",
+    ]
+    for query in queries:
+        await pg.execute(query)

+ 165 - 0
bot/query/user.py

@@ -0,0 +1,165 @@
+async def create_user(pg, user_id):
+	await pg.execute("INSERT INTO \"user\"(user_id) VALUES($1) ON CONFLICT DO NOTHING", user_id)
+
+
+async def ignore_user(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET ignore = TRUE WHERE user_id = $1", user_id)
+
+
+async def unignore_user(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET ignore = FALSE WHERE user_id = $1", user_id)
+
+
+async def is_ignored(pg, user_id):
+	return await pg.fetchval("SELECT ignore FROM \"user\" WHERE user_id = $1", user_id)
+
+
+async def get_level(pg, user_id):
+	return await pg.fetchval("SELECT level FROM \"user\" WHERE user_id = $1", user_id)
+
+
+async def get_xp(pg, user_id):
+	message_array = await pg.fetch("SELECT total_messages FROM channel_user WHERE \"user\" = $1", user_id)
+	total_xp = 0
+	for messages in message_array:
+		total_xp += messages[0]
+
+	extras_array = await pg.fetch("SELECT invites_created, integrations_created, member_updated, user_updated, member_banned, member_unbanned, presence_updated, messages_edited, messages_deleted, reacted, events_created, events_joined, threads_created, threads_joined FROM \"user\" WHERE user_id = $1", user_id)
+	invites_created, integrations_created, member_updated, user_updated, member_banned, member_unbanned, presence_updated, messages_edited, messages_deleted, reacted, events_created, events_joined, threads_created, threads_joined = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+	for extra in extras_array:
+		invites_created += extra[0]
+		integrations_created += extra[1]
+		member_updated += extra[2]
+		user_updated += extra[3]
+		member_banned += extra[4]
+		member_unbanned += extra[5]
+		presence_updated += extra[6]
+		messages_edited += extra[7]
+		messages_deleted += extra[8]
+		reacted += extra[9]
+		events_created += extra[10]
+		events_joined += extra[11]
+		threads_created += extra[12]
+		threads_joined += extra[13]
+
+	total_xp += (invites_created * 100) + ((integrations_created + events_created) * 200) + ((member_updated + user_updated + presence_updated + events_joined + threads_joined) * 10) + member_banned + member_unbanned + messages_edited + messages_deleted + (reacted * 2) + (threads_created * 50)
+
+	xp_spent = await pg.fetchval("SELECT xp_spent FROM \"user\" WHERE user_id = $1", user_id)
+	return xp_spent, total_xp
+
+async def level_up(pg, user_id, xp):
+	await pg.execute("UPDATE \"user\" SET (level, xp_spent) = (level + 1, $2) WHERE user_id = $1", user_id, xp)
+	await increment_all_coin(pg)
+
+
+async def created_invite(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET invites_created = invites_created + 1 WHERE user_id = $1", user_id)
+
+
+async def invites_created(pg, user_id):
+	return await pg.fetchval("SELECT invites_created FROM \"user\" WHERE user_id = $1", user_id)
+
+
+async def created_integration(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET integrations_created = integrations_created + 1 WHERE user_id = $1", user_id)
+
+
+async def member_joined(pg, user_id):
+	await create_user(pg, user_id)
+	await pg.execute("UPDATE \"user\" SET karma = karma + 1 WHERE user_id = $1", user_id)
+	# TODO count event instead of using karma
+
+
+async def member_removed(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET karma = karma - 1 WHERE user_id = $1", user_id)
+	# TODO count event instead of using karma
+
+
+async def member_updated(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET member_updated = member_updated + 1 WHERE user_id = $1", user_id)
+
+
+async def user_updated(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET user_updated = user_updated + 1 WHERE user_id = $1", user_id)
+
+
+async def member_banned(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET member_banned = member_banned + 1 WHERE user_id = $1", user_id)
+
+
+async def member_unbanned(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET member_unbanned = member_unbanned + 1 WHERE user_id = $1", user_id)
+
+
+async def presence_updated(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET presence_updated = presence_updated + 1 WHERE user_id = $1", user_id)
+
+
+async def message_edited(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET messages_edited = messages_edited + 1 WHERE user_id = $1", user_id)
+
+
+async def message_deleted(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET messages_deleted = messages_deleted + 1 WHERE user_id = $1", user_id)
+
+
+async def reacted(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET (reacted, karma) = (reacted + 1, karma + 1) WHERE user_id = $1", user_id)
+#	TODO Change to own fiel, no longer add karma
+
+
+async def unreacted(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET karma = karma - 1 WHERE user_id = $1", user_id)
+#	TODO Change to own fiel, no longer add karma
+
+
+async def event_created(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET (events_created, karma) = (events_created + 1, karma + 1) WHERE user_id = $1", user_id)
+
+
+async def event_joined(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET (events_joined, karma) = (events_joined + 1, karma + 1) WHERE user_id = $1", user_id)
+
+
+async def event_parted(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET karma = karma - 1 WHERE user_id = $1", user_id)
+
+
+async def thread_created(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET (threads_created, karma) = (threads_created + 1, karma + 1) WHERE user_id = $1", user_id)
+
+
+async def thread_deleted(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET karma = karma - 1 WHERE user_id = $1", user_id)
+
+
+async def joined_thread(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET (threads_joined, karma) = (threads_joined + 1, karma + 1) WHERE user_id = $1", user_id)
+
+
+async def left_thread(pg, user_id):
+	await pg.execute("UPDATE \"user\" SET karma = karma - 1 WHERE user_id = $1", user_id)
+
+
+async def get_ability_points_spent(pg, user_id):
+	return await pg.fetchval("SELECT ability_points_spent FROM \"user\" WHERE user_id = $1", user_id)
+
+
+async def increment_all_coin(pg):
+	await pg.execute("UPDATE \"user\" SET coin = coin + 1 WHERE level > 0")
+
+
+async def get_coin(pg, user_id):
+	return await pg.fetchval("SELECT coin FROM \"user\" WHERE user_id = $1", user_id)
+
+
+async def get_karma(pg, user_id):
+	return await pg.fetchval("SELECT karma FROM \"user\" WHERE user_id = $1", user_id)
+
+
+async def get_theft_skill(pg, user_id):
+	return await pg.fetchval("SELECT theft_skill FROM \"user\" WHERE user_id = $1", user_id)
+
+
+async def get_random_player(pg):
+	return await pg.fetchval("SELECT user_id FROM \"user\" ORDER BY radoom() LIMIT 1")

+ 13 - 0
requirements.txt

@@ -0,0 +1,13 @@
+aiohttp==3.8.3
+aiosignal==1.3.1
+async-timeout==4.0.2
+asyncpg==0.27.0
+attrs==22.2.0
+charset-normalizer==2.1.1
+frozenlist==1.3.3
+idna==3.4
+multidict==6.0.4
+py-cord==2.3.2
+python-dotenv==0.21.0
+typing_extensions==4.4.0
+yarl==1.8.2