Building Discord moderation log app using Streamlit

Kanak Mittal
Streamlit
Published in
7 min readApr 10, 2023

--

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:

  1. Search for a specific user
  2. Add a new entry
  3. See/edit the complete database

Tutorial

To build this app, we'll use the following libraries:

  1. Streamlit — for building the web application
  2. Pandas — for working with the data
  3. NumPy — for handling NULL values
  4. 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="✅")
Specific User Page (not allowing NULL entries to be saved)

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.

The Add New Entry Page

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:

See/Edit the complete database page

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! 🎈

--

--