diff --git a/.shi2.sh.swo b/.shi2.sh.swo new file mode 100644 index 0000000..2e6ed7e Binary files /dev/null and b/.shi2.sh.swo differ diff --git a/.shi2.sh.swp b/.shi2.sh.swp new file mode 100644 index 0000000..113fe30 Binary files /dev/null and b/.shi2.sh.swp differ diff --git a/README.md b/README.md index f9aadb5..6a9aaf8 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,131 @@ -#shi +# shi **SH**ell **I**mageboard -no php, no js, just POSIX shell, gnu coreutils, nc, html and css +the programming equivalent of a shitpost +(second edition) ### features +- its all POSIX sh + - seriously - posting - replies - images - timestamps - more -### to use it -- `git clone ` -- `cd ` -- `touch log.txt` -- `cp templates/mainindex.html ./index.html` -- point your web server to ./index.html -- to run shi - - `./shi.sh &` - - `tail -f log.txt` -- to make a board: - - `mkdir -p boards/` - - `cp templates/index.html.template boards//index.html` - - `cp templates/main.css.template boards//main.css` -- to stop shi - - `killall shi.sh` -- to post to shi - - copy the script at the top of the board into your terminal - - hit enter, follow the prompts +### how to set up +- clone this repo +- cd to this repo +- `. ./console.sh` to start the shi console + - `start_listener` to start lisenting for posts +- you might want to do `tail -f log.txt` in another terminal to see whats going on +### how backends work +- shi has a builtin framework for swappable backends both for post recieving and storage, +currently there is: + - tar backend + - like the old version of shi + - recives tar files through nc and generates posts + - bad + - dont use it + - http backend + - listens for an http POST request with form data + - allows user-friendly html post form + - cool + - good + - file db backend + - stores posts as a series of text files in folders + - unchanged from old shi + - ok +- **how backends work** + - **listening** + - a backend is a shell script with a `listen` function + - this function is expected to coninuosly output post data it recives like so: + ``` + New post + board: + parent: + user: + title: + content:<content> + image_name:<name of image> + image content:<base64 encoded image> + End new post + ``` + - as long as the backend has a listen function that outputs data to stdout like this, + shi doesn't care where it comes from. + - **db** + - a db backend is simply a shell script provinding the following funtions: + - `path_to_post <post id>` + - `create post <board> <parent> <user> <title> <content> <image_name> <image_content> <post_id> <post_type>` + - `get_post_data <post_data_path> <item> + - note that since these functions are both in the same file, `post_data_path` can be in any format you desire + - again, shi doesn't care how this is done, only that it follows this format + +### anatomy of a post +- user + - name of user who posted the post +- title + - title of post + - this will be ignored for replies +- content + - the text content of a post +- thumb_path + - optional, filesystem path to the thumbnail of the post's image +- image_path + - optional, filesystem path to the image +- html_path + - path to where the html of the post is stored +- post_id + - unique numerical id of the post +- post_type + - type of post, specifies what template to use + - currently there is 'oppost' and 'reply' supported +- replies + - newline separated list of post_id's for all the replies to the op post + - a trailing /. is added to prevent `find` from seeing `post_data_path` itself + +### defining new post templates +- edit your template of choice in the 'templates' folder, +- {{{ TRIPLE BRACES }}} will be replaced with their respective variables + +### style notes +- if it can be done in a single line, you can set a variable to it, +otherwise declare the variable as empty and then set it to your operation +- follow shellcheck + +# files n folders +- boards + - where all the html for the boards is stored +- console.sh + - souce this to allow you to control shi +- globals.sh + - global variables (really just settings) +- inbox + - where posts data is cached before being processed +- latest + - the id of the latest post + - very important!! +- LICENSE + - free software, free society +- log.txt + - log +- problems.txt + - current issues with shi +- README.md + - this file +- reset.sh + - debug script that nukes all boards and completly resets shi + - don't use this unless you've really screwed up +- shi.sh + - memories +- templates + - templates +- util.sh + - utility functions employed by console.sh -i know the file permissions are weird i'll fix them at some point + + +this whole rewrite started because of a feature request by desvox. Unfortunately, due to the complexity of responding to http requests with sh, this feature was not added. + +Sorry desvox :( diff --git a/console.sh b/console.sh new file mode 100755 index 0000000..5ad49d9 --- /dev/null +++ b/console.sh @@ -0,0 +1,43 @@ +#!/bin/sh +. ./globals.sh +. ./util.sh +. ./shi2.sh + +alias log="tail -f $log_file" + +echo "Your shell is now a shi console!" +oldps1="$PS1" +PS1="shi>" +listener_pid="" + + +start_shi() { + start_listener & + listener_pid="$!" + echo "Started shi" +} + +stop_shi() { + kill -- -"$listener_pid" +# kill -- -$(ps -o pgid= $listener_pid | grep -o [0-9]*) + echo "Stopped shi" +} + +restart(){ + stop_shi + . ./globals.sh + . ./util.sh + . ./shi2.sh + echo "Reloaded source files" + start_shi +} + +#tail -f "$log_file" & # uncomment this if you want constant log output to console + + +cleanup() { + PS1="$oldps1" + alias log="log" +} + +trap cleanup EXIT INT TERM diff --git a/file_db_backend.sh b/file_db_backend.sh new file mode 100755 index 0000000..27f6b84 --- /dev/null +++ b/file_db_backend.sh @@ -0,0 +1,157 @@ +#!/bin/sh +# the file/folder db backend +# since the backend provides both the add_post and fetch_post +# functions the data path can be in whatever format you like +# TODO: Post deletion, both on front and backend + +# NOTE: A post db path should always begin at $board_dir + +. ./globals.sh + + +path_to_post() { + post_id="$1" + find "$board_dir" -maxdepth 4 -name "$post_id" -not -path '*/\.*' +} + +create_post() { + # This function takes strings correspoding to post + # data and stores them in the boards folder in the + # shi folder/file format + + + local board="$1" + local parent="$2" + local user="$3" + local title="$4" + local content="$(echo "$5" | tr '\a' '\n')" # change the BEL characters back to newlines + local image_name="$6" + local image_content="$7" + local post_id="$8" + local post_type="$9" + + echo "Adding data for post with id $post_id" >> "$log_file" + + local post_directory="$board_dir/$board/$parent/$post_id" # where the post data will be stored + post_directory="$(realpath "$post_directory")" + + echo "[file_db_backend.sh] board: $board" >> "$log_file" + echo "[file_db_backend.sh] parent: $parent" >> "$log_file" + echo "[file_db_backend.sh] user: $user" >> "$log_file" + echo "[file_db_backend.sh] title: $title" >> "$log_file" + echo "[file_db_backend.sh] content: $content" >> "$log_file" + echo "[file_db_backend.sh] image_name: $image_name" >> "$log_file" + # uncomment this if you want base64 spam vvv +# echo "image_content: $image_content" >> "$log_file" +# echo "image_content: $image_content" | head -c 50 >> "$log_file" + echo "[file_db_backend.sh] post id: $post_id" >> "$log_file" + echo "[file_db_backend.sh] post type: $post_type" >> "$log_file" + + if [ -z "$user" ]; then echo "[file_db_backend.sh] ERROR: no user" >> "$log_file"; return 1; fi + if [ -z "$content" ]; then echo "[file_db_backend.sh] ERROR: no content"; return 1; fi + if ! (mkdir -p "$post_directory") + then + echo "[file_db_backend.sh] ERROR: could not create post dir" >> "$log_file" + return 1 + fi + + local image_path="$post_directory/$image_name" + + echo "$user" > "$post_directory/user" + echo "$title" > "$post_directory/title" + echo "$content" > "$post_directory/content" + echo "$post_type" > "$post_directory/type" + if [ -n "$image_name" ] && [ -n "$image_content" ] + then + echo "$image_content" | base64 -d > "$image_path" + echo "$image_name" > "$post_directory/image_name" + fi + + if command -v convert > /dev/null && [ -n "$image_name" ] + then + local thumb_path="$post_directory/thumb_$image_name.jpg" + echo "[file_db_backend.sh] 'convert' found, generating thumbnail" >> "$log_file" + # convert the image to a jpeg + if echo "$image_path" | grep '.gif' + then + convert "$image_path"'[0]' "$thumb_path" + else + convert "$image_path" "$thumb_path" + echo "[file_db_backend.sh] converted image to jpeg" >> "$log_file" + fi + # re-compress the jpeg to be under 100kb + convert "$thumb_path" -define jpeg:extent=100kb "$thumb_path" + echo "[file_db_backend.sh] re compressed image at $thumb_path" >> "$log_file" + fi + + echo "[file_db_backend.sh] Finished adding post $post_id on $(date)" >> "$log_file" + echo "[file_db_backend.sh] post dir: $post_directory" >> "$log_file" + + echo "$post_directory" + + return 0 +} + +get_post_data() { + # this function is given a post id and a data item to be + # retrieved. It returns the value of the requested item + + # TODO: file path caching + + local post_data_path="$1" + local item="$2" + local image_path="" + + echo "[file_db_backend.sh] Got request for $item from post at $post_data_path" >> "$log_file" + + if ! [ -d "$post_data_path" ]; then return 1; fi + + case "$item" in "parent") + basename $(realpath "$post_data_path/..") + ;; + "user") + cat "$post_data_path/user" + ;; + "title") + cat "$post_data_path/title" + ;; + "content") + cat "$post_data_path/content" + ;; + "thumb_path") # where the post's image is stored on the filesystem + echo "thumb_$(cat "$post_data_path/$image_name").jpg" + ;; + "image_path") # where the post's image is stored on the filesystem + image_path="$post_data_path/$(cat "$post_data_path/image_name")" + if [ -f "$image_path" ] + then + echo "$image_path" + else + echo "" + fi + ;; + "html_path") # where the post's html is on the filesystem + echo "$post_data_path/index.html" + ;; + "post_id") + basename "$post_data_path" + ;; + "post_type") + cat "$post_data_path/type" + ;; + "replies") + # Newline separated list of post_id for all replies to the + # specified post + # A trailing /. is added to prevent find from seeing + # post_data_path itself + + find "$post_data_path" \ + -mindepth 1 \ + -type d \ + -iname '[0-9]*' \ + -printf '%p\n' | sort + ;; + esac +} + +# no paths, only use the post id diff --git a/globals.sh b/globals.sh new file mode 100755 index 0000000..b4eabff --- /dev/null +++ b/globals.sh @@ -0,0 +1,8 @@ +port="7070" +log_file="./log.txt" +board_dir="./boards" +template_dir="./templates" +latest_file="./latest" +inbox="./inbox" +image_types="(\.png|\.jpg|\.gif)" +shi_sub_url="/shi" # the sub url shi is being run from (eg. http://domain.tld/[shi/]) diff --git a/http_backend.sh b/http_backend.sh new file mode 100755 index 0000000..bf29691 --- /dev/null +++ b/http_backend.sh @@ -0,0 +1,147 @@ +#!/bin/sh +. ./globals.sh + + +echo "Using http backend." >> "$log_file" + +gen_post_data_from_file() { +# TODO: if variables are unset by the end, set them to "error" + local file="$1" + local board="" + local parent="" + local user="" + local title="" + local content="" + local image_name="" + local image_content="" + + # find the form boundary + local boundary="$(grep -am1 \ + "Content-Type: multipart/form-data; boundary=" \ + $file | cut -f 2- -d "=")" + + local in_boundary="0" + local data_type="" + local file_upload_start="" # the line at which binary file data starts (if any) + local iterator="0" + echo "New post" + while read -r line + do + iterator="$(( iterator + 1 ))" + # dont read the binary data contained in files, + # as it will break the script + if echo "$line" | grep -q 'Content-Disposition: form-data; name="fileupload";' + then + file_upload_start="$iterator" + break + fi + + # if a boundary is seen + if echo "$line" | grep -q -- "$boundary" + then + # and we are not already in a boundary + if [ "$in_boundary" -eq 0 ] + then + # then we are now in a boundary + in_boundary="1" + else + # otherwise, we are in a new boundary, + # so reset data_type + data_type="" + fi + fi + + # if we are in a boundary, the beginning of form data is seen, + # and we are not already reading form data + if [ "$in_boundary" -eq "1" ] \ + && echo "$line" \ + | grep -q "Content-Disposition: form-data; name=" \ + && [ -z "$data_type" ] + then + + data_type=$(echo "$line" | cut -f 2 -d '"') + fi + + case "$data_type" in "post_to") + # remove LFs from line + line="$(echo "$line" | tr -d '\r')" + + # if our post is a reply + if echo "$line" | grep -qE "[0-9]$" + then + # set parent to the post it is replying to + parent="$(find $board_dir \ + -maxdepth 3 \ + -name "$line" \ + -not -path '*/\.*')" + # make sure the parent post exists + if [ -z "$parent" ]; then return 1; fi + # and set the board value to the board it is to be posted to + board="$(basename `dirname "$parent"`)" + # then change the parent value from adirectory path to a plain string + parent="$(basename `realpath "$parent"`)" + else # otherwise the post is a oppost + # set the board value appropriately + board="$line" + fi + ;; + "name") + user="$(echo "$line" | tr -d '\r')" + ;; + "title") + title="$(echo "$line" | tr -d '\r')" + ;; + "content") + # carrige returns need to be removed from multiline content + content="$content$(printf "\n%s\n" "$line" | tr -d '\r')" + ;; + "fileupload") + echo "read line of fileupload" + ;; + esac + done < "$file" + + # remove the first line from content, which is html form junk + content="$(echo "$content" | sed -e '1,2d' | tr '\n' '\a')" + image_name="$(sed -n "$file_upload_start"p $file \ + | grep -oE 'filename=".*' \ + | cut -f 2 -d '"' \ + | tr -d '"' )" + image_content="$(sed -e "1,$(( file_upload_start + 2 ))d" "$file" | base64 -w 0)" + + echo "board:$board" + echo "parent:$parent" + echo "user:$user" + echo "title:$title" + echo "content:$content" + echo "image_name:$image_name" + echo "image_content:$image_content" + echo "End new post" + rm "$file" +} + +get_request() { + submission_id="$(date '+%s%N')" # unique id of the submission + printf "HTTP/1.1 200 OK\r +Date: %s\r +Server: BSD Netcat/lmao\r +Connection: close\r +Content-Type: text/html\r\n\r\n +<script>window.close();</script>\n" "$(date -Ru | sed -e 's/\+0000/GMT/')" \ + | nc -lp "$port" -w 3 | head -c 1000000000 > "$inbox/$submission_id" + + mv "$inbox/$submission_id" "$inbox/$submission_id.submission" +} + +listen() { + while true + do + # TODO: scale to allow for posting in rapid succession + get_request + for post in $(find "$inbox" -iname "*.submission") + do + gen_post_data_from_file "$post" + done + done +} +trap break EXIT INT TERM diff --git a/main.css b/main.css new file mode 100755 index 0000000..3d0097a --- /dev/null +++ b/main.css @@ -0,0 +1,108 @@ +html * +{ + color: #fff !important; + font-family: monospace !important; +} + +div.postContainer { + border-style: solid; + padding-left: 5px; + margin: 0px; + margin-top: 5px; + width: 75%; + float: left; + border-width: 1px; + /*display: inline;*/ +} + +div.opContainer { + border-style: solid; + border-width: 1px; + padding: 0px 5px; + max-width: 1000px; + margin: 0px; + overflow: hidden; +} + +div.opContainer + div.opContainer { + margin-top: 10px; +} + +div.postHeader { + padding: 0px; +} +div.opContent { + /*display: inline;*/ +} + +div.postContent { + width: 100%; + height: 100%; +} + +p.opText { + /*font-family: sans;*/ + padding-bottom: 2px; + margin: 0px; + margin-top: 2px; + padding-bottom: 10px; + font-size: 12px; + height: 12px; + display: inline; +} + +p.postText { + /*font-family: sans;*/ + word-wrap: break-word; + padding-left: 5px; + padding-bottom: 2px; + margin: 0px; + margin-top: 2px; + font-size: 12px; +} +p.imageName { + margin: 0; + margin-top: -5px; + font-size: 10px; +} + +div.imageContainer { + height: 100%; + width: 20%; + padding-right: 5px; + float: left; + display: block; +} +img.postImage { + max-width: 100%; + height: auto; + width: auto; +} +div.reply { + border-style: double; +} +p.box { + border-style: double; +} +p.postTitle { + margin: 0px; + font-size: 14px; + display: inline-block; + padding-left: 9%; +} +a.postHeaderText { + margin: 1px; + font-size: 10px; + display: inline-block; +} + +p.replyHeaderText { + margin: 1px; + font-size: 10px; + display: inline-block; +} + +body { + padding: 20px; + background-color: black; +} diff --git a/post.sh b/post.sh new file mode 100755 index 0000000..96c9699 --- /dev/null +++ b/post.sh @@ -0,0 +1,3 @@ +#!/bin/sh +readdat () { read -r var; echo "$var" | grep -qE "[a-Z].*" || var=$2; echo "$var" > "$1"; };echo "Post to:"; read -r parent; echo "$parent" | grep -qE "([a-Z]|[1-9]).*" || exit;postdir=$(echo "$parent"-"$RANDOM");mkdir ./"$postdir" && cd ./"$postdir" || exit; echo "Name (blank for anonymous):";readdat "user" "Anonymous";echo "Title (blank for none):";readdat "title" "";echo "Absolute path to image (blank for none):";read -r imagepath; echo "$imagepath" | grep -qE "[a-Z].*" && if test -f +"$imagepath"; then cp "$imagepath" .;else echo "could not find $imagepath";fi;echo "Post text (Ctrl-D to finish):"; cat > content; cd ..; tar -cf "$postdir".tar ./"$postdir"; diff --git a/problems.txt b/problems.txt new file mode 100644 index 0000000..497daa8 --- /dev/null +++ b/problems.txt @@ -0,0 +1,11 @@ +incredibly slow to post images + +not entirely synchronus, a post is lost if it is received at the same time as another + +tar backend has no clean way to exit, leaves an orphan nc process behind + +css in post page +test rapid succession posting + - I think it works? +test replies in post html + diff --git a/reset.sh b/reset.sh index ee6fbed..e07d72a 100755 --- a/reset.sh +++ b/reset.sh @@ -1,6 +1,10 @@ #!/bin/sh +# DEBUG PURPOSES ONLY +# USERS SHOULD USE THE FUNCTION PROVIDED IN util.sh! + echo '0' > latest echo "" > log.txt +rm -rf ./inbox/* for board in boards/* do echo reseting board "$board" @@ -15,3 +19,4 @@ do done done +pkill -f nc diff --git a/shi/README.md b/shi/README.md new file mode 100644 index 0000000..f9aadb5 --- /dev/null +++ b/shi/README.md @@ -0,0 +1,33 @@ +#shi +**SH**ell **I**mageboard + +no php, no js, just POSIX shell, gnu coreutils, nc, html and css + +### features +- posting +- replies +- images +- timestamps +- more + +### to use it +- `git clone <this repo>` +- `cd <this repos folder>` +- `touch log.txt` +- `cp templates/mainindex.html ./index.html` +- point your web server to ./index.html +- to run shi + - `./shi.sh &` + - `tail -f log.txt` +- to make a board: + - `mkdir -p boards/<boardname>` + - `cp templates/index.html.template boards/<boardname>/index.html` + - `cp templates/main.css.template boards/<boardname>/main.css` +- to stop shi + - `killall shi.sh` +- to post to shi + - copy the script at the top of the board into your terminal + - hit enter, follow the prompts + + +i know the file permissions are weird i'll fix them at some point diff --git a/shi.sh b/shi/shi.sh similarity index 84% rename from shi.sh rename to shi/shi.sh index 018e084..0b3a42a 100755 --- a/shi.sh +++ b/shi/shi.sh @@ -27,7 +27,7 @@ IFS='' LOG=log.txt TEMPLATEDIR="templates" - +alias echo='echo [shi.sh]:' newPost() { POSTTYPE=$2 POSTNUM=$(basename "$1") @@ -150,45 +150,48 @@ addPostToBoard() { cat "$TEMPFILE" > "$BOARDDIR"/index.html } - +addNewPost() { + post="$1" + if basename "$post" | grep -qE "[0-9]$" && [ "$(basename "$post")" -gt "$2" ] + then + TEMPFILE=$(mktemp) + if dirname "$post" | grep -qE "[0-9]*+/[0-9]*$" # If reply generate a reply + then + echo "Post type: reply" > $LOG + PARENT=$(dirname "$post" | sed 's/^.*\/\([0-9]\+\)/\1/') + while read -r line + do + if echo "$line" | grep -q "<!--END REPLIES $PARENT-->" + then + newPost "$post" "reply" + printf "\t\t\t\t\t\t<!--END REPLIES %s-->\n" "$PARENT" + else + echo "$line" + fi + done < "$post/../index.html" > "$TEMPFILE" + cat "$TEMPFILE" > "$post/../index.html" + addPostToBoard "$(dirname "$post")" + else # Else generate an op post + echo "Post type: OP post" >> $LOG + while read -r line + do + echo "$line" + if echo "$line" | grep -q "<!--BEGIN POSTS-->" + then + newPost "$post" "oppost" + fi + done < "$(dirname "$post")/index.html.template" > "$TEMPFILE" + cat "$TEMPFILE" > "$post"/index.html + rm "$TEMPFILE" + addPostToBoard "$post" + fi + fi +} update() { echo "Posting started at $(date -u)" >> $LOG find "$1" | while read -r post do - if basename "$post" | grep -qE "[0-9]$" && [ "$(basename "$post")" -gt "$2" ] - then - TEMPFILE=$(mktemp) - if dirname "$post" | grep -qE "[0-9]*+/[0-9]*$" # If reply generate a reply - then - echo "Post type: reply" > $LOG - PARENT=$(dirname "$post" | sed 's/^.*\/\([0-9]\+\)/\1/') - while read -r line - do - if echo "$line" | grep -q "<!--END REPLIES $PARENT-->" - then - newPost "$post" "reply" - printf "\t\t\t\t\t\t<!--END REPLIES %s-->\n" "$PARENT" - else - echo "$line" - fi - done < "$post/../index.html" > "$TEMPFILE" - cat "$TEMPFILE" > "$post/../index.html" - addPostToBoard "$(dirname "$post")" - else # Else generate an op post - echo "Post type: OP post" >> $LOG - while read -r line - do - echo "$line" - if echo "$line" | grep -q "<!--BEGIN POSTS-->" - then - newPost "$post" "oppost" - fi - done < "$(dirname "$post")/index.html.template" > "$TEMPFILE" - cat "$TEMPFILE" > "$post"/index.html - rm "$TEMPFILE" - addPostToBoard "$post" - fi - fi + addNewPost "$post" done echo $(( $2 + 1 )) > ./latest echo "Posting finished at $(date -u)" >> $LOG diff --git a/shi2.sh b/shi2.sh new file mode 100755 index 0000000..c3a7d8a --- /dev/null +++ b/shi2.sh @@ -0,0 +1,364 @@ +#!/bin/sh +# TODO: gif support and thumbnails if Imagemagick is installed +# TODO: More comments +# TODO: discard reply if parent doesnt exist, right now shi hangs + +backend="./http_backend.sh" +db_backend="./file_db_backend.sh" + +. ./globals.sh +. ./util.sh +. "$backend" +. "$db_backend" + + +sanitize_text() { + # Sanitizes text, the first argument is the text to be + # sanitized, the rest represent what sanitations should be applied + # (html, sed, etc.) + + # sed sanitation will prepend a leading backslash to sed characters + # html sanitation will escape '&', '<', and '>' and replace newlines + # with '<br>' tags + + # NOTE: 'sed' must be placed before 'html" in the options if they + # are both specified (This should be fixed later) + + local text="$1" # text to be sanitized + + for option in "$@" # parse options to find specified sanitations + do + case "$option" in "$text") + ;; # if it is the first option, ignore it + "sed") + echo "sanitizing text $text for $option" >> $log_file + text="$(echo "$text" \ + | sed -e 's/&/&/g' \ + -e 's/</\</g' \ + -e 's/>/\>/g' \ + -e 's/\([-$^*()+{\[\.?\/]\)/\\\1/g')" + ;; + "html") + echo "sanitizing text $text for $option" >> $log_file + text="$(echo "$text" \ + | sed -e ':a;N;$!ba;s/\n/<br>/g')" + ;; + esac + done + + echo "$text" +} + +get_op_post_html() { + # Get the html of an op post if it has already been generated + local temp_file="$(mktemp)" + local post_db_path="$1" + local post_html_path="$(get_post_data "$post_db_path" "html_path")" + local post_id="$(get_post_data "$post_db_path" "post_id")" + local trim="$2" # how many replies to get + local post_replies="$(get_post_data "$post_db_path" "replies")" + + # id of the most recent reply to be included on the board page + local reply_lim="$(echo "$post_replies" | sed -n "$trim"p | tail -c 1)" + + echo "REPLIES $post_replies" >> "$log_file" + cat "$post_html_path" \ + | sed -e ':a' -e 'N' -e '$!ba' \ + -e "s/.*\(<!--BEGIN OP POST $post_id-->.*<!--END OP POST $post_id-->\).*/\1/g" \ + > "$temp_file" + + sed -i -e ':a' -e 'N' -e '$!ba' \ + -e "s/<!--BEGIN REPLY $(( post_id - trim ))-->.*<!--END OP POST $post_id-->//g" \ + "$temp_file" + + cat "$temp_file" + +# printf "<!--END REPLIES %s-->\n</div>\n</div>\n<!--END OP POST %s-->" \ +# "$post_id" "$post_id" + + rm "$temp_file" +} + +gen_post_html() { + # Takes a path to a database entry and generates html + # based on it's content + + # TODO: implement thumbnails + # TODO: separate html frontend to enable creation of other frontends + # eval + + local post_db_path="$1" # db path to post + local template_path="" # filesystem path to the post template to be used + + echo "[Genereate post html]" >> "$log_file" + + echo "Generating post html for $post_db_path" >> "$log_file" + local date="$(date -u)" + local user="$(get_post_data "$post_db_path" "user")" + local title="$(get_post_data "$post_db_path" "title")" + local content="$(get_post_data "$post_db_path" "content")" + local image_path="$(get_post_data "$post_db_path" "image_path")" + local image_size="" + local image_name="" + local post_id="$(get_post_data "$post_db_path" "post_id")" + local post_type="$(get_post_data "$post_db_path" "post_type")" + + + # http path to post directory (generated from html path) + local post_url_path="$(get_post_data "$post_db_path" "html_path")" + post_url_path="$(dirname "$post_url_path")" + + # http path to image + local image_url_path="" + + # http path to thumbail + local thumb_url_path="" + + # truncate the url path to start at $board_dir + case "$post_type" in "oppost") + post_url_path="$(echo "$post_url_path" \ + | rev \ + | cut -f -4 -d '/' \ + | rev)" + ;; + "reply") + post_url_path="$(echo "$post_url_path" \ + | rev \ + | cut -f -5 -d '/' \ + | rev)" + ;; + esac + + post_url_path="$shi_sub_url/$post_url_path" + + if [ -z "$image_path" ] # if no image is found + then + post_type="$post_type"-noimage # specify the post type as imageless + echo "Image path is null, -noimage post" >> "$log_file" + else + image_size="$(du -h "$image_path" | cut -f 1)" + image_name="$(basename "$image_path")" + image_url_path="$post_url_path/$image_name" + thumb_url_path="$post_url_path/thumb_$image_name.jpg" + fi + + template_path="$template_dir/$post_type".template + + content="$(sanitize_text "$content" sed html)" # sanitize content + title="$(sanitize_text "$title" sed)" # sanitize title for sed + post_url_path="$(sanitize_text "$post_url_path" sed)" # sanitize post url for sed + image_url_path="$(sanitize_text "$image_url_path" sed)" # sanitize image path for sed + thumb_url_path="$(sanitize_text "$thumb_url_path" sed)" # sanitize image path for sed + + # read through the appropriate template and replace + # placeholders with their actual values + while read -r line + do + echo "$line" \ + | sed -e "s/{{{ POSTNUM }}}/$post_id/g" \ + -e "s/{{{ POSTURL }}}/$post_url_path/g" \ + -e "s/{{{ DATE }}}/$date/g" \ + -e "s/{{{ USER }}}/$user/g" \ + -e "s/{{{ POSTTITLE }}}/$title/g" \ + -e "s/{{{ POSTTEXT }}}/$content/g" \ + -e "s/{{{ IMAGEPATH }}}/$image_url_path/g" \ + -e "s/{{{ THUMBPATH }}}/$thumb_url_path/g" \ + -e "s/{{{ IMAGENAME }}}/$image_name/g" \ + -e "s/{{{ IMAGESIZE }}}/$image_size/g" + done < "$template_path" +} + +insert_post() { + # TODO: cache (?) the position of <!--BEGIN POSTS--> in the html + # currently a large and exponential slowdown + + # read from a cache file, if it doesn't exist, make it + + local post_db_path="$1" # path to the post data + local target_html="$2" # the html to be modified + local trim="3" # how many replies to display on the main board page + + echo "[Insert post into html]" >> "$log_file" + echo "Inserting post $post_db_path into $target_html" >> "$log_file" + + local post_id="$(get_post_data "$post_db_path" "post_id")" + local post_type="$(get_post_data "$post_db_path" "post_type")" + local post_replies="" + local parent="" + local temp_file="$(mktemp)" + + local replace="" # regex to find the exitsing post's html + local insert_at="" # html will be inserted after the line containing this string + + case "$post_type" in "oppost") + replace="<!--BEGIN OP POST $post_id-->.*<!--END OP POST $post_id-->" + insert_at="<!--BEGIN POSTS-->" + ;; + "reply") + # In the future, we will want to display replies in reverse chronological order + parent="$(get_post_data "$post_db_path" "parent")" + replace="<!--BEGIN REPLY $post_id-->.*<!--END REPLY $post_id-->\n" + insert_at="<!--BEGIN REPLIES $parent-->" + ;; + esac + + # remove any existing instances of the post in the html + sed -i \ + -e ':a' \ + -e 'N' \ + -e '$!ba' \ + -e "s/$replace//g" "$target_html" + + + while read -r line + do + echo "$line" + if echo "$line" | grep -qE "$insert_at" + then + echo "Inserting html at line $line matching $insert_at" >> "$log_file" + if [ "$post_type" = "oppost" ] + then + post_replies="$(get_post_data "$post_db_path" "replies")" + if [ -n "$post_replies" ] + then + get_op_post_html "$post_db_path" "$trim" + else + gen_post_html "$post_db_path" + fi + else + gen_post_html "$post_db_path" + fi + fi + + done < "$target_html" > "$temp_file" + cat "$temp_file" > "$target_html" + + +# NOTE: This section only applies when regenerating an entire board +# If the post is an oppost, we will need to also generate the replies +# if [ "$post_type" = "oppost" ] +# then +# # NOTE: Very redundant, find a way to process both files at once +# post_replies=$(get_post_data "$post_db_path" "replies" | tac) +# for reply_db_path in $post_replies +# do +# echo "Inserting "$reply_db_path" into post html" >> "$log_file" +# insert_post "$reply_db_path" "$(get_post_data "$post_db_path" "html_path")" +# done +# +# # NOTE: This will break if the board directory has spaces +# for reply_db_path in $(echo $post_replies | cut -f "$trim"- -d ' ') +# do +# echo "Inserting "$reply_db_path" into board html" >> "$log_file" +# insert_post "$reply_db_path" "$target_html" +# done +# fi +} + +start_listener() { + # This function reads a continuous stream of data + # (provided by the `listen` function from a user + # specified backend) and generates posts from the + # data it receives. + + local status=0 # 0 is normal, 1 is receiving post data + + local board="" + local parent="" + local user="" + local title="" + local content="" + local image_name="" + local image_content="" + local post_id="" + local post_type="" + local post_html_path="" + + local latest="" + + listen | while read -r line + do + + if [ "$line" = "End new post" ] + then + echo [New post] >> "$log_file" + status=0 + latest="$(cat "$latest_file")" + post_id="$(( latest + 1 ))" + post_db_path="$(create_post \ + "$board" \ + "$parent" \ + "$user" \ + "$title" \ + "$content" \ + "$image_name" \ + "$image_content" \ + "$post_id" \ + "$post_type")" + + post_html_path="$(get_post_data "$post_db_path" "html_path")" + parent_html_path="" + parent_db_path="" + + + if [ "$post_type" == "oppost" ] + then + cp "$board_dir/$board/index.html.template" "$post_html_path" + insert_post "$post_db_path" "$post_html_path" + insert_post "$post_db_path" "$board_dir/$board/index.html" + else + parent_html_path="$board_dir/$board/$parent/index.html" + parent_db_path="$board_dir/$board/$parent" + insert_post "$post_db_path" "$parent_html_path" + insert_post "$parent_db_path" "$board_dir/$board/index.html" + fi + + + echo "$post_id" > "$latest_file" # update the latest post file + echo "Finished adding post $post_id at $(date)" + fi + + if [ "$status" -eq 1 ] + then + key="$(echo "$line" | cut -f 1 -d :)" value="$(echo "$line" | cut -f 2- -d :)" + + echo "Reading key $key from backend" >> "$log_file" + case "$key" in "board") + board="$value" + ;; + "parent") + parent="$value" + if [ -z "$parent" ] + then + post_type="oppost" + else + post_type="reply" + fi + ;; + "user") + user="$value" + ;; + "title") + title="$value" + ;; + "content") + content="$value" + ;; + "image_name") + image_name="$value" + ;; + "image_content") + image_content="$value" + ;; + esac + fi + + if [ "$line" = "New post" ] + then + echo "Processing new post" >> "$log_file" + status=1 + fi + done +} + +trap break EXIT INT TERM diff --git a/tar_backend.sh b/tar_backend.sh new file mode 100755 index 0000000..85710f1 --- /dev/null +++ b/tar_backend.sh @@ -0,0 +1,99 @@ +#!/bin/sh +# Tar listening backend. +# This backend looks for tarfiles in the folder specified by $inbox. +# It then un-tars them and and gets the username, title and post content +# from their respecively named files. It will use the first image matching +# one of $image_types types as the image for the post. + +# TODO: Trap the exit signal to kill nc +# or maybe use telnet + +# TODO: make this file backend agnostic + +. ./globals.sh + +echo "Using tar backend." >> "$log_file" +gen_post_from_dir() { + local dir="$1" + local post_to="$(basename "$dir" | sed 's/-.*//g')" # where the post wants to go + + local board="" + local parent="" + local user="" + local title="" + local content="" + local image_name="" + local image_content="" + + local image_path="" + + + echo "New post" + if echo "$post_to" | grep -qE "[0-9]$" # if our post is a reply + then + # set the parent to the post it is replying to + parent="$(find $board_dir -maxdepth 3 -name "$post_to" -not -path '*/\.*')" + # make sure the parent post exists + if [ -z "$parent" ]; then echo "Parent post does not exist" && return 1; fi + + # and set the board value to the board it is to be posted to + board="$(basename `dirname "$parent"`)" + # then change the parent value from a directory path to a plain string + parent="$(basename `realpath "$parent"`)" + else # otherwise, the post is an OP post. + # set the board value to the board it is to be posted to + board="$post_to" + fi + + image_path="$(find "$dir" | grep -Em1 "$image_types")" + if ! [ -z "$image_path" ] + then + image_name="$(basename "$image_path")" + image_content="$(cat "$image_path" | base64 -w 0)" + fi + + user="$(cat "$dir/user")" + content="$(tr '\n' '\a' < "$dir/content")" # change newline characters to BEL + title="$(cat "$dir/title")" + + echo "board:$board" + echo "parent:$parent" + echo "user:$user" + echo "title:$title" + echo "content:$content" + echo "image_name:$image_name" + echo "image_content:$image_content" + echo "End new post" + + return 0 +} + +listen() { + # NOTE: Tar extraction takes a nontrivial amount of time, which + # means if two connections were made in close succecion + # there is a possibility that the second connection would + # not receive the "busy" signal, and send it's data anyways + # + # additionally, there should be a way to prevent the sending of + # malicious data to interfere with normal post processing + # + # to prevent zip bombs, break the tar -x -C operation up and use + # `zcat $file | head -c $file_limit` + + blocker_pid="" # the pid of the loop that sends the busy signal + while nc -lp "$port" | tar -x -C "$inbox" + do + (while true + do + echo "Busy" | nc -lp 7070 + done)>/dev/null & blocker_pid="$!" + + for dir in $inbox/* + do + echo "Got post" >> "$log_file" + gen_post_from_dir "$dir" + rm -rf "$dir" + done + kill "$blocker_pid" >/dev/null + done +} diff --git a/templates/index.html.template b/templates/index.html.template index 31637b6..64d6c34 100755 --- a/templates/index.html.template +++ b/templates/index.html.template @@ -15,9 +15,15 @@ a description for the board </p> <br> - <p style="text-align:left;font-size: 9px; margin-right: 25%; margin-left: 25%"> - readdat () { read -r var; echo "$var" | grep -qE "[a-Z].*" || var=$2; echo "$var" > "$1"; };echo "Post to:"; read -r parent; echo "$parent" | grep -qE "([a-Z]|[1-9]).*" || exit;postdir=$(echo "$parent"-"$RANDOM");mkdir ./"$postdir" && cd ./"$postdir" || exit; echo "Name (blank for anonymous):";readdat "user" "Anonymous";echo "Title (blank for none):";readdat "title" "";echo "Absolute path to image (blank for none):";read -r imagepath; echo "$imagepath" | grep -qE "[a-Z].*" && if test -f "$imagepath"; then cp "$imagepath" .;else echo "could not find $imagepath";fi;echo "Post text (Ctrl-D to finish):"; cat > content; cd ..; tar -cf "$postdir".tar ./"$postdir";rm -rf "$postdir"; echo "posting..."; nc -q2 [the adress shi is at] [the port shi is running on] < "$postdir".tar - </p> + <form> + Name: <br> + <input type="text" name="username"> <br> + Post title: <br> + <input type="text" name="title"> <br> + Content: <br> + <input type="text" name="content"> <br> + <input type="submit" value="Submit"> <br> + </form> <br> <div class="posts"> <!--BEGIN POSTS--> diff --git a/templates/main.css.template b/templates/main.css.template index 3d0097a..87ac843 100755 --- a/templates/main.css.template +++ b/templates/main.css.template @@ -106,3 +106,9 @@ body { padding: 20px; background-color: black; } +input { + background-color: black; +} +textarea { + background-color: black; +} diff --git a/templates/oppost-noimage.template b/templates/oppost-noimage.template index 4645fb5..df379a3 100755 --- a/templates/oppost-noimage.template +++ b/templates/oppost-noimage.template @@ -1,7 +1,7 @@ <!--BEGIN OP POST {{{ POSTNUM }}}--> <div class="opContainer"> <div class="postHeader"> - <a class="postHeaderText" href="{{{ POSTNUM }}}/index.html">Post #{{{ POSTNUM }}} by {{{ USER }}} {{{ DATE }}}</a> + <a class="postHeaderText" href="{{{ POSTURL }}}">Post #{{{ POSTNUM }}} by {{{ USER }}} {{{ DATE }}}</a> <p class="postTitle">{{{ POSTTITLE }}}</p> </div> <div class="opContent"> diff --git a/templates/oppost.template b/templates/oppost.template index a7b29cc..72468f1 100755 --- a/templates/oppost.template +++ b/templates/oppost.template @@ -1,13 +1,13 @@ <!--BEGIN OP POST {{{ POSTNUM }}}--> <div class="opContainer"> <div class="postHeader"> - <a class="postHeaderText" href="{{{ POSTNUM }}}/index.html">Post #{{{ POSTNUM }}} by {{{ USER }}} {{{ DATE }}}</a> + <a class="postHeaderText" href="{{{ POSTURL }}}">Post #{{{ POSTNUM }}} by {{{ USER }}} {{{ DATE }}}</a> <p class="postTitle" >{{{ POSTTITLE }}}</p> </div> <div class="opContent"> <div class="imageContainer"> - <a href="/shi/{{{ IMAGEPATH }}}" target="_blank"> - <img class="postImage" src="/shi/{{{ THUMBPATH }}}" alt="{{{ IMAGENAME }}}"> + <a href="{{{ IMAGEPATH }}}" target="_blank"> + <img class="postImage" src="{{{ THUMBPATH }}}" alt="{{{ IMAGENAME }}}"> </a> <p class="imageName"> {{{ IMAGENAME }}} ({{{ IMAGESIZE }}})</p> </div> diff --git a/templates/reply.template b/templates/reply.template index 7783fcd..2cdccdb 100755 --- a/templates/reply.template +++ b/templates/reply.template @@ -5,8 +5,8 @@ </div> <div class="postContent"> <div class="imageContainer"> - <a href="/shi/{{{ THUMBPATH }}}" target="_blank"> - <img class="postImage" src="/shi/{{{ IMAGEPATH }}}" alt="{{{ IMAGENAME }}}"> + <a href="{{{ THUMBPATH }}}" target="_blank"> + <img class="postImage" src="{{{ IMAGEPATH }}}" alt="{{{ IMAGENAME }}}"> </a> <p class="imageName"> {{{ IMAGENAME }}} ({{{ IMAGESIZE }}})</p> </div> diff --git a/util.sh b/util.sh new file mode 100755 index 0000000..14ef9d6 --- /dev/null +++ b/util.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# These are the functions that are run by the admin to manage posts + +file_path_to_post() { + post_id="$1" + find "$board_dir" -maxdepth 4 -name "$post_id" -not -path '*/\.*' +} + +delete_post() { + post_id="$1" + rm -rf "$(path_to_post "$post_id")" +} + +regen_board() { + . ./shi2.sh + board="$1" + posts="$(find "$board" \ + -mindepth 1 \ + -maxdepth 1 \ + -type d \ + -iname '[0-9]*' \ + -printf '%p\n' | sort)" + for post in $posts + do + echo "Generating post $post" + insert_post "$post" "$board/index.html" + done + +}