diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/discord-webhook-data-limit-check.sh b/discord-webhook-data-limit-check.sh new file mode 100755 index 0000000..91ec1cc --- /dev/null +++ b/discord-webhook-data-limit-check.sh @@ -0,0 +1,218 @@ +#!/bin/bash + +#### SCRIP USAGE FUNCTION #### + +# Usage function. +usage() { + echo "" + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " -m, --message The Discord webhook message JSON to be checked." + echo " -d, --debug Turns on console output" + echo " -h, --help Show this help message and exit." + echo "" + echo "This script will check a Discord webhook message JSON string against content" + echo "limits set by Discord. The message data must be within the limits in order" + echo "to be successfully sent. The script will return an exit number based on the" + echo "results of the check." + echo "" + echo " 0) The message data is within the limimts." + echo " 1) The message data is outside limits, but can be sent in multiple messages." + echo " 2) The message data is outside limits and cannot be sent." + echo "" + echo "Refer to Discord Webook documentation for more details on the webhook limits:" + echo "" + echo " https://birdie0.github.io/discord-webhooks-guide/other/field_limits.html" + echo "" +} + +#### PARSE ARGUMENTS #### + +# Parsed from command line arguments. +while [[ $# -gt 0 ]]; do + case "$1" in + -m|--message) + jsonMessage="$2" + shift 2 + ;; + -d|--debug) + debug=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Invalid option: $1" >&2 + usage + exit 2 + ;; + esac +done + +# Check if JSON is provided and not empty +if [ -z "${jsonMessage}" ]; then + echo "Error: The --message option is mandatory." >&2 + usage + exit 2 +fi + +#### INITIALIZATION & PARAMETERS #### + +# Character lenght limits & object count limits (the order is important!) +limits=( + 'Username .username 80 char 1' + 'Content .content 2000 char 1' + 'Embeds .embeds 10 obj 0' + 'Author .embeds[].author.name 256 char 1' + 'Title .embeds[].title 256 char 1' + 'Description .embeds[].description 1024 char 1' + 'Fields .embeds[].fields 25 obj 1' + 'Name .embeds[].fields[].name 256 char 1' + 'Value .embeds[].fields[].value 1024 char 1' + 'Footer .embeds[].footer.text 2048 char 1' + 'Totals na 6000 char 1' + 'Total na 6000 char 0' +) + +# Set limit parameter names +limitParameters="name section value type critical count" + +# Initialize result variables +criticalResult=false +outsideLimits=false +results="" +totalCharactersAllEmbeds=0 + +#### LIMIT CHECK & RESULTS FUNCTION #### + +# Get count, check against limit and update results +function updateResults() { + + local indexedSection="$1" + + # Get the object/character count if section is an object + [ "${indexedSection}" != "na" ] && count=$(jq "$indexedSection | length" <<< "${jsonMessage}") + + # Output info if debug enabled + [ ${debug} ] && echo "${name}: ${count} / ${value}" >&2 + + # Compare count with limit value + if [[ ${count} -gt ${value} ]]; then + + # Set 'outside limit' flag + outsideLimits=true + + # Check if limit is a critical one + if [ "${critical}" == "1" ]; then + + # Set 'critical limit' flag + criticalResult=true + + fi + + # Append to result if outside limit + results+="${name} ${indexedSection} ${value} ${type} ${critical} ${count}\n" + + fi + +} + +#### CHECK CONTENT #### + +# Check content agains each limit +for limit in "${limits[@]}"; do + + # Read parameters from limit + IFS=' ' read -r $limitParameters <<< "$limit" + + # Check if limit relates to the embeds section + if [[ "${section}" == *"embeds[]"* ]]; then + + # Check each embed section individually for applicable limits + for (( i=0; i<$(jq ".embeds | length" <<< "${jsonMessage}"); i++ )); do + + # Inject array index in embeds element for correct parsing + embedSection="${section/embeds[]/embeds[${i}]}" + + # Check if section is a field section as this is an array + if [[ "${section}" == *"fields[]"* ]]; then + + # Check each field section individually for applicable limits + for (( j=0; j<$(jq ".embeds[$i].fields | length" <<< "${jsonMessage}"); j++ )); do + + # Inject array index in fields element for correct parsing + fieldSection="${embedSection/fields[]/fields[${j}]}" + + # Check limits and update results + updateResults "${fieldSection}" + + # Add to total characters if character type + [ "${type}" == "char" ] && ((embedTotals[$i]+=count)) + + done + + else + + # Check limits and update results + updateResults "${embedSection}" + + # Add to total characters if character type + [ "${type}" == "char" ] && ((embedTotals[$i]+=count)) + + fi + + done + + elif [[ "${name}" == "Totals" ]]; then + + # Read the total character count for each embed section + for count in "${embedTotals[@]}"; do + + # Check agains limit and update result + updateResults "${section}" ${count} + + # Add to the overall total character count + ((totalCharactersAllEmbeds+=count)) + + done + + elif [[ "${name}" == "Total" ]]; then + + # Get the total character count for all embeds + count=${totalCharactersAllEmbeds} + + # Check agains limit and update result + updateResults "${section}" ${count} + + else + + # Check agains limit and update result + updateResults "${section}" + + fi + + +done + +#### RETURN RESULTS #### + +# Output results +[ ${debug} ] && [ -n "${results}" ] && echo -e "${results%??}" >&2 + +# Exit with appropriate status +if ${criticalResult}; then + + exit 2 + +elif ${outsideLimits}; then + + exit 1 + +else + + exit 0 + +fi \ No newline at end of file diff --git a/discord-webhook.sh b/discord-webhook.sh new file mode 100755 index 0000000..98e4653 --- /dev/null +++ b/discord-webhook.sh @@ -0,0 +1,191 @@ +#!/usr/bin/bash + +#### SET PATHS #### + +# Get path to where script is located. +scriptPath=$(echo "${0%/*}") + +# Set other paths. +limitCheckScrip=${scriptPath}/discord-webhook-data-limit-check.sh +lastMessageFile=${scriptPath}/discord-webhook-last-message.json + +#### SCRIP USAGE FUNCTION #### + +# Usage function. +usage() { + echo "" + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " -c, --content Set the content of the Discord message." + echo " -e, --embeds Set the embeds of the Discord message." + echo " -f, --file Set the file attachment path (optional)." + echo " -d, --debug Turns on console output" + echo " -h, --help Show this help message and exit." + echo "" + echo "Refer to the Discord documentation for more information on Webhooks" + echo "" + echo " https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" + echo "" +} + +### SEND WEBHOOK FUNCTION ### + +# Send Discord notification with or without payloaad. +sendWebhook() { + + local discordJsonData="$1" + local includeAttachment="$2" + + # Write last message that was attmepted to be sent to file + echo "${discordJsonData}" > ${lastMessageFile} + + # Check if attachment pathe exits and if is should be included in the message + if [ -z "${discordAttachmentPath}" ] || [ "${includeAttachment}" = "false" ]; then + + # Send message without attachment + echo ${discordWebhookBase}"/"${discordID}"/"${discordToken} + echo "$discordJsonData" + response=$(curl -H "Content-Type: application/json" -d "$discordJsonData" ${discordWebhookBase}"/"${discordID}"/"${discordToken}) + + else + + # Send message with attachment + response=$(curl -s -F payload_json="${discordJsonData}" -F "file1=@${discordAttachmentPath}" ${discordWebhookBase}"/"${discordID}"/"${discordToken}) + + fi + + # Check if curl was successful + if [ $? -ne 0 ]; then + echo "Error: Failed to send webhook message." >&2 + exit 1 + fi + + # Output curl reposnse if debug enabled + [ ${debug} ] && echo "${response}" >&2 + +} + +#### PARSE ARGUMENTS #### + +# Parsed from command line arguments. +while [[ $# -gt 0 ]]; do + case "$1" in + -c|--content) + discordMessageContent="$2" + shift 2 + ;; + -e|--embeds) + discordMessageEmbeds="$2" + shift 2 + ;; + -f|--file) + discordAttachmentPath="$2" + shift 2 + ;; + -d|--debug) + debug=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Invalid option: $1" >&2 + usage + exit 1 + ;; + esac +done + +# Check if JSON is provided and not empty +if [ -z "${discordMessageContent}" ] && [ -z "${discordMessageEmbeds}" ]; then + echo "Error: Either a message 'content' or message 'embed' is required." >&2 + usage + exit 1 +fi + +#### DISCORD VARIABLES #### + +# Discord webhook address base. +discordWebhookBase="https://discord.com/api/webhooks" + +# Import secret variables. +source ${scriptPath}/discord-variables.sh + +# Discord secret variables. +discordToken=${DISCORD_TOKEN} +discordID=${DISCORD_ID} +discordUsername=${DISCORD_USER} +discordAvatarURL=${DISCORD_AVATAR_URL} +discordRoleID=${DISCORD_ROLE_ID} + +#### REPLACE ROLES #### + +# Replace @admin mention with correct ID. +discordMessageEmbeds=$(echo "${discordMessageEmbeds}" | sed 's/\@admin/\<\@\&'${discordRoleID}'/g') + +#### BUILD DISCORD MESSAGE #### + +# Complete the Discord JSON string. +discordJson='{ + "username":"'"${discordUsername}"'", + "content":"'"${discordMessageContent}"'", + "avatar_url":"'"${discordAvatarURL}"'", + "embeds": [ '${discordMessageEmbeds%?}' ] +}' + +#### MESSAGE LIMIT CHECK & SEND #### + +# Call script to perform limit checks (pass on debug argument if debug enabled) +${limitCheckScrip} -m "${discordJson}" ${debug:+-d} + +# Send full, split or drop message based in limit check +case ${?} in + + # Send full message + 0) + # Output info if debug enabled + [ ${debug} ] && echo "Content within limits. Sending full webhook message." >&2 + + # Send full Json + sendWebhook "${discordJson}" true + ;; + + # Split message in multiple webhooks + 1) + # Output info if debug enabled + [ ${debug} ] && echo "Content to large, but within manageable limits. Splitting webhook message" >&2 + + # Remove embeds section + discordJsonMinusEmbeds=$(jq "del(.embeds)" <<< "${discordJson}") + + # Send message without embeds + sendWebhook "${discordJsonMinusEmbeds}" true + + # Get number of embeds in original message + embedsCount=$(jq ".embeds | length" <<< "${discordJson}") + + # Send each embed as a separate message + for (( index=0; index<$embedsCount; index++ )); do + + # Get current embed in embeds section + embed=$(jq ".embeds[$index]" <<< "${discordJson}") + + # Replace embeds in orignal message with current single embed and remove content section + discordJsonSingleEmbed=$(jq ".embeds=[$embed] | del(.content)" <<< "${discordJson}") + + # Send message with single embed and without the rest of the data + sendWebhook "${discordJsonSingleEmbed}" false + + done + ;; + + # Drop message and exit with error + *) + echo "Error: Limit check failed or content outside manageable limits. Unable to send webhook." >&2 + exit 1 + ;; + +esac \ No newline at end of file