Building Discord moderation log app using Streamlit
Hey, community! 👋
My name is Kanak, and I'm a Cloud Quality and Reliability Intern at Zscaler. As an AI enthusiast, I'm always excited to explore new technologies and tools that can help me solve complex problems and make data-driven decisions. Streamlit, an open-source Python library, has been a game-changer for my data-related projects.
One of the biggest advantages of using Streamlit is its ability to easily build web applications for data science and machine learning projects without worrying about the frontend/backend or deployment. It's especially useful in hackathons where time is of the essence, and I can focus on creating the project's essential features.
Streamlit has a fantastic and thriving community of data enthusiasts, developers, and makers pushing the boundaries of what is possible with Streamlit. I've been part of this community as a Discord community moderator for over six months now and have been constantly amazed by all the incredible projects created by everyone.
With the recent launch of editable dataframes, Streamlit has taken dataframe interactivity to the next level. In this post, you'll see how to build simple moderation actions log app for a Discord server using Streamlit and editable dataframes.
If you want to jump right in, here's a sample app and a repo code.
The moderation log app will have three main features:
- Search for a specific user
- Add a new entry
- See/edit the complete database
Tutorial
To build this app, we'll use the following libraries:
- Streamlit — for building the web application
- Pandas — for working with the data
- NumPy — for handling NULL values
- Cloudinary — for storing the data in the cloud
The app will be divided into three tabs, each containing one of the three features. Let's get started by importing the necessary libraries and setting up the app's configuration:
import streamlit as st
import pandas as pd
import numpy as np
import cloudinary
from cloudinary.uploader import upload
from cloudinary.api import resource
st.set_page_config(
page_title="Discord Moderation Log",
layout="wide"
)
st.header("Discord Moderation Log")
st.write("This is a simple moderation actions log app for the Streamlit Discord server.")
Start by setting the page title and layout of the app and creating a header and sub-header to provide some context for the app.
Next, configure your Cloudinary API credentials. Cloudinary is a cloud-based image and video management platform that you'll use to store your data. To securely store our API credentials, use Streamlit's secrets
module:
cloudinary.config(
cloud_name = st.secrets["cloud_name"],
api_key = st.secrets["api_key"],
api_secret = st.secrets["api_secret"],
secure = True
)
The secrets
module is used to access secrets that are stored securely on Streamlit's servers. Create a file called secrets.toml
in the root directory of your project with the following format:
[cloudinary]
cloud_name = "your_cloud_name"
api_key = "your_api_key"
api_secret = "your_api_secret"
Next, define a function to read and save your data:
def read_df():
try:
df = pd.read_feather("moderation_log.feather")
return df
except:
df = pd.DataFrame(columns=["Discord Username", "Action Taken", "Date", "Action Taken By", "Reason"])
return df
def save_to_cloudinary(df):
df = df.reset_index(drop=True)
df.to_feather("moderation_log.feather")
object = upload("moderation_log.feather", public_id="moderation_log.feather", resource_type="raw", overwrite=True)
The read_df
function reads a feather file from Cloudinary if it exists or creates an empty dataframe if it doesn't. This function allows you to maintain the state of our moderation log across sessions.
The save_to_cloudinary
function overwrites the existing feather file on the cloud and assigns a public ID to the uploaded data. This makes it easy to read the uploaded file in subsequent sessions, making it easier to collaborate with other moderators and maintain a consistent record of all actions.
Note that you'll use the feather format, a lightweight binary file format that is faster to read and write than other formats like CSV.
Now, let's move on to the tabs and implement each of the app's features.
1. Search for a specific user
The first tab of the app is dedicated to searching for a specific user and making changes to their data. Ask the user to provide the Discord username to search for. Once the user inputs the username, load the database from Cloudinary and filter the rows based on the username. If the user is not found in the database, display a message to add a new entry. Otherwise, display the specific user's data in an editable data frame using the experimental_data_editor
function. The user can change and save the data to the database by clicking the "Save Changes" button.
You'll also perform some validation checks before saving the changes, such as checking for NULL values and preventing changes to the username:
tab1, tab2, tab3 = st.tabs(["Search for a specific user", "Add New Entry", "See/Edit Complete Database"])
with tab1:
username = st.text_input("Enter Discord Username to search for")
if username:
df = read_df()
user_data = df[df["Discord Username"] == username]
if(len(user_data)==0):
st.write("Given user is a first time offender, please add new entry")
else:
specific_user_data = st.experimental_data_editor(user_data, key="specific data", use_container_width=True)
if st.button("Save Changes", key="update specifc data"):
# replace empty strings / complete whitespaces with NaN
temp_specific_user_data = specific_user_data.replace(r'^\s*$', np.nan, regex=True)
temp_specific_user_data = temp_specific_user_data.dropna(how='all') # drop rows with all NaN values
x = list(set(temp_specific_user_data["Discord Username"]))
if len(temp_specific_user_data) == 0: # All rows of user deleted
df = df[df["Discord Username"] != username]
save_to_cloudinary(df)
st.success("Changes Saved Successfully", icon="✅")
elif temp_specific_user_data.isnull().values.any():
st.error("NULL values not allowed in the Database", icon="🚨")
elif len(x)>1 or x[0]!=username:
st.error("Username Change Not Allowed", icon="🚨")
else:
df = df[df["Discord Username"] != username]
df = pd.concat([df, specific_user_data], ignore_index=False)
save_to_cloudinary(df)
st.success("Changes Saved Successfully", icon="✅")
2. Add a new entry
Create a form with input fields for the various details to store in our database. Use the st.form
context manager to create the form and add the input fields:
with tab2:
with st.form(key="new entry form"):
c1, c2 = st.columns(2)
discord_username = c1.text_input("Enter Discord Username")
action_taken = c2.text_input("Enter Action Taken")
date = c1.text_input("Enter Date")
action_taken_by = c2.text_input("Enter Action Taker's Name")
reason = st.text_area("Enter Reason")
submitted = st.form_submit_button("Add Entry")
The st.columns
method allows you to arrange your input fields side-by-side in two columns. Use the text_input
method to create text input fields for the "Discord Username," "Action Taken," "Date," and "Action Taker's Name" fields. Use the text_area
method to create a larger input field for the "Reason" field.
Finally, use the st.form_submit_button
method to create an "Add Entry" button that the user can click to add a new entry to the database:
if submitted:
if len(discord_username) == 0 or \
len(action_taken) == 0 or \
len(date) == 0 or \
len(action_taken_by) == 0 or \
len(reason) == 0:
st.error("Please fill all the fields and re-submit the form", icon="🚨")
else:
df = read_df()
df.loc[len(df)] = [discord_username, action_taken, date, action_taken_by, reason]
save_to_cloudinary(df)
st.success("New Entry Added Successfully", icon="✅")
In the above code, you're checking if all input fields are filled out when the "Add Entry" button is clicked by checking the length of the strings. If any field is empty, display an error message using the st.error
method, and prevent the user from adding an incomplete entry to the database. If all fields are completed, add the new entry to the database, display a success message, and save the updated dataframe to Cloudinary.
3. See/edit the complete database
Display the complete database of all offenders, along with an option to edit their data. Use the st.experimental_data_editor
function to create an editable table:
with tab3:
st.warning("Please be careful while making changes", icon="⚠️")
df = read_df()
edited_df = st.experimental_data_editor(df, num_rows="dynamic", key="Full DB", use_container_width=True)
if st.button("Save Changes", key="save complete db"):
# replace empty strings / complete whitespaces with NaN
temp_edited_df = edited_df.replace(r'^\\s*$', np.nan, regex=True)
temp_edited_df = temp_edited_df.dropna(how='all') # drop rows with all NaN values
if len(temp_edited_df) == 0:
st.error("Database cannot be empty", icon="🚨")
elif temp_edited_df.isnull().values.any():
st.error("NULL values not allowed in the Database", icon="🚨")
else:
save_to_cloudinary(temp_edited_df)
st.success("Changes Saved Successfully", icon="✅")
To create an editable table of the entire database, read the complete database from Cloudinary using the st.experimental_data_editor
function. This function allows users to modify the data. Set the num_rows
parameter to dynamic
to allow for additions and deletions of existing rows.
Once the user has finished editing the table, they can save the changes to Cloudinary by clicking the "Save Changes" button. Perform the same checks for empty and null values as in the "Specific User" section. If these checks pass, save the edited dataframe to Cloudinary using the save_to_cloudinary
function and display a success message:
Wrapping up
Congratulations on completing this tutorial! I hope that you found it helpful and that it gave you a good understanding of how to build CRUD applications with Streamlit.
Remember that this is just the beginning, and there is much more you can do to enhance and customize your app. You should explore the Streamlit documentation and community to learn more about the library and its capabilities.
If you have any questions, please post them in the comments below or contact me via Twitter, LinkedIn, or GitHub.
Happy Streamlit-ing! 🎈