commit af523d7ae325fb34c8dcd477605791a30fa845b8
parent f05a87f3a2ed8dc6b4846396c196da7ed7436b3e
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date: Fri, 25 Jun 2021 14:04:16 -0700
Implement SQLite database
Diffstat:
5 files changed, 804 insertions(+), 785 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,2 +1,3 @@
.venv
data.json
+data.db
diff --git a/README.md b/README.md
@@ -23,7 +23,7 @@ A Discord bot to facilitate countdowns.
5. Open `data.json` (which was generated by `setup.py`) and add your bot's token.
```json
- {"token": "YOUR_TOKEN_HERE", "countdowns": {}}
+ {"token": "YOUR_TOKEN_HERE", "prefixes": ["c."]}
```
6. Run the bot
diff --git a/bot.py b/bot.py
@@ -1,5 +1,4 @@
# Import dependencies
-import copy
from datetime import datetime, timedelta
import discord
from discord.ext import commands
@@ -9,308 +8,52 @@ from matplotlib import pyplot as plt
from matplotlib.ticker import PercentFormatter
import os
import re
+from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, ForeignKey
+from sqlalchemy.orm import sessionmaker, relationship
+from sqlalchemy.ext.declarative import declarative_base
import tempfile
-# Global variables
-data = {}
-POINT_RULES = {
- "1000s": 1000,
- "1001s": 500,
- "200s": 200,
- "201s": 100,
- "100s": 100,
- "101s": 50,
- "Prime Numbers": 15,
- "Odd Numbers": 12,
- "Even Numbers": 10,
- "First Number": 0,
-}
-COLORS = {
- "error": 0xD52C42,
- "embed": 0x248AD1,
-}
-
-
-
-# Error classes
-class MessageNotAllowedError(Exception):
- """Raised when someone posts twice in a row."""
- pass
-
-class MessageIncorrectError(Exception):
- """Raised when someone posts an incorrect number."""
- pass
-
-
-
-# Static methods
-async def getUsername(id):
- """
- Get a username from a user ID.
-
- Parameters
- ----------
- id : int
- The user ID.
-
- Returns
- -------
- str
- The username (ex: "user#0000").
- """
-
- user = await bot.fetch_user(id)
- return f"{user.name}#{user.discriminator}"
-
-async def getNickname(server, id):
- """
- Get a user's nickname in a server
-
- Parameters
- ----------
- server : int
- The server ID
- id : int
- The user ID
+# Connect to database
+engine = create_engine("sqlite:///data.db")
- Returns
- -------
- str
- The nickname
- """
-
- return (await (bot.get_guild(server)).fetch_member(id)).nick or await getUsername(id)
-
-def saveData(data):
- """
- Save countdown data to the data.json file.
-
- Parameters
- ----------
- data : dict
- The countdown data
- """
-
- # Copy data
- obj = copy.deepcopy(data)
-
- # Format countdown objects
- for countdown in data["countdowns"]:
- obj["countdowns"][countdown] = {
- "messages": [],
- "server": data["countdowns"][countdown].server,
- "timezone": data["countdowns"][countdown].timezone,
- "prefixes": data["countdowns"][countdown].prefixes,
- "reactions": data["countdowns"][countdown].reactions,
- }
- for message in data["countdowns"][countdown].messages:
- obj["countdowns"][countdown]["messages"] += [{
- "timestamp": message.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
- "author": message.author,
- "number": message.number,
- }]
+# Define tables
+Base = declarative_base()
+class Countdown(Base):
+ __tablename__ = "countdown"
- # Save data
- with open(os.path.join(os.path.dirname(__file__), "data.json"), "w") as f:
- return json.dump(obj, f)
-
-def getCountdown(ctx, resortToFirst=True):
- """
- Get the most relevant countdown to a certain context.
-
- Parameters
- ----------
- ctx
- The context
- resortToFirst : bool
- Whether to return the 1st countdown if no relevant countdowns are found
-
- Returns
- -------
- Countdown
- The countdown
- """
-
- # Countdown channel
- global data
- if (str(ctx.channel.id) in data["countdowns"]):
- return data["countdowns"][str(ctx.channel.id)]
-
- # Server with countdown channel
- if (isinstance(ctx.channel, discord.channel.TextChannel)):
- # Get first countdown in this server that use the current prefix
- serverChannels = [x for x in data["countdowns"] if data["countdowns"][x].server == ctx.channel.guild.id and ctx.prefix in data["countdowns"][x].prefixes]
- if (len(serverChannels) > 0):
- return data["countdowns"][serverChannels[0]]
-
- # No countdown channels
- if (len(data["countdowns"]) == 0):
- raise Exception("Countdown channel not found.")
-
- # Return default countdown channel
- if resortToFirst:
- return list(data["countdowns"].values())[0]
- else:
- raise Exception("Countdown channel not found.")
-
-def getPrefix(bot, ctx):
- """
- Get the bot prefix for a certain context.
-
- Parameters
- ----------
- bot
- The bot
- ctx
- The context
- """
-
- # Countdown channel
- global data
- if (str(ctx.channel.id) in data["countdowns"] and len(data["countdowns"][str(ctx.channel.id)].prefixes) > 0):
- return data["countdowns"][str(ctx.channel.id)].prefixes
-
- # Server with countdown channels
- if (isinstance(ctx.channel, discord.channel.TextChannel)):
- serverChannels = [x for x in data["countdowns"] if data["countdowns"][x].server == ctx.channel.guild.id]
- # Get list of prefixes
- prefixes = []
- for channel in serverChannels:
- prefixes += data["countdowns"][channel].prefixes
- if (len(prefixes) > 0):
- return list(dict.fromkeys(prefixes))
-
- # Return default prefixes
- return data["prefixes"]
-
-
-
-# Message class
-class Message:
- """
- Represents a single, valid, countdown message.
-
- Attributes
- ----------
- id : int
- The message ID.
- channel : int
- The channel ID.
- author : int
- The message author ID.
- number : int
- The message content.
- """
-
- def __init__(self, obj):
- if (obj == None):
- self.channel = None
- self.id = None
- self.timestamp = None
- self.author = None
- self.number = None
- else:
- self.channel = obj.channel.id
- self.id = obj.id
- self.timestamp = obj.created_at
- self.author = obj.author.id
- self.number = int(re.findall("^[0-9,]+", obj.content)[0].replace(",",""))
-
- def __eq__(self, o: object) -> bool:
- if not isinstance(o, Message): return False
- else: return self.id == o.id
-
-
-
-# Countdown class
-class Countdown:
- """
- Represents a countdown.
-
- Attributes
- ----------
- messages : list
- The (valid) messages belonging to the countdown.
-
- Methods
- -------
- addMessage
- Add a message to the list of messages.
- parseMessage
- Parse a message and adds it to the list of messages.
- """
-
- def __init__(self, id, messages, server, timezone, prefixes, reactions):
- self.id = id
- self.messages = messages
- self.server = server
- self.timezone = timezone
- self.prefixes = prefixes
- self.reactions = reactions
+ id = Column(Integer, primary_key=True)
+ server_id = Column(Integer)
+ timezone = Column(Float)
+ prefixes = relationship("Prefix", back_populates="countdown", cascade="all, delete-orphan")
+ reactions = relationship("Reaction", back_populates="countdown", cascade="all, delete-orphan")
+ messages = relationship("Message", back_populates="countdown", cascade="all, delete-orphan")
def addMessage(self, message):
"""
- Add a message to the list of messages.
+ Add a message to the countdown
Parameters
----------
message : Message
- The message object.
+ The message object
Raises
------
MessageNotAllowedError
- If the author posted the last message.
+ If the author posted the last message
MessageIncorrectError
- If the message content is incorrect.
+ If the message content is incorrect
"""
- if (len(self.messages) != 0 and message.author == self.messages[-1].author):
+ if (len(self.messages) != 0 and message.author_id == self.messages[-1].author_id):
raise MessageNotAllowedError()
elif (len(self.messages) != 0 and message.number + 1 != self.messages[-1].number):
raise MessageIncorrectError()
else:
self.messages += [message]
- async def parseMessage(self, rawMessage):
- """
- Parse a message and add it to the list of messages.
-
- Notes
- -----
- If the message is invalid or incorrect, a reacted will be added accordingly.
-
- Parameters
- ----------
- rawMessage : obj
- The raw Discord message object.
- """
-
- try:
- # Parse message
- message = Message(rawMessage)
-
- # Add message
- self.addMessage(message)
-
- # Mark important messages
- if (str(message.number) in self.reactions):
- for reaction in self.reactions[str(message.number)]:
- try:
- await rawMessage.add_reaction(reaction)
- except:
- pass
- if (self.messages[0].number >= 500 and message.number % (self.messages[0].number // 50) == 0):
- await rawMessage.pin()
- except MessageNotAllowedError:
- await rawMessage.add_reaction("⛔")
- except MessageIncorrectError:
- await rawMessage.add_reaction("❌")
- except:
- pass
-
def contributors(self):
"""
Get countdown contributor statistics.
@@ -322,14 +65,14 @@ class Countdown:
"""
# Get contributors
- authors = list(set([x.author for x in self.messages]))
+ authors = list(set([x.author_id for x in self.messages]))
# Get contributions
contributors = []
for author in authors:
contributors += [{
"author":author,
- "contributions":len([x for x in self.messages if x.author == author]),
+ "contributions":len([x for x in self.messages if x.author_id == author]),
}]
# Sort contributors by contributions
@@ -338,7 +81,7 @@ class Countdown:
# Return contributors
return contributors
- def eta(self, period=timedelta(days=1), tz=timedelta(hours=0)):
+ def eta(self, period=timedelta(days=1)):
"""
Get countdown eta statistics.
@@ -346,8 +89,6 @@ class Countdown:
----------
period : timedelta
The period size. The default is 1 day.
- tz : timedelta
- The timezone. The default is +0 (UTC)
Returns
-------
@@ -360,26 +101,26 @@ class Countdown:
return [[], []]
# Initialize period data
- periodEnd = self.messages[0].timestamp + tz + period
+ periodEnd = self.messages[0].timestamp + timedelta(hours=self.timezone) + period
lastMessage = 0
# Initialize result and add first data point
- data = [[self.messages[0].timestamp + tz], [self.messages[0].timestamp + tz]]
+ data = [[self.messages[0].timestamp + timedelta(hours=self.timezone)], [self.messages[0].timestamp + timedelta(hours=self.timezone)]]
# Calculate timestamp for last data point
if (self.messages[-1].number == 0):
- end = self.messages[-1].timestamp + tz
+ end = self.messages[-1].timestamp + timedelta(hours=self.timezone)
else:
- end = datetime.utcnow() + tz
+ end = datetime.utcnow() + timedelta(hours=self.timezone)
# Add data points
while (periodEnd < end):
# Advance to last message in period
- while (lastMessage+1 < len(self.messages) and self.messages[lastMessage+1].timestamp + tz < periodEnd):
+ while (lastMessage+1 < len(self.messages) and self.messages[lastMessage+1].timestamp + timedelta(hours=self.timezone) < periodEnd):
lastMessage += 1
# Calculate data
- rate = (self.messages[0].number - self.messages[lastMessage].number) / ((periodEnd - (self.messages[0].timestamp + tz)) / timedelta(days=1))
+ rate = (self.messages[0].number - self.messages[lastMessage].number) / ((periodEnd - (self.messages[0].timestamp + timedelta(hours=self.timezone))) / timedelta(days=1))
eta = periodEnd + timedelta(days=self.messages[lastMessage].number/rate)
data[0] += [periodEnd]
data[1] += [eta]
@@ -426,9 +167,9 @@ class Countdown:
# Calculate contributor points
points = {}
for message in self.messages:
- if (message.author not in points):
- points[message.author] = {
- "author": message.author,
+ if (message.author_id not in points):
+ points[message.author_id] = {
+ "author": message.author_id,
"breakdown": {
"1000s": 0,
"1001s": 0,
@@ -442,16 +183,16 @@ class Countdown:
"First Number": 0,
},
}
- if (message.number == self.messages[0].number): points[message.author]["breakdown"]["First Number"] += 1
- elif (message.number % 1000 == 0): points[message.author]["breakdown"]["1000s"] += 1
- elif (message.number % 1000 == 1): points[message.author]["breakdown"]["1001s"] += 1
- elif (message.number % 200 == 0): points[message.author]["breakdown"]["200s"] += 1
- elif (message.number % 200 == 1): points[message.author]["breakdown"]["201s"] += 1
- elif (message.number % 100 == 0): points[message.author]["breakdown"]["100s"] += 1
- elif (message.number % 100 == 1): points[message.author]["breakdown"]["101s"] += 1
- elif (message.number in primes): points[message.author]["breakdown"]["Prime Numbers"] += 1
- elif (message.number % 2 == 1): points[message.author]["breakdown"]["Odd Numbers"] += 1
- else: points[message.author]["breakdown"]["Even Numbers"] += 1
+ if (message.number == self.messages[0].number): points[message.author_id]["breakdown"]["First Number"] += 1
+ elif (message.number % 1000 == 0): points[message.author_id]["breakdown"]["1000s"] += 1
+ elif (message.number % 1000 == 1): points[message.author_id]["breakdown"]["1001s"] += 1
+ elif (message.number % 200 == 0): points[message.author_id]["breakdown"]["200s"] += 1
+ elif (message.number % 200 == 1): points[message.author_id]["breakdown"]["201s"] += 1
+ elif (message.number % 100 == 0): points[message.author_id]["breakdown"]["100s"] += 1
+ elif (message.number % 100 == 1): points[message.author_id]["breakdown"]["101s"] += 1
+ elif (message.number in primes): points[message.author_id]["breakdown"]["Prime Numbers"] += 1
+ elif (message.number % 2 == 1): points[message.author_id]["breakdown"]["Odd Numbers"] += 1
+ else: points[message.author_id]["breakdown"]["Even Numbers"] += 1
# Create ranked leaderboard
leaderboard = []
@@ -511,7 +252,7 @@ class Countdown:
"eta": eta,
}
- def speed(self, period=timedelta(days=1), tz=timedelta(hours=0)):
+ def speed(self, period=timedelta(days=1)):
"""
Get countdown speed statistics.
@@ -519,8 +260,6 @@ class Countdown:
----------
periodLength : timedelta
The period size. The default is 1 day.
- tz : timedelta
- The timezone. The default is +0 (UTC)
Returns
-------
@@ -533,7 +272,7 @@ class Countdown:
periodStart = datetime(2018, 1, 1) # Starts on Monday, Jan 1st
for message in self.messages:
# If data point isn't in the current period
- while (message.timestamp + tz - period >= periodStart):
+ while (message.timestamp + timedelta(hours=self.timezone) - period >= periodStart):
periodStart += period
# Add new period if needed
@@ -547,30 +286,294 @@ class Countdown:
# Return speed statistics
return data
+class Prefix(Base):
+ __tablename__ = "prefix"
+
+ id = Column(Integer, primary_key=True)
+ countdown_id = Column(Integer, ForeignKey("countdown.id"))
+ countdown = relationship("Countdown", back_populates="prefixes")
+ value = Column(String)
+
+class Reaction(Base):
+ __tablename__ = "reaction"
+
+ id = Column(Integer, primary_key=True)
+ countdown_id = Column(Integer, ForeignKey("countdown.id"))
+ countdown = relationship("Countdown", back_populates="reactions")
+ number = Column(Integer)
+ value = Column(String)
+
+class Message(Base):
+ __tablename__ = "message"
+
+ id = Column(Integer, primary_key=True)
+ countdown_id = Column(Integer, ForeignKey("countdown.id"))
+ countdown = relationship("Countdown", back_populates="messages")
+ author_id = Column(Integer)
+ timestamp = Column(DateTime)
+ number = Column(Integer)
+
+# Create database
+Base.metadata.create_all(bind=engine)
+Session = sessionmaker(bind=engine)
+
+
+
+# Global variables
+data = {}
+POINT_RULES = {
+ "1000s": 1000,
+ "1001s": 500,
+ "200s": 200,
+ "201s": 100,
+ "100s": 100,
+ "101s": 50,
+ "Prime Numbers": 15,
+ "Odd Numbers": 12,
+ "Even Numbers": 10,
+ "First Number": 0,
+}
+COLORS = {
+ "error": 0xD52C42,
+ "embed": 0x248AD1,
+}
+
+
+
+# Error classes
+class MessageNotAllowedError(Exception):
+ """Raised when someone posts twice in a row."""
+ pass
+
+class MessageIncorrectError(Exception):
+ """Raised when someone posts an incorrect number."""
+ pass
+
+
+
+# Static methods
+async def getUsername(id):
+ """
+ Get a username from a user ID.
+
+ Parameters
+ ----------
+ id : int
+ The user ID.
+
+ Returns
+ -------
+ str
+ The username (ex: "user#0000").
+ """
+
+ user = await bot.fetch_user(id)
+ return f"{user.name}#{user.discriminator}"
+
+async def getNickname(server, id):
+ """
+ Get a user's nickname in a server
+
+ Parameters
+ ----------
+ server : int
+ The server ID
+ id : int
+ The user ID
+
+ Returns
+ -------
+ str
+ The nickname
+ """
+
+ return (await (bot.get_guild(server)).fetch_member(id)).nick or await getUsername(id)
+
+def getCountdown(session, id):
+ """
+ Get a countdown object
+
+ Parameters
+ ----------
+ session : sqlalchemy.orm.Session
+ The database session to use
+ id : int
+ The countdown id
+
+ Returns
+ -------
+ Countdown
+ The Countdown
+ """
+
+ return session.query(Countdown).filter(Countdown.id == id).first()
+
+def getContextCountdown(session, ctx, resortToFirst=True):
+ """
+ Get the most relevant countdown to a certain context.
+
+ Parameters
+ ----------
+ session : sqlalchemy.orm.Session
+ The database session to use
+ ctx : discord.ext.commands.Context
+ The context
+ resortToFirst : bool
+ Whether to return the 1st countdown if no relevant countdowns are found
+
+ Returns
+ -------
+ Countdown
+ The countdown
+ """
+
+ global data
+
+ if (isinstance(ctx.channel, discord.channel.TextChannel)):
+ # Countdown channel
+ countdown = getCountdown(session, ctx.channel.guild.id)
+ if (countdown): return countdown
+
+ # Server with countdown channel: get first countdown in this server that use the current prefix
+ countdown = session.query(Countdown).filter(Countdown.server_id == ctx.channel.guild.id and ctx.prefix in [x.value for x in Countdown.prefixes]).first()
+ if (countdown): return countdown
+
+ # First countdown channel
+ countdown = session.query(Countdown).first()
+ if (resortToFirst and countdown): return countdown
+ else: raise Exception("Countdown channel not found")
+
+def getPrefix(bot, ctx):
+ """
+ Get the bot prefix for a certain context.
+
+ Parameters
+ ----------
+ bot : commands.Bot
+ The bot
+ ctx : discord.ext.commands.Context
+ The context
+ """
+
+ with Session() as session:
+ # Countdown channel
+ global data
+ countdown = getCountdown(session, ctx.channel.id)
+ if (countdown and len(countdown.prefixes) > 0):
+ return [x.value for x in countdown.prefixes]
+
+ # Server with countdown channels
+ if (isinstance(ctx.channel, discord.channel.TextChannel)):
+ serverCountdowns = session.query(Countdown).filter(Countdown.server_id == ctx.channel.guild.id).all()
+ # Get list of prefixes
+ prefixes = []
+ for countdown in serverCountdowns:
+ prefixes += [x.value for x in countdown.prefixes]
+ if (len(prefixes) > 0):
+ return list(dict.fromkeys(prefixes))
+
+ # Return default prefixes
+ return data["prefixes"]
+
+def parseMessage(message):
+ """
+ Parses a countdown message from a Discord message
+
+ Parameters
+ ----------
+ message : discord.Message
+ The Discord message
+
+ Returns
+ -------
+ Message
+ """
+
+ return Message(
+ id = message.id,
+ countdown_id = message.channel.id,
+ author_id = message.author.id,
+ timestamp = message.created_at,
+ number = int(re.findall("^[0-9,]+", message.content)[0].replace(",","")),
+ )
+
+async def addMessage(countdown, rawMessage):
+ """
+ Parse a message and add it to a countdown
+
+ Notes
+ -----
+ If the message is invalid or incorrect, a reacted will be added accordingly
+
+ Parameters
+ ----------
+ countdown : Countdown
+ The countdown
+ rawMessage : discord.Message
+ The Discord message object
+
+ Returns
+ -------
+ bool
+ Whether the message was valid and added to the countdown
+ """
+
+ try:
+ # Parse message
+ message = parseMessage(rawMessage)
+
+ # Add message
+ countdown.addMessage(message)
+
+ # Mark important messages
+ if (message.number in [x.number for x in countdown.reactions]):
+ for reaction in [x for x in countdown.reactions if x.number == message.number]:
+ try:
+ await rawMessage.add_reaction(reaction.value)
+ except:
+ pass
+ if (countdown.messages[0].number >= 500 and message.number % (countdown.messages[0].number // 50) == 0):
+ await rawMessage.pin()
+ except MessageNotAllowedError:
+ await rawMessage.add_reaction("⛔")
+ return False
+ except MessageIncorrectError:
+ await rawMessage.add_reaction("❌")
+ return False
+ except:
+ return False
+ else:
+ return True
+
+async def loadCountdown(bot, countdown):
+ """
+ Loads countdown messages from a Discord countdown
+
+ Parameters
+ ----------
+ bot : commands.Bot
+ The bot to load messages with
+ countdown : Countdown
+ The countdown to load messages for
+ """
+
+ # Clear countdown
+ countdown.messages = []
+
+ # Get Discord messages
+ rawMessages = await bot.get_channel(countdown.id).history(limit=10100).flatten()
+ rawMessages.reverse()
+
+ # Add messages to countdown
+ for rawMessage in rawMessages:
+ await addMessage(countdown, rawMessage)
+
# Load countdown data
with open(os.path.join(os.path.dirname(__file__), "data.json"), "a+") as f:
f.seek(0)
- rawData = json.load(f)
- data = copy.deepcopy(rawData)
-
- # Format countdown objects
- for countdown in rawData["countdowns"]:
- data["countdowns"][countdown] = Countdown(
- countdown,
- [],
- rawData["countdowns"][countdown]["server"],
- rawData["countdowns"][countdown]["timezone"],
- rawData["countdowns"][countdown]["prefixes"],
- rawData["countdowns"][countdown]["reactions"],
- )
- for message in rawData["countdowns"][countdown]["messages"]:
- messageObj = Message(None)
- messageObj.timestamp = datetime.strptime(message["timestamp"], "%Y-%m-%d %H:%M:%S")
- messageObj.author = message["author"]
- messageObj.number = message["number"]
- data["countdowns"][countdown].messages += [messageObj]
+ data = json.load(f)
@@ -589,11 +592,19 @@ async def on_ready():
@bot.event
async def on_message(obj):
+ # Respond to @mentions
if bot.user in obj.mentions:
embed=discord.Embed(title="countdown-bot", description=f"Use `{(await bot.get_prefix(obj))[0]}help` to view help information", color=COLORS["embed"])
await obj.channel.send(embed=embed)
- if (str(obj.channel.id) in data["countdowns"] and obj.author.name != "countdown-bot"):
- await data["countdowns"][str(obj.channel.id)].parseMessage(obj)
+
+ # Parse countdown message
+ with Session() as session:
+ countdown = getCountdown(session, obj.channel.id)
+ if (countdown and obj.author.name != "countdown-bot"):
+ # Add message to countdown and commit changes
+ if (await addMessage(countdown, obj)): session.commit()
+
+ # Run commands
try:
await bot.process_commands(obj)
except:
@@ -619,44 +630,48 @@ async def activate(ctx):
Turns a channel into a countdown
"""
- # Channel is already a coutndown
- if (str(ctx.channel.id) in data["countdowns"]):
- embed = discord.Embed(title="Error", description="This channel is already a countdown", color=COLORS["error"])
- await ctx.send(embed=embed)
+ with Session() as session:
+ # Channel is already a coutndown
+ if (getCountdown(session, ctx.channel.id)):
+ embed = discord.Embed(title="Error", description="This channel is already a countdown", color=COLORS["error"])
+ await ctx.send(embed=embed)
- # Channel is a DM
- elif (not isinstance(ctx.channel, discord.channel.TextChannel)):
- embed = discord.Embed(title="Error", description="This command must be run inside a server", color=COLORS["error"])
- await ctx.send(embed=embed)
+ # Channel is a DM
+ elif (not isinstance(ctx.channel, discord.channel.TextChannel)):
+ embed = discord.Embed(title="Error", description="This command must be run inside a server", color=COLORS["error"])
+ await ctx.send(embed=embed)
- # User isn't authorized
- elif (not ctx.message.author.guild_permissions.administrator):
- embed = discord.Embed(title="Error", description="You must be an administrator to turn a channel into a countdown", color=COLORS["error"])
- await ctx.send(embed=embed)
-
- # Channel is valid
- else:
- # Create countdown channel
- data["countdowns"][str(ctx.channel.id)] = Countdown(ctx.channel.id, [], ctx.channel.guild.id, 0, data["prefixes"], {})
+ # User isn't authorized
+ elif (not ctx.message.author.guild_permissions.administrator):
+ embed = discord.Embed(title="Error", description="You must be an administrator to turn a channel into a countdown", color=COLORS["error"])
+ await ctx.send(embed=embed)
- # Send initial responce
- print(f"Activated {bot.get_channel(ctx.channel.id)} as a countdown")
- embed = discord.Embed(title=":clock3: Loading Countdown", description="@here This channel is now a countdown\nPlease wait to start counting", color=COLORS["embed"])
- msg = await ctx.send(embed=embed)
+ # Channel is valid
+ else:
+ # Create countdown
+ countdown = Countdown(
+ id = ctx.channel.id,
+ server_id = ctx.channel.guild.id,
+ timezone = 0,
+ prefixes = [Prefix(countdown_id=ctx.channel.id, value=x) for x in data["prefixes"]],
+ reactions = [],
+ messages = [],
+ )
- # Get messages
- rawMessages = await bot.get_channel(ctx.channel.id).history(limit=10100).flatten()
- rawMessages.reverse()
+ # Send initial responce
+ print(f"Activated {bot.get_channel(ctx.channel.id)} as a countdown")
+ embed = discord.Embed(title=":clock3: Loading Countdown", description="@here This channel is now a countdown\nPlease wait to start counting", color=COLORS["embed"])
+ msg = await ctx.send(embed=embed)
- # Load messages
- for rawMessage in rawMessages:
- await data["countdowns"][str(ctx.channel.id)].parseMessage(rawMessage)
- saveData(data)
+ # Load countdown
+ await loadCountdown(bot, countdown)
+ session.add(countdown)
+ session.commit()
- # Send final responce
- print(f"Loaded messages from {bot.get_channel(ctx.channel.id)}")
- embed = discord.Embed(title=":white_check_mark: Countdown Activated", description="@here This channel is now a countdown\nYou may start counting!", color=COLORS["embed"])
- await msg.edit(embed=embed)
+ # Send final responce
+ print(f"Loaded messages from {bot.get_channel(ctx.channel.id)}")
+ embed = discord.Embed(title=":white_check_mark: Countdown Activated", description="@here This channel is now a countdown\nYou may start counting!", color=COLORS["embed"])
+ await msg.edit(embed=embed)
@@ -666,23 +681,24 @@ async def analytics(ctx):
Shows all countdown analytics
"""
- # Get countdown channel
- countdown = getCountdown(ctx)
+ with Session() as session:
+ # Get countdown channel
+ countdown = getContextCountdown(session, ctx)
- # Check if countdown is empty
- if (len(countdown.messages) == 0):
- embed=discord.Embed(title=":bar_chart: Countdown Analytics", color=COLORS["error"])
- embed.description = "The countdown is empty"
- await ctx.send(embed=embed)
+ # Check if countdown is empty
+ if (len(countdown.messages) == 0):
+ embed=discord.Embed(title=":bar_chart: Countdown Analytics", color=COLORS["error"])
+ embed.description = "The countdown is empty"
+ await ctx.send(embed=embed)
- # Run analytics commands
- else:
- await contributors(ctx, "")
- await contributors(ctx, "history")
- if (len(countdown.messages) >= 2): await eta(ctx) # Countdown must have 2 messages to run eta command
- await leaderboard(ctx)
- await progress(ctx)
- await speed(ctx)
+ # Run analytics commands
+ else:
+ await contributors(ctx, "")
+ await contributors(ctx, "history")
+ if (len(countdown.messages) >= 2): await eta(ctx) # Countdown must have 2 messages to run eta command
+ await leaderboard(ctx)
+ await progress(ctx)
+ await speed(ctx)
@@ -695,69 +711,70 @@ async def config(ctx, key=None, *args):
# Create embed
embed = discord.Embed(title=":gear: Countdown Settings", color=COLORS["embed"])
- # Get countdown channel
- try:
- countdown = getCountdown(ctx, resortToFirst=False)
- except:
- embed.color = COLORS["error"]
- embed.description = "This command must be run in a countdown channel or a server with a countdown channel"
- else:
- # Get / set settings
- if (key is None):
- embed.description = f"**Countdown Channel:** <#{countdown.id}>\n"
- embed.description += f"**Command Prefixes:** `{'`, `'.join(countdown.prefixes)}`\n"
- if (countdown.timezone < 0):
- embed.description += f"**Countdown Timezone:** UTC-{-1 * countdown.timezone}\n"
- else:
- embed.description += f"**Countdown Timezone:** UTC+{countdown.timezone}\n"
- if (len(countdown.reactions) == 0):
- embed.description += f"**Reactions:** none\n"
- else:
- embed.description += f"**Reactions:**\n"
- for reaction in sorted(countdown.reactions.keys(), reverse=True):
- embed.description += f"**-** #{reaction}: {', '.join(countdown.reactions[reaction])}\n"
- elif (not ctx.message.author.guild_permissions.administrator):
- embed.color = COLORS["error"]
- embed.description = f"You must be an administrator to modify settings"
- elif (len(args) == 0):
+ with Session() as session:
+ # Get countdown channel
+ try:
+ countdown = getContextCountdown(session, ctx, resortToFirst=False)
+ except:
embed.color = COLORS["error"]
- embed.description = f"Please provide a value for the setting"
- elif (key in ["tz", "timezone"]):
- embed.description = f"Done"
- try:
- countdown.timezone = int(args[0])
- except:
+ embed.description = "This command must be run in a countdown channel or a server with a countdown channel"
+ else:
+ # Get / set settings
+ if (key is None):
+ embed.description = f"**Countdown Channel:** <#{countdown.id}>\n"
+ embed.description += f"**Command Prefixes:** `{'`, `'.join([x.value for x in countdown.prefixes])}`\n"
+ if (countdown.timezone < 0):
+ embed.description += f"**Countdown Timezone:** UTC-{-1 * countdown.timezone}\n"
+ else:
+ embed.description += f"**Countdown Timezone:** UTC+{countdown.timezone}\n"
+ if (len(countdown.reactions) == 0):
+ embed.description += f"**Reactions:** none\n"
+ else:
+ embed.description += f"**Reactions:**\n"
+ for number in list(dict.fromkeys([x.number for x in countdown.reactions])):
+ embed.description += f"**-** #{number}: {', '.join([x.value for x in countdown.reactions if x.number == number])}\n"
+ elif (not ctx.message.author.guild_permissions.administrator):
+ embed.color = COLORS["error"]
+ embed.description = f"You must be an administrator to modify settings"
+ elif (len(args) == 0):
+ embed.color = COLORS["error"]
+ embed.description = f"Please provide a value for the setting"
+ elif (key in ["tz", "timezone"]):
+ embed.description = f"Done"
try:
- countdown.timezone = float(args[0])
+ countdown.timezone = int(args[0])
+ except:
+ try:
+ countdown.timezone = float(args[0])
+ except:
+ embed.color = COLORS["error"]
+ embed.description = f"Invalid timezone: {args[0]}"
+ elif (key in ["prefix", "prefixes"]):
+ countdown.prefixes = [Prefix(countdown_id=ctx.channel.id, value=x) for x in args]
+ embed.description = f"Done"
+ elif (key in ["react"]):
+ try:
+ number = int(args[0])
+ if (number < 0):
+ embed.color = COLORS["error"]
+ embed.description = f"Number must be greater than zero"
+ elif (len(args) == 1):
+ countdown.reactions = [x for x in countdown.reactions if x.number != number]
+ embed.description = f"Removed reactions for #{number}"
+ else:
+ countdown.reactions = [x for x in countdown.reactions if x.number != number]
+ countdown.reactions += [Reaction(countdown_id=countdown.id, number=number, value=x) for x in args[1:]]
+ embed.description = f"Updated reactions for #{number}"
except:
embed.color = COLORS["error"]
- embed.description = f"Invalid timezone: {args[0]}"
- elif (key in ["prefix", "prefixes"]):
- countdown.prefixes = args
- embed.description = f"Done"
- elif (key in ["react"]):
- try:
- number = int(args[0])
- if (number < 0):
- embed.color = COLORS["error"]
- embed.description = f"Number must be greater than zero"
- elif (len(args) == 1):
- if (str(number) in countdown.reactions):
- del countdown.reactions[str(number)]
- embed.description = f"Removed reactions for #{number}"
- else:
- countdown.reactions[str(number)] = args[1:]
- embed.description = f"Updated reactions for #{number}"
- except:
+ embed.description = f"Invalid number: {args[0]}"
+ else:
embed.color = COLORS["error"]
- embed.description = f"Invalid number: {args[0]}"
- else:
- embed.color = COLORS["error"]
- embed.description = f"Setting not found: `{key}`\n"
- embed.description += f"Use `{(await bot.get_prefix(ctx))[0]}help config` to view the list of settings"
+ embed.description = f"Setting not found: `{key}`\n"
+ embed.description += f"Use `{(await bot.get_prefix(ctx))[0]}help config` to view the list of settings"
- # Save changes
- saveData(data)
+ # Save changes
+ session.commit()
# Send embed
await ctx.send(embed=embed)
@@ -770,90 +787,91 @@ async def contributors(ctx, option=""):
Shows information about countdown contributors
"""
- # Get countdown channel
- countdown = getCountdown(ctx)
+ with Session() as session:
+ # Get countdown channel
+ countdown = getContextCountdown(session, ctx)
- # Create temp file
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
- tmp.close()
+ # Create temp file
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
+ tmp.close()
- # Get stats
- stats = countdown.progress()
- contributors = countdown.contributors()
+ # Get stats
+ stats = countdown.progress()
+ contributors = countdown.contributors()
- # Create embed
- embed=discord.Embed(title=":busts_in_silhouette: Countdown Contributors", color=COLORS["embed"])
+ # Create embed
+ embed=discord.Embed(title=":busts_in_silhouette: Countdown Contributors", color=COLORS["embed"])
- # Make sure the countdown has started
- if (len(countdown.messages) == 0):
- embed.color = COLORS["error"]
- embed.description = "The countdown is empty."
- elif (option.lower() in ["h", "history"]):
- # Create figure
- fig, ax = plt.subplots()
- ax.set_xlabel("Progress")
- ax.set_ylabel("Percentage of Contributions")
- ax.yaxis.set_major_formatter(PercentFormatter())
-
- # Get historical contributor data
- authors = {}
- for author in contributors:
- authors[author["author"]] = [{"progress":0, "percentage":0, "total":0}]
- for message in countdown.messages:
- for author in authors:
- if (author == message.author):
- authors[author] += [{"progress":(stats["total"] - message.number), "percentage":(authors[author][-1]["total"] + 1)/(stats["total"] - message.number + 1) * 100, "total":authors[author][-1]["total"] + 1}]
- else:
- authors[author] += [{"progress":(stats["total"] - message.number), "percentage":(authors[author][-1]["total"] + 0)/(stats["total"] - message.number + 1) * 100, "total":authors[author][-1]["total"] + 0}]
-
- # Plot data and add legend
- for author in list(authors.keys())[:min(len(authors), 15)]:
- # Top 15 contributors get included in the legend
- ax.plot([x["progress"] for x in authors[author]], [x["percentage"] for x in authors[author]], label=await getUsername(author))
- for author in list(authors.keys())[15:max(len(authors), 15)]:
- ax.plot([x["progress"] for x in authors[author]], [x["percentage"] for x in authors[author]])
- ax.legend(bbox_to_anchor=(1,1.025), loc="upper left")
-
- # Save graph
- fig.savefig(tmp.name, bbox_inches="tight", pad_inches=0.2)
- file = discord.File(tmp.name, filename="image.png")
-
- # Add content to embed
- embed.description = f"**Countdown Channel:** <#{countdown.id}>"
- embed.set_image(url="attachment://image.png")
- elif (option == ""):
- # Create figure
- fig, ax = plt.subplots()
-
- # Add data to graph
- x = [x["author"] for x in contributors]
- y = [x["contributions"] for x in contributors]
- pieData = ax.pie(y, autopct="%1.1f%%", startangle=90)
-
- # Add legend
- ax.legend(pieData[0], [await getUsername(i) for i in x[:min(len(x), 15)]], bbox_to_anchor=(1,1.025), loc="upper left")
-
- # Save graph
- fig.savefig(tmp.name, bbox_inches="tight", pad_inches=0.2)
- file = discord.File(tmp.name, filename="image.png")
-
- # Add content to embed
- embed.description = f"**Countdown Channel:** <#{countdown.id}>"
- ranks = ""
- users = ""
- contributions = ""
- for i in range(0, min(len(x), 20)):
- ranks += f"{i+1:,}\n"
- contributions += f"{y[i]:,} *({round(y[i] / len(countdown.messages) * 100, 1)}%)*\n"
- users += f"<@{x[i]}>\n"
- embed.add_field(name="Rank",value=ranks, inline=True)
- embed.add_field(name="User",value=users, inline=True)
- embed.add_field(name="Contributions",value=contributions, inline=True)
- embed.set_image(url="attachment://image.png")
- else:
- embed.color = COLORS["error"]
- embed.description = f"Unrecognized option: `{option}`\n"
- embed.description += f"Use `{(await bot.get_prefix(ctx))[0]}help contributors` to view help information"
+ # Make sure the countdown has started
+ if (len(countdown.messages) == 0):
+ embed.color = COLORS["error"]
+ embed.description = "The countdown is empty."
+ elif (option.lower() in ["h", "history"]):
+ # Create figure
+ fig, ax = plt.subplots()
+ ax.set_xlabel("Progress")
+ ax.set_ylabel("Percentage of Contributions")
+ ax.yaxis.set_major_formatter(PercentFormatter())
+
+ # Get historical contributor data
+ authors = {}
+ for author in contributors:
+ authors[author["author"]] = [{"progress":0, "percentage":0, "total":0}]
+ for message in countdown.messages:
+ for author in authors:
+ if (author == message.author_id):
+ authors[author] += [{"progress":(stats["total"] - message.number), "percentage":(authors[author][-1]["total"] + 1)/(stats["total"] - message.number + 1) * 100, "total":authors[author][-1]["total"] + 1}]
+ else:
+ authors[author] += [{"progress":(stats["total"] - message.number), "percentage":(authors[author][-1]["total"] + 0)/(stats["total"] - message.number + 1) * 100, "total":authors[author][-1]["total"] + 0}]
+
+ # Plot data and add legend
+ for author in list(authors.keys())[:min(len(authors), 15)]:
+ # Top 15 contributors get included in the legend
+ ax.plot([x["progress"] for x in authors[author]], [x["percentage"] for x in authors[author]], label=await getUsername(author))
+ for author in list(authors.keys())[15:max(len(authors), 15)]:
+ ax.plot([x["progress"] for x in authors[author]], [x["percentage"] for x in authors[author]])
+ ax.legend(bbox_to_anchor=(1,1.025), loc="upper left")
+
+ # Save graph
+ fig.savefig(tmp.name, bbox_inches="tight", pad_inches=0.2)
+ file = discord.File(tmp.name, filename="image.png")
+
+ # Add content to embed
+ embed.description = f"**Countdown Channel:** <#{countdown.id}>"
+ embed.set_image(url="attachment://image.png")
+ elif (option == ""):
+ # Create figure
+ fig, ax = plt.subplots()
+
+ # Add data to graph
+ x = [x["author"] for x in contributors]
+ y = [x["contributions"] for x in contributors]
+ pieData = ax.pie(y, autopct="%1.1f%%", startangle=90)
+
+ # Add legend
+ ax.legend(pieData[0], [await getUsername(i) for i in x[:min(len(x), 15)]], bbox_to_anchor=(1,1.025), loc="upper left")
+
+ # Save graph
+ fig.savefig(tmp.name, bbox_inches="tight", pad_inches=0.2)
+ file = discord.File(tmp.name, filename="image.png")
+
+ # Add content to embed
+ embed.description = f"**Countdown Channel:** <#{countdown.id}>"
+ ranks = ""
+ users = ""
+ contributions = ""
+ for i in range(0, min(len(x), 20)):
+ ranks += f"{i+1:,}\n"
+ contributions += f"{y[i]:,} *({round(y[i] / len(countdown.messages) * 100, 1)}%)*\n"
+ users += f"<@{x[i]}>\n"
+ embed.add_field(name="Rank",value=ranks, inline=True)
+ embed.add_field(name="User",value=users, inline=True)
+ embed.add_field(name="Contributions",value=contributions, inline=True)
+ embed.set_image(url="attachment://image.png")
+ else:
+ embed.color = COLORS["error"]
+ embed.description = f"Unrecognized option: `{option}`\n"
+ embed.description += f"Use `{(await bot.get_prefix(ctx))[0]}help contributors` to view help information"
# Send embed
try:
@@ -875,26 +893,28 @@ async def deactivate(ctx):
Deactivates a countdown channel
"""
- # Channel isn't a countdown
- if (str(ctx.channel.id) not in data["countdowns"]):
- embed = discord.Embed(title="Error", description="This channel isn't a countdown", color=COLORS["error"])
- await ctx.send(embed=embed)
+ with Session() as session:
+ # Channel isn't a countdown
+ countdown = getCountdown(session, ctx.channel.id)
+ if (not countdown):
+ embed = discord.Embed(title="Error", description="This channel isn't a countdown", color=COLORS["error"])
+ await ctx.send(embed=embed)
- # User isn't authorized
- elif (not ctx.message.author.guild_permissions.administrator):
- embed = discord.Embed(title="Error", description="You must be an administrator to deactivate a countdown channel", color=COLORS["error"])
- await ctx.send(embed=embed)
+ # User isn't authorized
+ elif (not ctx.author.guild_permissions.administrator):
+ embed = discord.Embed(title="Error", description="You must be an administrator to deactivate a countdown channel", color=COLORS["error"])
+ await ctx.send(embed=embed)
- # Channel is valid
- else:
- # Add channel data
- del data["countdowns"][str(ctx.channel.id)]
- saveData(data)
+ # Channel is valid
+ else:
+ # Delete countdown
+ session.delete(countdown)
+ session.commit()
- # Send initial responce
- print(f"Deactivated {bot.get_channel(ctx.channel.id)} as a countdown")
- embed = discord.Embed(title=":octagonal_sign: Countdown Deactivated", description="@here This channel is no longer a countdown", color=COLORS["embed"])
- await ctx.send(embed=embed)
+ # Send responce
+ print(f"Deactivated {bot.get_channel(ctx.channel.id)} as a countdown")
+ embed = discord.Embed(title=":octagonal_sign: Countdown Deactivated", description="@here This channel is no longer a countdown", color=COLORS["embed"])
+ await ctx.send(embed=embed)
@@ -904,68 +924,69 @@ async def eta(ctx, period="24.0"):
Shows information about the estimated completion date
"""
- # Get countdown channel
- countdown = getCountdown(ctx)
+ with Session() as session:
+ # Get countdown channel
+ countdown = getContextCountdown(session, ctx)
- # Create temp file
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
- tmp.close()
+ # Create temp file
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
+ tmp.close()
- # Create embed
- embed=discord.Embed(title=":calendar: Countdown Estimated Completion Date", color=COLORS["embed"])
+ # Create embed
+ embed=discord.Embed(title=":calendar: Countdown Estimated Completion Date", color=COLORS["embed"])
- # Parse period
- try:
- period = float(period)
- except:
- embed.color = COLORS["error"]
- embed.description = "The period must be a number"
- else:
- if (len(countdown.messages) < 2):
- embed.color = COLORS["embed"]
- embed.description = "The countdown must have at least two messages"
- elif (period < 0.01):
+ # Parse period
+ try:
+ period = float(period)
+ except:
embed.color = COLORS["error"]
- embed.description = "The period cannot be less than 0.01 hours"
+ embed.description = "The period must be a number"
else:
- # Get stats
- eta = countdown.eta(timedelta(hours=period), tz=timedelta(hours=countdown.timezone))
-
- # Create figure
- fig, ax = plt.subplots()
- ax.set_xlabel("Time")
- fig.autofmt_xdate()
-
- # Add ETA data to graph
- ax.plot(eta[0], eta[1], "C0", label="Estimated Completion Date")
-
- # Add reference line graph
- ax.plot([eta[0][0], eta[0][-1]], [eta[0][0], eta[0][-1]], "--C1", label="Current Date")
-
- # Add legend
- ax.legend()
-
- # Save graph
- fig.savefig(tmp.name, bbox_inches="tight", pad_inches=0.2)
- file = discord.File(tmp.name, filename="image.png")
-
- # Calculate embed data
- maxEta = max(eta[1])
- maxDate = eta[0][eta[1].index(maxEta)]
- minEta = min(eta[1][1:])
- minDate = eta[0][eta[1].index(minEta)]
- end = eta[1][-1] + timedelta(hours=countdown.timezone)
- endDiff = eta[1][-1] - datetime.utcnow()
-
- # Add content to embed
- embed.description = f"**Countdown Channel:** <#{countdown.id}>\n\n"
- embed.description += f"**Maximum Estimate:** {maxEta.date()} (on {maxDate.date()})\n"
- embed.description += f"**Minimum Estimate:** {minEta.date()} (on {minDate.date()})\n"
- if endDiff < timedelta(seconds=0):
- embed.description += f"**Actual Completion Date:** {end.date()} ({(-1 * endDiff).days:,} days ago)\n"
+ if (len(countdown.messages) < 2):
+ embed.color = COLORS["embed"]
+ embed.description = "The countdown must have at least two messages"
+ elif (period < 0.01):
+ embed.color = COLORS["error"]
+ embed.description = "The period cannot be less than 0.01 hours"
else:
- embed.description += f"**Current Estimate:** {end.date()} ({endDiff.days:,} days from now)\n"
- embed.set_image(url="attachment://image.png")
+ # Get stats
+ eta = countdown.eta(timedelta(hours=period))
+
+ # Create figure
+ fig, ax = plt.subplots()
+ ax.set_xlabel("Time")
+ fig.autofmt_xdate()
+
+ # Add ETA data to graph
+ ax.plot(eta[0], eta[1], "C0", label="Estimated Completion Date")
+
+ # Add reference line graph
+ ax.plot([eta[0][0], eta[0][-1]], [eta[0][0], eta[0][-1]], "--C1", label="Current Date")
+
+ # Add legend
+ ax.legend()
+
+ # Save graph
+ fig.savefig(tmp.name, bbox_inches="tight", pad_inches=0.2)
+ file = discord.File(tmp.name, filename="image.png")
+
+ # Calculate embed data
+ maxEta = max(eta[1])
+ maxDate = eta[0][eta[1].index(maxEta)]
+ minEta = min(eta[1][1:])
+ minDate = eta[0][eta[1].index(minEta)]
+ end = eta[1][-1] + timedelta(hours=countdown.timezone)
+ endDiff = eta[1][-1] - datetime.utcnow()
+
+ # Add content to embed
+ embed.description = f"**Countdown Channel:** <#{countdown.id}>\n\n"
+ embed.description += f"**Maximum Estimate:** {maxEta.date()} (on {maxDate.date()})\n"
+ embed.description += f"**Minimum Estimate:** {minEta.date()} (on {minDate.date()})\n"
+ if endDiff < timedelta(seconds=0):
+ embed.description += f"**Actual Completion Date:** {end.date()} ({(-1 * endDiff).days:,} days ago)\n"
+ else:
+ embed.description += f"**Current Estimate:** {end.date()} ({endDiff.days:,} days from now)\n"
+ embed.set_image(url="attachment://image.png")
# Send embed
try:
@@ -1155,94 +1176,95 @@ async def leaderboard(ctx, user=None):
Shows the countdown leaderboard
"""
- # Get countdown channel
- countdown = getCountdown(ctx)
+ with Session() as session:
+ # Get countdown channel
+ countdown = getContextCountdown(session, ctx)
- # Get leaderboard
- leaderboard = countdown.leaderboard()
+ # Get leaderboard
+ leaderboard = countdown.leaderboard()
- # Create embed
- embed=discord.Embed(title=":trophy: Countdown Leaderboard", color=COLORS["embed"])
+ # Create embed
+ embed=discord.Embed(title=":trophy: Countdown Leaderboard", color=COLORS["embed"])
- # Make sure the countdown has started
- if (len(countdown.messages) == 0):
- embed.color = COLORS["error"]
- embed.description = "The countdown is empty."
- elif (user is None):
- # Add description
- embed.description = f"**Countdown Channel:** <#{countdown.id}>"
-
- # Add leaderboard
- ranks = ""
- points = ""
- users = ""
- for i in range(0, min(len(leaderboard), 20)):
- ranks += f"{i+1:,}\n"
- points += f"{leaderboard[i]['points']:,}\n"
- users += f"<@{leaderboard[i]['author']}>\n"
- embed.add_field(name="Rank",value=ranks, inline=True)
- embed.add_field(name="Points",value=points, inline=True)
- embed.add_field(name="User",value=users, inline=True)
-
- # Add leaderboard rules
- rules = ""
- values = ""
- for rule in POINT_RULES:
- rules += f"{rule}\n"
- values += f"{POINT_RULES[rule]} points\n"
- embed.add_field(name="Rules", value="Only 1 rule is applied towards each number", inline=False)
- embed.add_field(name="Numbers", value=rules, inline=True)
- embed.add_field(name="Points", value=values, inline=True)
- else:
- rank = None
- if (re.match("^\d+$", user) and int(user) > 0 and int(user) <= len(leaderboard)):
- # Get user from rank
- rank = int(user) - 1
- elif (re.match("^<@!\d+>$", user) and int(user[3:-1]) in [x["author"] for x in leaderboard]):
- # Get user from mention
- rank = [x["author"] for x in leaderboard].index(int(user[3:-1]))
+ # Make sure the countdown has started
+ if (len(countdown.messages) == 0):
+ embed.color = COLORS["error"]
+ embed.description = "The countdown is empty."
+ elif (user is None):
+ # Add description
+ embed.description = f"**Countdown Channel:** <#{countdown.id}>"
+
+ # Add leaderboard
+ ranks = ""
+ points = ""
+ users = ""
+ for i in range(0, min(len(leaderboard), 20)):
+ ranks += f"{i+1:,}\n"
+ points += f"{leaderboard[i]['points']:,}\n"
+ users += f"<@{leaderboard[i]['author']}>\n"
+ embed.add_field(name="Rank",value=ranks, inline=True)
+ embed.add_field(name="Points",value=points, inline=True)
+ embed.add_field(name="User",value=users, inline=True)
+
+ # Add leaderboard rules
+ rules = ""
+ values = ""
+ for rule in POINT_RULES:
+ rules += f"{rule}\n"
+ values += f"{POINT_RULES[rule]} points\n"
+ embed.add_field(name="Rules", value="Only 1 rule is applied towards each number", inline=False)
+ embed.add_field(name="Numbers", value=rules, inline=True)
+ embed.add_field(name="Points", value=values, inline=True)
else:
- # Get user from username
- for contributor in leaderboard:
- username = await getUsername(contributor["author"])
- if (username.lower().startswith(user.lower())):
- rank = leaderboard.index(contributor)
-
- if (rank == None):
- # Get user from nickname
+ rank = None
+ if (re.match("^\d+$", user) and int(user) > 0 and int(user) <= len(leaderboard)):
+ # Get user from rank
+ rank = int(user) - 1
+ elif (re.match("^<@!\d+>$", user) and int(user[3:-1]) in [x["author"] for x in leaderboard]):
+ # Get user from mention
+ rank = [x["author"] for x in leaderboard].index(int(user[3:-1]))
+ else:
+ # Get user from username
for contributor in leaderboard:
- nickname = await getNickname(countdown.server, contributor["author"])
- if (nickname.lower().startswith(user.lower())):
+ username = await getUsername(contributor["author"])
+ if (username.lower().startswith(user.lower())):
rank = leaderboard.index(contributor)
if (rank == None):
- # User not found
- embed.color = COLORS["error"]
- embed.description = f"User not found: `{user}`"
- await ctx.send(embed=embed)
- return
-
- # Add description
- embed.description = f"**Countdown Channel:** <#{countdown.id}>\n\n"
- embed.description += f"**User:** <@{leaderboard[rank]['author']}>\n"
- embed.description += f"**Rank:** #{rank + 1:,}\n"
- embed.description += f"**Total Points:** {leaderboard[rank]['points']:,}\n"
- embed.description += f"**Total Contributions:** {leaderboard[rank]['contributions']:,} *({round(leaderboard[rank]['contributions'] / len(countdown.messages) * 100, 1)}%)*\n"
-
- # Add points breakdown
- rules = ""
- points = ""
- percentage = ""
- for category in leaderboard[rank]["breakdown"]:
- rules += f"{category}\n"
- points += f"{leaderboard[rank]['breakdown'][category] * POINT_RULES[category]:,} *({leaderboard[rank]['breakdown'][category]:,})*\n"
- if (leaderboard[rank]['points'] > 0):
- percentage += f"{round(leaderboard[rank]['breakdown'][category] * POINT_RULES[category] / leaderboard[rank]['points'] * 100, 1)}%\n"
- else:
- percentage += "0%\n"
- embed.add_field(name="Category", value=rules, inline=True)
- embed.add_field(name="Points", value=points, inline=True)
- embed.add_field(name="Percentage", value=percentage, inline=True)
+ # Get user from nickname
+ for contributor in leaderboard:
+ nickname = await getNickname(countdown.server_id, contributor["author"])
+ if (nickname.lower().startswith(user.lower())):
+ rank = leaderboard.index(contributor)
+
+ if (rank == None):
+ # User not found
+ embed.color = COLORS["error"]
+ embed.description = f"User not found: `{user}`"
+ await ctx.send(embed=embed)
+ return
+
+ # Add description
+ embed.description = f"**Countdown Channel:** <#{countdown.id}>\n\n"
+ embed.description += f"**User:** <@{leaderboard[rank]['author']}>\n"
+ embed.description += f"**Rank:** #{rank + 1:,}\n"
+ embed.description += f"**Total Points:** {leaderboard[rank]['points']:,}\n"
+ embed.description += f"**Total Contributions:** {leaderboard[rank]['contributions']:,} *({round(leaderboard[rank]['contributions'] / len(countdown.messages) * 100, 1)}%)*\n"
+
+ # Add points breakdown
+ rules = ""
+ points = ""
+ percentage = ""
+ for category in leaderboard[rank]["breakdown"]:
+ rules += f"{category}\n"
+ points += f"{leaderboard[rank]['breakdown'][category] * POINT_RULES[category]:,} *({leaderboard[rank]['breakdown'][category]:,})*\n"
+ if (leaderboard[rank]['points'] > 0):
+ percentage += f"{round(leaderboard[rank]['breakdown'][category] * POINT_RULES[category] / leaderboard[rank]['points'] * 100, 1)}%\n"
+ else:
+ percentage += "0%\n"
+ embed.add_field(name="Category", value=rules, inline=True)
+ embed.add_field(name="Points", value=points, inline=True)
+ embed.add_field(name="Percentage", value=percentage, inline=True)
# Send embed
await ctx.send(embed=embed)
@@ -1267,55 +1289,56 @@ async def progress(ctx):
Shows information about countdown progress
"""
- # Get countdown channel
- countdown = getCountdown(ctx)
+ with Session() as session:
+ # Get countdown channel
+ countdown = getContextCountdown(session, ctx)
- # Create temp file
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
- tmp.close()
+ # Create temp file
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
+ tmp.close()
- # Create embed
- embed=discord.Embed(title=":chart_with_downwards_trend: Countdown Progress", color=COLORS["embed"])
+ # Create embed
+ embed=discord.Embed(title=":chart_with_downwards_trend: Countdown Progress", color=COLORS["embed"])
- # Make sure the countdown has started
- if (len(countdown.messages) == 0):
- embed.color = COLORS["error"]
- embed.description = "The countdown is empty."
- else:
- # Get progress stats
- stats = countdown.progress()
-
- # Create figure
- fig, ax = plt.subplots()
- ax.set_xlabel("Time")
- ax.set_ylabel("Progress")
- fig.autofmt_xdate()
-
- # Add data to graph
- x = [stats["start"] + timedelta(hours=countdown.timezone)] + [x["time"] + timedelta(hours=countdown.timezone) for x in stats["progress"]]
- y = [0] + [x["progress"] for x in stats["progress"]]
- ax.plot(x, y)
-
- # Save graph
- fig.savefig(tmp.name, bbox_inches="tight", pad_inches=0.2)
- file = discord.File(tmp.name, filename="image.png")
-
- # Calculate embed data
- start = (stats["start"] + timedelta(hours=countdown.timezone)).date()
- startDiff = (datetime.utcnow() - stats["start"]).days
- end = (stats["eta"] + timedelta(hours=countdown.timezone)).date()
- endDiff = stats["eta"] - datetime.utcnow()
-
- # Add content to embed
- embed.description = f"**Countdown Channel:** <#{countdown.id}>\n\n"
- embed.description += f"**Progress:** {stats['total'] - stats['current']:,} / {stats['total']:,} ({round(stats['percentage'], 1)}%)\n"
- embed.description += f"**Average Progress per Day:** {round(stats['rate']):,}\n"
- embed.description += f"**Start Date:** {start} ({startDiff:,} days ago)\n"
- if endDiff < timedelta(seconds=0):
- embed.description += f"**End Date:** {end} ({(-1 * endDiff).days:,} days ago)\n"
+ # Make sure the countdown has started
+ if (len(countdown.messages) == 0):
+ embed.color = COLORS["error"]
+ embed.description = "The countdown is empty."
else:
- embed.description += f"**Estimated End Date:** {end} ({endDiff.days:,} days from now)\n"
- embed.set_image(url="attachment://image.png")
+ # Get progress stats
+ stats = countdown.progress()
+
+ # Create figure
+ fig, ax = plt.subplots()
+ ax.set_xlabel("Time")
+ ax.set_ylabel("Progress")
+ fig.autofmt_xdate()
+
+ # Add data to graph
+ x = [stats["start"] + timedelta(hours=countdown.timezone)] + [x["time"] + timedelta(hours=countdown.timezone) for x in stats["progress"]]
+ y = [0] + [x["progress"] for x in stats["progress"]]
+ ax.plot(x, y)
+
+ # Save graph
+ fig.savefig(tmp.name, bbox_inches="tight", pad_inches=0.2)
+ file = discord.File(tmp.name, filename="image.png")
+
+ # Calculate embed data
+ start = (stats["start"] + timedelta(hours=countdown.timezone)).date()
+ startDiff = (datetime.utcnow() - stats["start"]).days
+ end = (stats["eta"] + timedelta(hours=countdown.timezone)).date()
+ endDiff = stats["eta"] - datetime.utcnow()
+
+ # Add content to embed
+ embed.description = f"**Countdown Channel:** <#{countdown.id}>\n\n"
+ embed.description += f"**Progress:** {stats['total'] - stats['current']:,} / {stats['total']:,} ({round(stats['percentage'], 1)}%)\n"
+ embed.description += f"**Average Progress per Day:** {round(stats['rate']):,}\n"
+ embed.description += f"**Start Date:** {start} ({startDiff:,} days ago)\n"
+ if endDiff < timedelta(seconds=0):
+ embed.description += f"**End Date:** {end} ({(-1 * endDiff).days:,} days ago)\n"
+ else:
+ embed.description += f"**Estimated End Date:** {end} ({endDiff.days:,} days from now)\n"
+ embed.set_image(url="attachment://image.png")
# Send embed
try:
@@ -1337,31 +1360,25 @@ async def reload(ctx):
Reloads the countdown cache
"""
- if (str(ctx.channel.id) in data["countdowns"]):
- # Send inital responce
- print(f"Reloading messages from {bot.get_channel(ctx.channel.id)}")
- embed = discord.Embed(title=":clock3: Reloading Countdown Cache", description="Please wait to continue counting.", color=COLORS["embed"])
- msg = await ctx.channel.send(embed=embed)
-
- # Get messages
- rawMessages = await bot.get_channel(ctx.channel.id).history(limit=10100).flatten()
- rawMessages.reverse()
-
- # Create countdown
- data["countdowns"][str(ctx.channel.id)].messages = []
-
- # Load messages
- for rawMessage in rawMessages:
- await data["countdowns"][str(ctx.channel.id)].parseMessage(rawMessage)
- saveData(data)
-
- # Send final responce
- print(f"Reloaded messages from {bot.get_channel(ctx.channel.id)}")
- embed = discord.Embed(title=":white_check_mark: Countdown Cache Reloaded", description="Done! You may continue counting!", color=COLORS["embed"])
- await msg.edit(embed=embed)
- else:
- embed = discord.Embed(title="Error", description="This command must be used in a countdown channel", color = COLORS["error"])
- await ctx.channel.send(embed=embed)
+ with Session() as session:
+ countdown = getCountdown(session, ctx.channel.id)
+ if (countdown):
+ # Send inital responce
+ print(f"Reloading messages from {bot.get_channel(ctx.channel.id)}")
+ embed = discord.Embed(title=":clock3: Reloading Countdown Cache", description="Please wait to continue counting.", color=COLORS["embed"])
+ msg = await ctx.channel.send(embed=embed)
+
+ # Reload messages
+ await loadCountdown(bot, countdown)
+ session.commit()
+
+ # Send final responce
+ print(f"Reloaded messages from {bot.get_channel(ctx.channel.id)}")
+ embed = discord.Embed(title=":white_check_mark: Countdown Cache Reloaded", description="Done! You may continue counting!", color=COLORS["embed"])
+ await msg.edit(embed=embed)
+ else:
+ embed = discord.Embed(title="Error", description="This command must be used in a countdown channel", color = COLORS["error"])
+ await ctx.channel.send(embed=embed)
@@ -1371,61 +1388,62 @@ async def speed(ctx, period="24.0"):
Shows information about countdown speed
"""
- # Get countdown channel
- countdown = getCountdown(ctx)
+ with Session() as session:
+ # Get countdown channel
+ countdown = getContextCountdown(session, ctx)
- # Create temp file
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
- tmp.close()
+ # Create temp file
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
+ tmp.close()
- # Create embed
- embed=discord.Embed(title=":stopwatch: Countdown Speed", color=COLORS["embed"])
+ # Create embed
+ embed=discord.Embed(title=":stopwatch: Countdown Speed", color=COLORS["embed"])
- # Parse period
- try:
- period = float(period)
- except:
- embed.color = COLORS["error"]
- embed.description = "The period must be a number"
- else:
- if (len(countdown.messages) == 0):
- embed.color = COLORS["error"]
- embed.description = "The countdown is empty."
- elif (period < 0.01):
+ # Parse period
+ try:
+ period = float(period)
+ except:
embed.color = COLORS["error"]
- embed.description = "The period cannot be less than 0.01 hours"
+ embed.description = "The period must be a number"
else:
- # Get stats
- stats = countdown.progress()
- period = timedelta(hours=period)
- speed = countdown.speed(period, tz=timedelta(hours=countdown.timezone))
-
- # Create figure
- fig, ax = plt.subplots()
- ax.set_xlabel("Time")
- ax.set_ylabel("Progress per Period")
- fig.autofmt_xdate()
-
- # Add data to graph
- for i in range(0, len(speed[0])):
- ax.bar(speed[0][i], speed[1][i], width=period, align="edge", color="#1f77b4")
-
- # Save graph
- fig.savefig(tmp.name, bbox_inches="tight", pad_inches=0.2)
- file = discord.File(tmp.name, filename="image.png")
-
- # Add content to embed
- embed.description = f"**Countdown Channel:** <#{countdown.id}>\n\n"
- embed.description += f"**Period Size:** {period}\n"
- if (len(countdown.messages) > 1):
- rate = (stats['total'] - stats['current'])/((countdown.messages[-1].timestamp - countdown.messages[0].timestamp) / period)
+ if (len(countdown.messages) == 0):
+ embed.color = COLORS["error"]
+ embed.description = "The countdown is empty."
+ elif (period < 0.01):
+ embed.color = COLORS["error"]
+ embed.description = "The period cannot be less than 0.01 hours"
else:
- rate = 0
- embed.description += f"**Average Progress per Period:** {round(rate):,}\n"
- embed.description += f"**Record Progress per Period:** {max(speed[1]):,}\n"
- embed.description += f"**Last Period Start:** {speed[0][-1]}\n"
- embed.description += f"**Progress during Last Period:** {speed[1][-1]:,}\n"
- embed.set_image(url="attachment://image.png")
+ # Get stats
+ stats = countdown.progress()
+ period = timedelta(hours=period)
+ speed = countdown.speed(period)
+
+ # Create figure
+ fig, ax = plt.subplots()
+ ax.set_xlabel("Time")
+ ax.set_ylabel("Progress per Period")
+ fig.autofmt_xdate()
+
+ # Add data to graph
+ for i in range(0, len(speed[0])):
+ ax.bar(speed[0][i], speed[1][i], width=period, align="edge", color="#1f77b4")
+
+ # Save graph
+ fig.savefig(tmp.name, bbox_inches="tight", pad_inches=0.2)
+ file = discord.File(tmp.name, filename="image.png")
+
+ # Add content to embed
+ embed.description = f"**Countdown Channel:** <#{countdown.id}>\n\n"
+ embed.description += f"**Period Size:** {period}\n"
+ if (len(countdown.messages) > 1):
+ rate = (stats['total'] - stats['current'])/((countdown.messages[-1].timestamp - countdown.messages[0].timestamp) / period)
+ else:
+ rate = 0
+ embed.description += f"**Average Progress per Period:** {round(rate):,}\n"
+ embed.description += f"**Record Progress per Period:** {max(speed[1]):,}\n"
+ embed.description += f"**Last Period Start:** {speed[0][-1]}\n"
+ embed.description += f"**Progress during Last Period:** {speed[1][-1]:,}\n"
+ embed.set_image(url="attachment://image.png")
# Send embed
try:
diff --git a/requirements.txt b/requirements.txt
@@ -1,2 +1,3 @@
discord
matplotlib
+sqlalchemy
diff --git a/setup.py b/setup.py
@@ -6,7 +6,6 @@ import os
with open(os.path.join(os.path.dirname(__file__), "data.json"), "w") as f:
data = {
"token": "YOUR_TOKEN_HERE",
- "prefixes": ["c."],
- "countdowns": {}
+ "prefixes": ["c."]
}
json.dump(data, f)