saltyim is the Go library and reference client and broker implementation for Salty IM it contains a command-line client (cli), a terminal user interface (tui), builtin server/broker and a Mobile / Desktop App PWA (progressive web app) https://salty.im/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

392 lines
8.3 KiB

#!/bin/sh
set -e
# Validate environment
if ! command -v jq > /dev/null; then
printf "missing jq command. Please install it via your system's package manager\n"
exit 1
fi
if ! command -v msgbus > /dev/null; then
printf "missing msgbus command. Use: go install git.mills.io/prologic/msgbus/cmd/msgbus@latest\n"
exit 1
fi
for cmd in salty salty-keygen; do
if ! command -v $cmd > /dev/null; then
printf "missing %s command. Use go install go.mills.io/salty/cmd/$cmd@latest\n" "$cmd"
exit 1
fi
done
if [ -n "$XDG_CONFIG_HOME" ]; then
data_path="$XDG_CONFIG_HOME/salty"
else
data_path="$HOME/.config/salty"
fi
if [ -z "$SALTY_IDENTITY" ]; then
export SALTY_IDENTITY="$data_path/$USER.key"
fi
clear_line() {
printf '\r\033[2K'
}
move_cursor_up() {
printf '\033[1A'
}
get_user () {
user=$(grep user: "$SALTY_IDENTITY" | awk '{print $3}')
if [ -z "$user" ]; then
user="$USER"
fi
echo "$user"
}
format_message() {
me="$1"
message="$2"
printf "%s\t(%s)\t%s" "$(date -u +%FT%TZ)" "$me" "$message"
}
stream () {
if [ -z "$SALTY_IDENTITY" ]; then
echo "SALTY_IDENTITY not set"
return 2
fi
sig=$(mktemp /tmp/salty.XXXXXX)
msg="$(jq -r '.payload' | base64 -d | salty -i "$SALTY_IDENTITY" -d 2> "$sig")"
if [ -n "$SALTY_CHATWITH" ]; then
if ! echo "$msg" | grep -q "\t($SALTY_CHATWITH)" > /dev/null; then
exit
fi
fi
echo '\007'
if [ -n "$SALTY_CHATKEY" ]; then
if ! grep -q "$SALTY_CHATKEY" "$sig" > /dev/null; then
echo "[warning key does not match!] $(cat "$sig") != $SALTY_CHATKEY"
fi
fi
rm "$sig"
move_cursor_up
clear_line
echo "$msg" # | lextwt-cli -salty | jq -r '[.created,.twter.nick,.text]|@csv'
printf ">"
}
check_cors() {
return 0
if [ $# -lt 1 ]; then
printf "check_cors takes 1 arugment %d given\n" "$#"
printf "Try %s check_cors uri\n" "$(basename "$0")"
return 1
fi
uri="$1"
if [ "$(curl -v -o - -X GET "$uri" 2>&1 | grep -c -i -E 'access-control-allow-(headers|origin)')" -lt 2 ]; then
return 1
fi
if [ "$(curl -v -o - -X OPTIONS "$uri" 2>&1 | grep -c -i -E 'access-control-allow-(headers|origin)')" -lt 2 ]; then
return 1
fi
return 0
}
lookup () {
if [ $# -lt 1 ]; then
printf "lookup takes 1 arugment %d given\n" "$#"
printf "Try %s lookup nick@domain\n" "$(basename "$0")"
return 1
fi
user="$1"
nick="$(echo "$user" | awk -F@ '{ print $1 }')"
domain="$(echo "$user" | awk -F@ '{ print $2 }')"
hash="$(printf "%s" "$user" | sha256sum | cut -f 1 -d ' ')"
discovery_host="$(dig +short SRV _salty._tcp."$domain" | cut -f 4 -d' ' | sed 's/\.$//')"
if [ -z "$discovery_host" ]; then
discovery_host="$domain"
else
discovery_host="$(echo "$discovery_host" | sed 's/\.$//')"
fi
info=$(mktemp /tmp/salty.XXXXXX)
if ! curl -qfsSL "https://$discovery_host/.well-known/salty/${hash}.json" > "$info" 2> /dev/null; then
if ! curl -qfsSL "https://$discovery_host/.well-known/salty/${nick}.json" > "$info"; then
rm "$info"
echo "error: lookup failed"
return 1
else
if ! check_cors "https://$discovery_host/.well-known/salty/${nick}.json"; then
echo "error: lookup will fail for mobile users due to lack of CORS headers"
return 1
fi
fi
else
if ! check_cors "https://$discovery_host/.well-known/salty/${hash}.json"; then
echo "error: lookup will fail for mobile users due to lack of CORS headers"
return 1
fi
fi
cat "$info"
rm "$info"
}
readmsgs () {
topic="$1"
salty_json="$(mktemp /tmp/salty.XXXXXX)"
lookup "$user" > "$salty_json"
endpoint="$(jq -r '.endpoint' < "$salty_json")"
key="$(jq -r '.key' < "$salty_json")"
rm "$salty_json"
MSGBUS_URI=$(dirname "$endpoint")
export MSGBUS_URI
topic=$(basename "$endpoint")
msgbus sub "$topic" "$0"
}
sendmsg () {
if [ $# -lt 2 ]; then
printf "sendmsg requires 2 arguments %d provided\n" "$#"
printf "Try %s send nick@domain message\n" "$(basename "$0")"
return 1
fi
if [ -z "$SALTY_IDENTITY" ]; then
echo "SALTY_IDENTITY not set"
return 2
fi
user="$1"
message="$2"
if [ -z "$message" ]; then
echo "error: empty message"
exit 2
fi
salty_json="$(mktemp /tmp/salty.XXXXXX)"
lookup "$user" > "$salty_json"
endpoint="$(jq -r '.endpoint' < "$salty_json")"
key="$(jq -r '.key' < "$salty_json")"
rm "$salty_json"
me="$(get_user)"
format_message "$me" "$message" \
| salty -i "$SALTY_IDENTITY" -r "$key" \
| curl -fqsS --data-binary @- "$endpoint"
}
chatwith() {
if [ $# -lt 1 ]; then
printf "chatwith requires 1 arguments %d provided\n" "$#"
printf "Try %s chat nick@domain\n" "$(basename "$0")"
return 1
fi
if [ -z "$SALTY_IDENTITY" ]; then
echo "SALTY_IDENTITY not set"
return 2
fi
user="$1"
salty_json="$(mktemp /tmp/salty.XXXXXX)"
lookup "$user" > "$salty_json"
endpoint="$(jq -r '.endpoint' < "$salty_json")"
key="$(jq -r '.key' < "$salty_json")"
rm "$salty_json"
me="$(get_user)"
nick="$(echo "$me" | awk -F@ '{ print $1 }')"
domain="$(echo "$me" | awk -F@ '{ print $2 }')"
export SALTY_CHATWITH="$user"
export SALTY_CHATKEY="$key"
echo chat with "$SALTY_CHATWITH" via "$endpoint" key "$key"
readmsgs &
READ_PID=$!
trap 'kill $READ_PID' EXIT
while true; do
printf "> "
read -r message
clear_line
if [ -z "$message" ]; then
continue
fi
format_message "$me" "$message" \
| salty -i "$SALTY_IDENTITY" -r "$key" \
| curl -fqsS --data-binary @- "$endpoint"
move_cursor_up
clear_line
format_message "$me" "$message"
echo
done
}
make_user () {
mkdir -p "$data_path"
if [ $# -lt 1 ]; then
user=$USER
else
user=$1
fi
identity_file="$data_path/$user.key"
if [ -f "$identity_file" ]; then
printf "user key already exists!"
return 1
fi
# Check for msgbus env.. probably can make it fallback to looking for a config file?
if [ -z "$MSGBUS_URI" ]; then
printf "missing MSGBUS_URI in environment"
return 1
fi
salty-keygen -o "$identity_file"
echo "# user: $user" >> "$identity_file"
pubkey=$(grep key: "$identity_file" | awk '{print $4}')
hash="$(printf "%s" "$user" | sha256sum | cut -f 1 -d ' ')"
cat <<- EOF
Create this file in your webserver well-known folder. https://hostname.tld/.well-known/salty/$hash.json
{
"endpoint": "$MSGBUS_URI/$user",
"key": "$pubkey"
}
EOF
}
register () {
mkdir -p "$data_path"
if [ $# -lt 1 ]; then
printf "usage: %s register nick@domain\n" "$0"
exit 1
else
user=$1
fi
nick="$(echo "$user" | awk -F@ '{ print $1 }')"
domain="$(echo "$user" | awk -F@ '{ print $2 }')"
discovery_host="$(dig +short SRV _salty._tcp."$domain" | cut -f 4 -d' ' | sed 's/\.$//')"
if [ -z "$discovery_host" ]; then
echo "register is only supported with a saltyd service"
exit 1
fi
identity_file="$data_path/$nick.key"
if [ -f "$identity_file" ]; then
echo "user key already exists!"
return 1
fi
salty-keygen -o "$identity_file"
echo "# user: $user" >> "$identity_file"
pubkey=$(grep key: "$identity_file" | awk '{print $4}')
export SALTY_IDENTITY="$identity_file"
msgbus -u "https://$discovery_host/inbox" sub "$pubkey" "$0" &
pid="$!"
sendmsg "salty@$domain" REGISTER
sleep 1
kill "$pid"
}
show_help() {
printf "Usage: %s [options] <command> [arguments]\n" "$(basename "$0")"
printf "\n"
printf "Options:\n"
printf " -h/--help Show this help"
printf "\n"
printf "\n"
printf "Commands:\n"
printf " help -- Display this help message\n"
printf " chat -- Chat with a user by nick@domain\n"
printf " lookup -- Lookup a user by nick@domain\n"
printf " check-cors -- Perform a CORS check on a uri\n"
printf " make-user -- Generate a new user key pair\n"
printf " read -- Reads your messages\n"
printf " send -- Sends a message to nick@domain\n"
printf " register -- Sends a register message to a broker bot\n"
}
# check if streaming
if [ ! -t 1 ]; then
stream
exit 0
fi
# Show Help
if [ $# -lt 1 ]; then
show_help
exit 1
fi
CMD=$1
shift
case $CMD in
-h|--help|help)
show_help
;;
chat)
chatwith "$@"
;;
send)
sendmsg "$@"
;;
read)
readmsgs "$@"
;;
lookup)
lookup "$@"
;;
check-cors)
check_cors "@"
;;
make-user)
make_user "$@"
;;
register)
register "$@"
;;
esac