Integrating Etsy sale alerts to Discord

Discord bot sending an alert for a new sale with a gif of Claude from the Fire Emblem franchise
Hey Claude! 😉

Initial Planning

Authenticated read/write access to data belonging to the member who created the application. This includes reading data like Receipts and Billing as well as creating, editing or deleting a shop’s Listings. If necessary, access can be provided upon request for a small number of test users in addition to the member who owns the app.

My favorite WicksByWerby candle (photo from the WicksByWerby Etsy shop)

General Architecture

System Diagram
previous_inventory = read_last_inventory()
current_inventory = read_current_inventory_from_etsy()
diffs = compare(previous_inventory, current_inventory)
for diff in diffs:
send_discord_message(diff['name'])
# if the inventory has changed, write out the new one to
# our database
if diff:
write(current_inventory)

Step 1: Querying the Etsy API

class Etsy:
api_key = "SECRET ETSY API KEY"
store_name = "wicksbywerby"
def request_listings():
"""Query the etsy shop for the latest listings"""
endpoint = "https://openapi.etsy.com/v2/shops/{self.shop_name}"
response = http_get(endpoint,
params={
"includes": "Listings",
"api_key": api_key
})
return response
def transform_data(listings):
"""Transform raw etsy response data. We are only interested
in items that are active or sold out, and also only
interested in quantities"""
inventory_state = {}
for item in listings():
if item['state'] == 'active' or item['state'] == 'sold_out':
inventory_state[item['listing_id'] = {
"listing_id": item['listing_id'],
"name": item['title'],
"quantity": item['quantity']
}
return inventory_state
def get_inventory_state():
"""Just a combination of the other two functions"""
listings = request_listings()
return transform_data(listings)

Step 2: Comparing Listings

def get_inventory_diff(prev_inventory, cur_inventory):
"""Compare previous and current inventories and return
a dictionary of diffs, keyed by id of the listing. Only
return diffs where inventory has decreased, indicating
a sale."""
diffs = {}
for listing_id in prev_inventory:
name = prev_inventory[listing_id]['name']
prev_quantity = prev_inventory[listing_id]['quantity']
cur_quantity = cur_inventory[listing_id]['quantity']
if prev_quantity > cur_quantity:
diffs[listing_id] = {
'num_sales': cur_quantity - prev_quantity,
'name': name
}
return diffs
class Database:
data_dir = "/data"
inventory_file = "inventory.json"
def write_inventory(inventory):
file_obj = open(inventory_file)
file_obj.write(inventory)
def get_inventory():
file_obj = open(inventory_file)
return file_obj.read()

Step 3: Post to Discord

class Discord:
webhook_url = "DISCORD WEBHOOK URL"

def send_sales_message(message):
"""Post a message to Discord with an image"""
payload = {
"username": "Sale Alerter",
"avatar_url": "link_to_image",
"content": message,
}
http_post(webhook_url, json=payload)

Putting it all together

# get our data
previous_inventory = Database.get_inventory()
current_inventory = Etsy.get_inventory_state()
# compare the data
diffs = get_inventory_diff(previous_inventory, current_inventory)
# send a message for each inventory diff
for listing_id in diffs:
item = diffs[listing_id]
msg = "Shop just sold {item['num_sales']} of {item['name']}!!"
Discord.send_sale_message(msg)
# write out inventory for next run
if diffs:
Database.write_inventory(current_inventory)

Deploying

# crontab# once every minute: 
# 1. go to the directory where our code lives
# 2. activate the Python virtual environment
# 3. run the program and output logs to a file
*/1 * * * * cd /sales-alerter && venv/bin/python3 -m sales.main >> /sales-alerter/logs.txt 2>&1

Limitations

Extra Credit

# listing_images.json{
"listing_id": {
"name": "Zagreus",
"images": [
"https://yayzagreus.fakeurl.com/hotfoot.jpg",
"https://zagreusfanclub.fakeurl.com/pom_of_power.jpg"
]
}
...
}
def get_image_for_item(listing_id):
"""Read in our list of potential images and return one
random URL"""

all_listing_images = read(listing_images.json)
item_image_urls = all_listing_images[listing_id]['images']
return random(item_image_urls)
# get our data
previous_inventory = Database.get_inventory()
current_inventory = Etsy.get_inventory_state()
# compare the data
diffs = get_inventory_diff(previous_inventory, current_inventory)
# send a message for each inventory diff
for listing_id in diffs:
item = diffs[listing_id]
msg = "Shop just sold {item['num_sales']} of {item['name']}!!"
image_url = get_image_for_item(listing_id)
Discord.send_sale_message(msg, image_url)
# write out inventory for next run
if diffs:
Database.write_inventory(current_inventory)
class Discord:
...
def send_sales_message(message, image_url):
"""Post a message to Discord with an image"""
payload = {
"username": "Sale Alerter",
"avatar_url": "link_to_image",
"content": message,
"embeds": {
"image": {"url": image_url}
}

}
New Hades Bundle Sale! Embedded image from the Supergiant Games Twitter account

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store