├── .gitignore ├── README.md ├── assets ├── themes │ ├── analog.dialogrc │ ├── classic.dialogrc │ ├── dark_slackware.dialogrc │ ├── evangelion.dialogrc │ ├── monochrome.dialogrc │ └── nighttime.dialogrc └── welcome_banner ├── boards └── .gitkeep ├── dockerfile ├── entrypoint.sh ├── go.mod ├── go.sum ├── server.go └── src ├── controllers.sh ├── logger.sh ├── models.sh └── visuals.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *logs 2 | *feed 3 | boards/* 4 | archive/* 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SSH Forum 2 | A ssh forum, nothing more nothing less 3 | 4 | ![analog](https://github.com/user-attachments/assets/5bae266e-77a3-44e4-bdad-c75d753cd7cc) 5 | 6 | ## Features 7 | - Anonymous text Forum 8 | - Chat 9 | - Theme personalitzation 10 | 11 | ## Adding/Removing boards 12 | Easy as adding/removing entries from a bash array. 13 | Open the `entrypoint.sh` (all configuration variables are there) and edit the array `BOARDS`: 14 | ```bash 15 | # ... 16 | 17 | export BOARDS=(\ 18 | # "Board Name" "Description"\ 19 | "Board 1" "Dedicated to the cult of the number one"\ 20 | "Board 2" "Dedicated to the cult of the number two"\ 21 | ) 22 | 23 | # ... 24 | ``` 25 | For each board you will want to add the board name and a description. 26 | 27 | ## Modifying the interface 28 | Basic layout configuration can be found on `entrypoint.sh`. There a bunch of constants defined there that control the size of different elements in the user interface. 29 | 30 | Another way of drastically changing the interface without rewritting the functionality would be providing a substitute for the `./src/visuals.sh` file. Keep the functions and their arguments and returns but change the display commands for the ones of your liking. 31 | 32 | ## Theme personalitzation 33 | Users can pick their prefered theme by putting the theme name as the username when connecting. 34 | For example if a user connects using `ssh classic@45.79.250.220` the user interface will be rendered with the theme at `./assets/themes/classic.dialogrc`. 35 | 36 | To change the default theme (in case the user doesn't provide a valid theme as username) you can just update the value of the `DIALOGRC` variable in `entrypoint.sh` to the one of your choosing. 37 | 38 | ## Installation 39 | ### Docker 40 | You will need to have installed `git` and `docker` 41 | ```bash 42 | git clone git@github.com:jfs-jfs/ssh-forum.git 43 | cd ssh-forum 44 | docker build -t ssh-forum . && docker run -d -p 2222:2222 ssh-forum 45 | ``` 46 | 47 | ### Doker with persistance 48 | This way you can keep the contents of the threads even if you kill the container 49 | 50 | First you will need to add a `.dockerignore` with this contents: 51 | ```text 52 | boards 53 | archive 54 | ``` 55 | 56 | And then run from inside the project folder: 57 | ```bash 58 | docker build -t ssh-forum . && docker run -d -p 2222:2222\ 59 | -v "$(pwd)/boards:/usr/src/app/boards:rw"\ 60 | -v "$(pwd)/archive:/usr/src/app/archive:rw"\ 61 | ssh-forum 62 | ``` 63 | 64 | ### Manual 65 | You will need to have installed `dialog`, `go` and `git` 66 | ```bash 67 | git clone git@github.com:jfs-jfs/ssh-forum.git 68 | cd ssh-forum 69 | go mod tidy && go build -v -o ssh-server server.go 70 | ./ssh-server # This will start the server at port 2222 71 | ``` 72 | -------------------------------------------------------------------------------- /assets/themes/analog.dialogrc: -------------------------------------------------------------------------------- 1 | # $Id: slackware.rc,v 1.2 2001/12/02 21:19:05 Patrick.J.Volkerding Exp $ 2 | # Run-time configuration file for dialog, matches Slackware color scheme. 3 | # 4 | # Types of values: 5 | # 6 | # Number - 7 | # String - "string" 8 | # Boolean - 9 | # Attribute - (foreground,background,highlight?) 10 | # edited by alislack 11 | 12 | 13 | # Set aspect-ration. 14 | aspect = 0 15 | 16 | # Set separator (for multiple widgets output). 17 | separate_widget = "" 18 | 19 | # Set tab-length (for textbox tab-conversion). 20 | tab_len = 2 21 | 22 | # Make tab-traversal for checklist, etc., include the list. 23 | visit_items = ON 24 | # visit_items = OFF 25 | 26 | # Shadow dialog boxes? This also turns on color. 27 | use_shadow = OFF 28 | 29 | # Turn color support ON or OFF 30 | use_colors = ON 31 | 32 | # Screen color 33 | screen_color = (BLUE,BLACK,OFF) 34 | 35 | # Shadow color 36 | shadow_color = (WHITE,BLACK,OFF) 37 | 38 | # Dialog box color 39 | dialog_color = (WHITE,BLACK,OFF) 40 | 41 | # Dialog box title color 42 | title_color = (MAGENTA,BLACK,ON) 43 | 44 | # Dialog box border color 45 | border_color = (BLUE,BLACK,OFF) 46 | 47 | # Active button color 48 | button_active_color = (MAGENTA,BLACK,ON) 49 | 50 | # Inactive button color 51 | button_inactive_color = (BLUE,BLACK,OFF) 52 | 53 | # Active button key color 54 | button_key_active_color = (MAGENTA,BLACK,ON) 55 | 56 | # Inactive button key color 57 | button_key_inactive_color = (BLUE,BLACK,OFF) 58 | 59 | # Active button label color 60 | button_label_active_color = (MAGENTA,BLACK,ON) 61 | 62 | # Inactive button label color 63 | button_label_inactive_color = (BLUE,BLACK,OFF) 64 | 65 | # Input box color TODO 66 | inputbox_color = (BLUE,WHITE,OFF) 67 | 68 | # Input box border color TODO 69 | inputbox_border_color = (BLACK,BLACK,ON) 70 | 71 | # Search box color TODO 72 | searchbox_color = (YELLOW,BLACK,ON) 73 | 74 | # Search box title color TODO 75 | searchbox_title_color = (WHITE,WHITE,ON) 76 | 77 | # Search box border color 78 | searchbox_border_color = (RED,WHITE,OFF) 79 | 80 | # File position indicator color 81 | position_indicator_color = (YELLOW,BLACK,OFF) 82 | 83 | # Menu box color 84 | menubox_color = (BLUE,BLACK,OFF) 85 | 86 | # Menu box border color 87 | menubox_border_color = (BLUE,BLACK,ON) 88 | 89 | # 90 | # Item color -- good green 91 | item_color = (WHITE,BLACK,OFF) 92 | 93 | # Selected item color 94 | item_selected_color = (BLACK,MAGENTA,OFF) 95 | 96 | # Tag color 97 | tag_color = (BLUE,BLACK,OFF) 98 | 99 | # Selected tag color 100 | tag_selected_color = (MAGENTA,BLACK,ON) 101 | 102 | # Tag key color 103 | tag_key_color = (BLUE,BLACK,OFF) 104 | 105 | # Selected tag key color 106 | tag_key_selected_color = (BLACK,MAGENTA,ON) 107 | 108 | # Check box color 109 | check_color = (CYAN,BLACK,OFF) 110 | 111 | # Selected check box color 112 | check_selected_color = (WHITE,CYAN,ON) 113 | 114 | # Up arrow color 115 | uarrow_color = (GREEN,BLUE,ON) 116 | 117 | # Down arrow color 118 | darrow_color = (GREEN,BLUE,ON) 119 | 120 | # Item help-text color 121 | itemhelp_color = (MAGENTA,BLACK,OFF) 122 | 123 | 124 | # alislack fix 125 | # border2 are the shadows arounds the boxes 126 | # change from dialog_color to GREEN,BLACK 127 | # to remove the shadow at bottom of screen 128 | # 129 | # Dialog box border2 color 130 | #border2_color = dialog_color 131 | border2_color = (BLUE,BLACK,OFF) 132 | 133 | # Input box border2 color 134 | inputbox_border2_color = (GREEN,BLACK,ON) 135 | 136 | 137 | # Search box border2 color 138 | searchbox_border2_color = (GREEN,BLACK,ON) 139 | 140 | 141 | # Menu box border2 color 142 | menubox_border2_color = (BLUE,BLACK,ON) 143 | 144 | -------------------------------------------------------------------------------- /assets/themes/classic.dialogrc: -------------------------------------------------------------------------------- 1 | # 2 | # Run-time configuration file for dialog 3 | # 4 | # Automatically generated by "dialog --create-rc " 5 | # 6 | # 7 | # Types of values: 8 | # 9 | # Number - 10 | # String - "string" 11 | # Boolean - 12 | # Attribute - (foreground,background,highlight?,underline?,reverse?) 13 | 14 | # Set aspect-ration. 15 | aspect = 0 16 | 17 | # Set separator (for multiple widgets output). 18 | separate_widget = "" 19 | 20 | # Set tab-length (for textbox tab-conversion). 21 | tab_len = 0 22 | 23 | # Make tab-traversal for checklist, etc., include the list. 24 | visit_items = OFF 25 | 26 | # Show scrollbar in dialog boxes? 27 | use_scrollbar = OFF 28 | 29 | # Shadow dialog boxes? This also turns on color. 30 | use_shadow = ON 31 | 32 | # Turn color support ON or OFF 33 | use_colors = ON 34 | 35 | # Screen color 36 | screen_color = (CYAN,BLUE,ON) 37 | 38 | # Shadow color 39 | shadow_color = (BLACK,BLACK,ON) 40 | 41 | # Dialog box color 42 | dialog_color = (BLACK,WHITE,OFF) 43 | 44 | # Dialog box title color 45 | title_color = (BLUE,WHITE,ON) 46 | 47 | # Dialog box border color 48 | border_color = (WHITE,WHITE,ON) 49 | 50 | # Active button color 51 | button_active_color = (WHITE,BLUE,ON) 52 | 53 | # Inactive button color 54 | button_inactive_color = dialog_color 55 | 56 | # Active button key color 57 | button_key_active_color = button_active_color 58 | 59 | # Inactive button key color 60 | button_key_inactive_color = (RED,WHITE,OFF) 61 | 62 | # Active button label color 63 | button_label_active_color = (YELLOW,BLUE,ON) 64 | 65 | # Inactive button label color 66 | button_label_inactive_color = (BLACK,WHITE,ON) 67 | 68 | # Input box color 69 | inputbox_color = dialog_color 70 | 71 | # Input box border color 72 | inputbox_border_color = dialog_color 73 | 74 | # Search box color 75 | searchbox_color = dialog_color 76 | 77 | # Search box title color 78 | searchbox_title_color = title_color 79 | 80 | # Search box border color 81 | searchbox_border_color = border_color 82 | 83 | # File position indicator color 84 | position_indicator_color = title_color 85 | 86 | # Menu box color 87 | menubox_color = dialog_color 88 | 89 | # Menu box border color 90 | menubox_border_color = border_color 91 | 92 | # Item color 93 | item_color = dialog_color 94 | 95 | # Selected item color 96 | item_selected_color = button_active_color 97 | 98 | # Tag color 99 | tag_color = title_color 100 | 101 | # Selected tag color 102 | tag_selected_color = button_label_active_color 103 | 104 | # Tag key color 105 | tag_key_color = button_key_inactive_color 106 | 107 | # Selected tag key color 108 | tag_key_selected_color = (RED,BLUE,ON) 109 | 110 | # Check box color 111 | check_color = dialog_color 112 | 113 | # Selected check box color 114 | check_selected_color = button_active_color 115 | 116 | # Up arrow color 117 | uarrow_color = (GREEN,WHITE,ON) 118 | 119 | # Down arrow color 120 | darrow_color = uarrow_color 121 | 122 | # Item help-text color 123 | itemhelp_color = (WHITE,BLACK,OFF) 124 | 125 | # Active form text color 126 | form_active_text_color = button_active_color 127 | 128 | # Form text color 129 | form_text_color = (WHITE,CYAN,ON) 130 | 131 | # Readonly form item color 132 | form_item_readonly_color = (CYAN,WHITE,ON) 133 | 134 | # Dialog box gauge color 135 | gauge_color = title_color 136 | 137 | # Dialog box border2 color 138 | border2_color = dialog_color 139 | 140 | # Input box border2 color 141 | inputbox_border2_color = dialog_color 142 | 143 | # Search box border2 color 144 | searchbox_border2_color = dialog_color 145 | 146 | # Menu box border2 color 147 | menubox_border2_color = dialog_color 148 | -------------------------------------------------------------------------------- /assets/themes/dark_slackware.dialogrc: -------------------------------------------------------------------------------- 1 | # 2 | # Run-time configuration file for dialog 3 | # 4 | # Automatically generated by "dialog --create-rc " 5 | # 6 | # 7 | # Types of values: 8 | # 9 | # Number - 10 | # String - "string" 11 | # Boolean - 12 | # Attribute - (foreground,background,highlight?) 13 | 14 | # Set aspect-ration. 15 | aspect = 0 16 | 17 | # Set separator (for multiple widgets output). 18 | separate_widget = "" 19 | 20 | # Set tab-length (for textbox tab-conversion). 21 | tab_len = 0 22 | 23 | # Make tab-traversal for checklist, etc., include the list. 24 | visit_items = OFF 25 | 26 | # Shadow dialog boxes? This also turns on color. 27 | use_shadow = OFF 28 | 29 | # Turn color support ON or OFF 30 | use_colors = ON 31 | 32 | # Screen color 33 | #screen_color = (CYAN,BLUE,ON) 34 | screen_color = (BLACK,BLACK,ON) 35 | 36 | # Shadow color 37 | shadow_color = (BLACK,BLACK,ON) 38 | 39 | # Dialog box color 40 | #dialog_color = (BLACK,WHITE,OFF) 41 | dialog_color = (CYAN,BLACK,OFF) 42 | 43 | # Dialog box title color 44 | #title_color = (BLUE,WHITE,ON) 45 | title_color = (YELLOW,BLACK,ON) 46 | 47 | # Dialog box border color 48 | #border_color = (WHITE,WHITE,ON) 49 | border_color = (BLACK,BLACK,ON) 50 | 51 | # Active button color 52 | #button_active_color = (WHITE,BLUE,ON) 53 | button_active_color = (CYAN,BLACK,ON) 54 | 55 | # Inactive button color 56 | #button_inactive_color = dialog_color 57 | button_inactive_color = (CYAN,BLACK,OFF) 58 | 59 | # Active button key color 60 | #button_key_active_color = button_active_color 61 | button_key_active_color = (RED,BLACK,ON) 62 | 63 | # Inactive button key color 64 | #button_key_inactive_color = (RED,WHITE,OFF) 65 | button_key_inactive_color = (RED,BLACK,ON) 66 | 67 | # Active button label color 68 | #button_label_active_color = (YELLOW,BLUE,ON) 69 | button_label_active_color = (YELLOW,BLACK,ON) 70 | 71 | # Inactive button label color 72 | #button_label_inactive_color = (BLACK,WHITE,ON) 73 | button_label_inactive_color = (RED,BLACK,OFF) 74 | 75 | # Input box color 76 | #inputbox_color = dialog_color 77 | inputbox_color = (WHITE,BLACK,OFF) 78 | 79 | # Input box border color 80 | #inputbox_border_color = dialog_color 81 | inputbox_border_color = (WHITE,BLACK,OFF) 82 | 83 | # Search box color 84 | #searchbox_color = dialog_color 85 | searchbox_color = (RED,BLACK,OFF) 86 | 87 | # Search box title color 88 | #searchbox_title_color = title_color 89 | searchbox_title_color = (YELLOW,BLACK,ON) 90 | 91 | # Search box border color 92 | #searchbox_border_color = border_color 93 | searchbox_title_color = (YELLOW,BLACK,ON) 94 | 95 | # File position indicator color 96 | #position_indicator_color = title_color 97 | position_indicator_color = (YELLOW,BLACK,ON) 98 | 99 | # Menu box color 100 | #menubox_color = dialog_color 101 | menubox_color = (MAGENTA,BLACK,ON) 102 | 103 | # Menu box border color 104 | #menubox_border_color = border_color 105 | menubox_border_color = (MAGENTA,BLACK,OFF) 106 | 107 | # Item color 108 | #item_color = dialog_color 109 | item_color = (WHITE,BLACK,OFF) 110 | 111 | # Selected item color 112 | #item_selected_color = button_active_color 113 | item_selected_color = (WHITE,BLUE,ON) 114 | 115 | # Tag color 116 | #tag_color = title_color 117 | tag_color = (YELLOW,BLACK,OFF) 118 | 119 | # Selected tag color 120 | #tag_selected_color = button_label_active_color 121 | tag_selected_color = (YELLOW,BLACK,ON) 122 | 123 | # Tag key color 124 | #tag_key_color = button_key_inactive_color 125 | tag_key_color = (RED,BLACK,ON) 126 | 127 | # Selected tag key color 128 | #tag_key_selected_color = (RED,BLUE,ON) 129 | tag_key_selected_color = (MAGENTA,BLACK,ON) 130 | 131 | # Check box color 132 | #check_color = dialog_color 133 | check_color = (RED,BLACK,OFF) 134 | 135 | # Selected check box color 136 | #check_selected_color = button_active_color 137 | check_selected_color = (RED,BLACK,ON) 138 | 139 | # Up arrow color 140 | #uarrow_color = (GREEN,WHITE,ON) 141 | uarrow_color = (GREEN,BLACK,ON) 142 | 143 | # Down arrow color 144 | #darrow_color = uarrow_color 145 | darrow_color = (MAGENTA,BLACK,ON) 146 | 147 | # Item help-text color 148 | itemhelp_color = (WHITE,BLACK,OFF) 149 | 150 | # Active form text color 151 | form_active_text_color = button_active_color 152 | 153 | # Form text color 154 | form_text_color = (WHITE,CYAN,ON) 155 | 156 | # Readonly form item color 157 | form_item_readonly_color = (CYAN,WHITE,ON) 158 | 159 | # Dialog box gauge color 160 | gauge_color = title_color 161 | 162 | # Dialog box border2 color 163 | border2_color = dialog_color 164 | 165 | # Input box border2 color 166 | inputbox_border2_color = dialog_color 167 | 168 | # Search box border2 color 169 | searchbox_border2_color = dialog_color 170 | 171 | # Menu box border2 color 172 | menubox_border2_color = dialog_color 173 | -------------------------------------------------------------------------------- /assets/themes/evangelion.dialogrc: -------------------------------------------------------------------------------- 1 | # 2 | # Run-time configuration file for dialog 3 | # 4 | # Automatically generated by "dialog --create-rc " 5 | # 6 | # 7 | # Types of values: 8 | # 9 | # Number - 10 | # String - "string" 11 | # Boolean - 12 | # Attribute - (foreground,background,highlight?) 13 | # Colors - BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN and WHITE 14 | 15 | # Set aspect-ration. 16 | aspect = 0 17 | 18 | # Set separator (for multiple widgets output). 19 | separate_widget = "" 20 | 21 | # Set tab-length (for textbox tab-conversion). 22 | tab_len = 0 23 | 24 | # Make tab-traversal for checklist, etc., include the list. 25 | visit_items = OFF 26 | 27 | # Shadow dialog boxes? This also turns on color. 28 | use_shadow = OFF 29 | 30 | # Turn color support ON or OFF 31 | use_colors = ON 32 | 33 | # Screen color 34 | screen_color = (YELLOW,BLACK,ON) 35 | 36 | # Shadow color 37 | shadow_color = (GREEN,YELLOW,ON) 38 | 39 | # Dialog box color 40 | dialog_color = (YELLOW,BLACK,OFF) 41 | 42 | # Dialog box title color 43 | title_color = (BLACK,YELLOW,ON) 44 | 45 | # Dialog box border color 46 | border_color = (BLACK,BLACK,ON) 47 | 48 | # Active button color 49 | button_active_color = (YELLOW,BLACK,ON) 50 | 51 | # Inactive button color 52 | button_inactive_color = (YELLOW, BLACK, OFF) 53 | 54 | # Active button key color 55 | button_key_active_color = button_active_color 56 | 57 | # Inactive button key color 58 | button_key_inactive_color = (YELLOW, BLACK, OFF) 59 | 60 | # Active button label color 61 | button_label_active_color = (BLACK,YELLOW,OFF) 62 | 63 | # Inactive button label color 64 | button_label_inactive_color = (YELLOW,BLACK,OFF) 65 | 66 | # Input box color 67 | inputbox_color = dialog_color 68 | 69 | # Input box border color 70 | inputbox_border_color = dialog_color 71 | 72 | # Search box color 73 | searchbox_color = dialog_color 74 | 75 | # Search box title color 76 | searchbox_title_color = title_color 77 | 78 | # Search box border color 79 | searchbox_border_color = border_color 80 | 81 | # File position indicator color 82 | position_indicator_color = (YELLOW, BLACK, ON) 83 | 84 | # Menu box color 85 | menubox_color = dialog_color 86 | 87 | # Menu box border color 88 | menubox_border_color = border_color 89 | 90 | # Item color 91 | item_color = dialog_color 92 | 93 | # Selected item color 94 | item_selected_color = (YELLOW, BLACK, OFF) 95 | 96 | # Tag color 97 | tag_color = title_color 98 | 99 | # Selected tag color 100 | tag_selected_color = button_label_active_color 101 | 102 | # Tag key color 103 | tag_key_color = button_key_inactive_color 104 | 105 | # Selected tag key color 106 | tag_key_selected_color = (RED,BLUE,ON) 107 | 108 | # Check box color 109 | check_color = dialog_color 110 | 111 | # Selected check box color 112 | check_selected_color = button_active_color 113 | 114 | # Up arrow color 115 | uarrow_color = (GREEN,WHITE,ON) 116 | 117 | # Down arrow color 118 | darrow_color = uarrow_color 119 | 120 | # Item help-text color 121 | itemhelp_color = (WHITE,BLACK,OFF) 122 | 123 | # Active form text color 124 | form_active_text_color = button_active_color 125 | 126 | # Form text color 127 | form_text_color = (WHITE,CYAN,ON) 128 | 129 | # Readonly form item color 130 | form_item_readonly_color = (CYAN,WHITE,ON) 131 | 132 | # Dialog box gauge color 133 | gauge_color = title_color 134 | 135 | # Dialog box border2 color 136 | border2_color = dialog_color 137 | 138 | # Input box border2 color 139 | inputbox_border2_color = dialog_color 140 | 141 | # Search box border2 color 142 | searchbox_border2_color = dialog_color 143 | 144 | # Menu box border2 color 145 | menubox_border2_color = dialog_color 146 | -------------------------------------------------------------------------------- /assets/themes/monochrome.dialogrc: -------------------------------------------------------------------------------- 1 | # 2 | # Run-time configuration file for dialog 3 | # 4 | # Automatically generated by "dialog --create-rc " 5 | # 6 | # 7 | # Types of values: 8 | # 9 | # Number - 10 | # String - "string" 11 | # Boolean - 12 | # Attribute - (foreground,background,highlight?) 13 | 14 | # Set aspect-ration. 15 | aspect = 0 16 | 17 | # Set separator (for multiple widgets output). 18 | separate_widget = "" 19 | 20 | # Set tab-length (for textbox tab-conversion). 21 | tab_len = 0 22 | 23 | # Make tab-traversal for checklist, etc., include the list. 24 | visit_items = OFF 25 | 26 | # Shadow dialog boxes? This also turns on color. 27 | use_shadow = OFF 28 | 29 | # Turn color support ON or OFF 30 | use_colors = OFF 31 | 32 | # Screen color 33 | screen_color = (CYAN,BLUE,ON) 34 | 35 | # Shadow color 36 | shadow_color = (BLACK,BLACK,ON) 37 | 38 | # Dialog box color 39 | dialog_color = (BLACK,WHITE,OFF) 40 | 41 | # Dialog box title color 42 | title_color = (BLUE,WHITE,ON) 43 | 44 | # Dialog box border color 45 | border_color = (WHITE,WHITE,ON) 46 | 47 | # Active button color 48 | button_active_color = (WHITE,BLUE,ON) 49 | 50 | # Inactive button color 51 | button_inactive_color = dialog_color 52 | 53 | # Active button key color 54 | button_key_active_color = button_active_color 55 | 56 | # Inactive button key color 57 | button_key_inactive_color = (RED,WHITE,OFF) 58 | 59 | # Active button label color 60 | button_label_active_color = (YELLOW,BLUE,ON) 61 | 62 | # Inactive button label color 63 | button_label_inactive_color = (BLACK,WHITE,ON) 64 | 65 | # Input box color 66 | inputbox_color = dialog_color 67 | 68 | # Input box border color 69 | inputbox_border_color = dialog_color 70 | 71 | # Search box color 72 | searchbox_color = dialog_color 73 | 74 | # Search box title color 75 | searchbox_title_color = title_color 76 | 77 | # Search box border color 78 | searchbox_border_color = border_color 79 | 80 | # File position indicator color 81 | position_indicator_color = title_color 82 | 83 | # Menu box color 84 | menubox_color = dialog_color 85 | 86 | # Menu box border color 87 | menubox_border_color = border_color 88 | 89 | # Item color 90 | item_color = dialog_color 91 | 92 | # Selected item color 93 | item_selected_color = button_active_color 94 | 95 | # Tag color 96 | tag_color = title_color 97 | 98 | # Selected tag color 99 | tag_selected_color = button_label_active_color 100 | 101 | # Tag key color 102 | tag_key_color = button_key_inactive_color 103 | 104 | # Selected tag key color 105 | tag_key_selected_color = (RED,BLUE,ON) 106 | 107 | # Check box color 108 | check_color = dialog_color 109 | 110 | # Selected check box color 111 | check_selected_color = button_active_color 112 | 113 | # Up arrow color 114 | uarrow_color = (GREEN,WHITE,ON) 115 | 116 | # Down arrow color 117 | darrow_color = uarrow_color 118 | 119 | # Item help-text color 120 | itemhelp_color = (WHITE,BLACK,OFF) 121 | 122 | # Active form text color 123 | form_active_text_color = button_active_color 124 | 125 | # Form text color 126 | form_text_color = (WHITE,CYAN,ON) 127 | 128 | # Readonly form item color 129 | form_item_readonly_color = (CYAN,WHITE,ON) 130 | 131 | # Dialog box gauge color 132 | gauge_color = title_color 133 | 134 | # Dialog box border2 color 135 | border2_color = dialog_color 136 | 137 | # Input box border2 color 138 | inputbox_border2_color = dialog_color 139 | 140 | # Search box border2 color 141 | searchbox_border2_color = dialog_color 142 | 143 | # Menu box border2 color 144 | menubox_border2_color = dialog_color 145 | -------------------------------------------------------------------------------- /assets/themes/nighttime.dialogrc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/dialog 2 | # Run-time configuration file for dialog 3 | # 4 | # Types of values: 5 | # 6 | # Number - 7 | # String - "string" 8 | # Boolean - 9 | # Attribute - (foreground,background,highlight?) 10 | # Colors - BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN and WHITE 11 | 12 | # Set aspect-ratio. 13 | aspect = 0 14 | 15 | # Set separator (for multiple widgets output). 16 | separate_widget = "" 17 | 18 | # Set tab-length (for textbox tab-conversion). 19 | tab_len = 0 20 | 21 | # Make tab-traversal for checklist, etc., include the list. 22 | visit_items = OFF 23 | 24 | # Shadow dialog boxes? This also turns on color. 25 | #use_shadow = OFF 26 | use_shadow = ON 27 | 28 | # Turn color support ON or OFF 29 | #use_colors = OFF 30 | use_colors = ON 31 | 32 | # Attribute - (foreground,background,highlight?) 33 | #screen_color = (CYAN,BLUE,ON) 34 | screen_color = (CYAN,BLACK,ON) 35 | 36 | #shadow_color = (BLACK,BLACK,ON) 37 | shadow_color = (BLACK,BLUE,ON) 38 | 39 | #dialog_color = (BLACK,WHITE,OFF) 40 | dialog_color = (WHITE,BLACK,OFF) 41 | 42 | # Dialog box title color 43 | #title_color = (BLUE,WHITE,ON) 44 | title_color = (BLUE,BLACK,ON) 45 | 46 | # Dialog box border color 47 | #border_color = (WHITE,WHITE,ON) 48 | border_color = (MAGENTA,BLACK,ON) 49 | 50 | # Active button color 51 | button_active_color = (WHITE,MAGENTA,ON) 52 | 53 | # Inactive button color 54 | button_inactive_color = (WHITE,BLACK,OFF) 55 | 56 | # Active button key color 57 | button_key_active_color = (YELLOW,BLUE,ON) 58 | 59 | # Inactive button key color 60 | button_key_inactive_color = (RED,BLACK,OFF) 61 | 62 | # Active button label color 63 | button_label_active_color = (WHITE,MAGENTA,ON) 64 | 65 | # Inactive button label color 66 | button_label_inactive_color = (MAGENTA,BLACK,ON) 67 | 68 | # Input box color 69 | inputbox_color = (WHITE,BLACK,OFF) 70 | 71 | # Input box border color 72 | inputbox_border_color = (WHITE,BLACK,OFF) 73 | 74 | # Search box color 75 | searchbox_color = (WHITE,BLACK,OFF) 76 | 77 | # Search box title color 78 | searchbox_title_color = (BLUE,BLACK,ON) 79 | 80 | # Search box border color 81 | searchbox_border_color = (WHITE,BLACK,ON) 82 | 83 | # File position indicator color 84 | position_indicator_color = (BLUE,WHITE,ON) 85 | 86 | # Menu box color 87 | menubox_color = (WHITE,BLACK,OFF) 88 | 89 | # Menu box border color 90 | menubox_border_color = border_color 91 | 92 | # Item color 93 | item_color = dialog_color 94 | 95 | # Selected item color 96 | item_selected_color = (WHITE,MAGENTA,ON) 97 | 98 | # Tag color 99 | tag_color = (BLUE,BLACK,ON) 100 | 101 | # Selected tag color 102 | tag_selected_color = (YELLOW,BLUE,ON) 103 | 104 | # Tag key color 105 | tag_key_color = (RED,BLACK,OFF) 106 | 107 | # Selected tag key color 108 | tag_key_selected_color = (RED,BLUE,ON) 109 | 110 | # Check box color 111 | check_color = (WHITE,BLACK,OFF) 112 | 113 | # Selected check box color 114 | check_selected_color = (WHITE,BLUE,ON) 115 | 116 | # Up arrow color 117 | uarrow_color = (GREEN,BLACK,ON) 118 | 119 | # Down arrow color 120 | darrow_color = (GREEN,BLACK,ON) 121 | 122 | # Item help-text color 123 | itemhelp_color = (WHITE,BLACK,OFF) 124 | 125 | # Active form text color 126 | form_active_text_color = (WHITE,BLUE,ON) 127 | 128 | # Form text color 129 | form_text_color = (WHITE,CYAN,ON) 130 | 131 | # Readonly form item color 132 | form_item_readonly_color = (CYAN,BLACK,ON) 133 | 134 | # Dialog box gauge color 135 | gauge_color = (BLUE,BLACK,ON) 136 | 137 | # Dialog box border2 color 138 | border2_color = dialog_color 139 | 140 | # Input box border2 color 141 | inputbox_border2_color = dialog_color 142 | 143 | # Search box border2 color 144 | searchbox_border2_color = dialog_color 145 | 146 | # Menu box border2 color 147 | menubox_border2_color = dialog_color 148 | -------------------------------------------------------------------------------- /assets/welcome_banner: -------------------------------------------------------------------------------- 1 | Welcome message here 2 | -------------------------------------------------------------------------------- /boards/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfs-jfs/ssh-forum/9820db06d06dc38199812284c24499b0b6430e9b/boards/.gitkeep -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23.0 2 | 3 | ENV TERM=xterm-256color 4 | 5 | RUN apt update && apt upgrade -y 6 | RUN apt install dialog locales 7 | RUN locale-gen en_US.UTF-8 8 | 9 | WORKDIR /usr/src/app 10 | 11 | COPY go.mod go.sum ./ 12 | RUN go mod download && go mod verify 13 | 14 | COPY . . 15 | RUN go build -v -o ssh-server server.go 16 | 17 | CMD ["./ssh-server"] 18 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## Set locale for special chars 3 | export LC_ALL=en_US.UTF-8 4 | 5 | # Imports 6 | source ./src/logger.sh 7 | source ./src/controllers.sh 8 | 9 | # Config vars # 10 | # Logs 11 | export LOG_LEVEL=$LOG_DEBUG 12 | export LOG_FILE="analog.logs" 13 | 14 | # Archived threads location 15 | export ARCHIVE="archive" 16 | 17 | # Chat 18 | export CHAT_FILE="chat.feed" 19 | export CHAT_MSG_MAX_LENGTH=200 20 | 21 | # Forum 22 | export THREAD_TITLE_MAX_LENGTH=40 23 | export THREAD_TITLE_MIN_LENGTH=5 24 | 25 | export THREAD_BODY_MAX_LENGTH=1024 26 | export THREAD_BODY_MIN_LENGTH=20 27 | 28 | export REPLY_MAX_LENGTH=1024 29 | export REPLY_MIN_LENGTH=2 30 | 31 | export MAX_ACTIVE_THREADS_PER_BOARD=28 32 | export MAX_REPLIES_PER_THREAD=120 33 | 34 | export BANNER="[!]AnalogCity :: Interface v3.0[!]" 35 | export BOARDS=(\ 36 | "Board Name" "Description"\ 37 | ) 38 | 39 | # User defualts 40 | export USER_NAME_MAX_LENGTH=12 41 | export USER_NAME_MIN_LENGTH=3 42 | export AUTHOR="pagan" 43 | export DIALOGRC="./assets/themes/analog.dialogrc" 44 | if [ -n "$1" ] && [ -f "./assets/themes/$1.dialogrc" ]; then 45 | export DIALOGRC="./assets/themes/$1.dialogrc" 46 | fi 47 | 48 | # User interface 49 | export WELCOME_WIDTH=60 50 | export WELCOME_HEIGHT=10 51 | 52 | export MAIN_MENU_WIDTH=80 53 | export MAIN_MENU_HEIGHT=13 54 | export MAIN_MENU_MENU_HEIGHT=6 55 | 56 | export CHAT_FEED_HEIGHT=30 57 | export CHAT_FEED_WIDTH=110 58 | export CHAT_INPUT_HEIGHT=8 59 | export CHAT_INPUT_WIDTH=110 60 | 61 | export BOARD_SELECTOR_HEIGHT=13 62 | export BOARD_SELECTOR_WIDTH=100 63 | export BOARD_SELECTOR_MENU_HEIGHT=5 64 | 65 | export THREAD_SELECTOR_HEIGHT=40 66 | export THREAD_SELECTOR_WIDTH=120 67 | export THREAD_SELECTOR_MENU_HEIGHT=30 68 | 69 | export THREAD_FEED_HEIGHT=40 70 | export THREAD_FEED_WIDTH=120 71 | 72 | export THREAD_TITLE_FORM_HEIGHT=10 73 | export THREAD_TITLE_FORM_WIDTH=50 74 | 75 | export THREAD_BODY_FORM_HEIGHT=20 76 | export THREAD_BODY_FORM_WIDTH=120 77 | 78 | export REPLY_FORM_HEIGHT=20 79 | export REPLY_FORM_WIDTH=120 80 | 81 | export USER_NAME_FORM_HEIGHT=10 82 | export USER_NAME_FORM_WIDTH=50 83 | # Config vars -- END # 84 | 85 | main_controller 86 | clear 87 | exit 0 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module ssh-forum-server 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/log v0.4.0 7 | github.com/charmbracelet/ssh v0.0.0-20241211182756-4fe22b0f1b7c 8 | github.com/charmbracelet/wish v1.4.4 9 | ) 10 | 11 | require ( 12 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 13 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 14 | github.com/charmbracelet/bubbletea v1.2.4 // indirect 15 | github.com/charmbracelet/keygen v0.5.1 // indirect 16 | github.com/charmbracelet/lipgloss v1.0.0 // indirect 17 | github.com/charmbracelet/x/ansi v0.4.5 // indirect 18 | github.com/charmbracelet/x/conpty v0.1.0 // indirect 19 | github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect 20 | github.com/charmbracelet/x/term v0.2.1 // indirect 21 | github.com/charmbracelet/x/termios v0.1.0 // indirect 22 | github.com/creack/pty v1.1.21 // indirect 23 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 24 | github.com/go-logfmt/logfmt v0.6.0 // indirect 25 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/mattn/go-localereader v0.0.1 // indirect 28 | github.com/mattn/go-runewidth v0.0.15 // indirect 29 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 30 | github.com/muesli/cancelreader v0.2.2 // indirect 31 | github.com/muesli/termenv v0.15.3-0.20240509142007-81b8f94111d5 // indirect 32 | github.com/rivo/uniseg v0.4.7 // indirect 33 | golang.org/x/crypto v0.31.0 // indirect 34 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 35 | golang.org/x/sync v0.10.0 // indirect 36 | golang.org/x/sys v0.28.0 // indirect 37 | golang.org/x/text v0.21.0 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 2 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= 6 | github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= 7 | github.com/charmbracelet/keygen v0.5.1 h1:zBkkYPtmKDVTw+cwUyY6ZwGDhRxXkEp0Oxs9sqMLqxI= 8 | github.com/charmbracelet/keygen v0.5.1/go.mod h1:zznJVmK/GWB6dAtjluqn2qsttiCBhA5MZSiwb80fcHw= 9 | github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= 10 | github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= 11 | github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= 12 | github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= 13 | github.com/charmbracelet/ssh v0.0.0-20241211182756-4fe22b0f1b7c h1:treQxMBdI2PaD4eOYfFux8stfCkUxhuUxaqGcxKqVpI= 14 | github.com/charmbracelet/ssh v0.0.0-20241211182756-4fe22b0f1b7c/go.mod h1:CY1xbl2z+ZeBmNWItKZyxx0zgDgnhmR57+DTsHOobJ4= 15 | github.com/charmbracelet/wish v1.4.4 h1:wtfoAMkf8Db9zi+9Lme2f7XKMxL6BqfgDWbqcTUHLaU= 16 | github.com/charmbracelet/wish v1.4.4/go.mod h1:XB8v51UxIFMRlUod9lLaAgOsj/wpe+qW9HjsoYIiNMo= 17 | github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= 18 | github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 19 | github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= 20 | github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= 21 | github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= 22 | github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= 23 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 24 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 25 | github.com/charmbracelet/x/termios v0.1.0 h1:y4rjAHeFksBAfGbkRDmVinMg7x7DELIGAFbdNvxg97k= 26 | github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8ouDK0AZEbG7r+/U= 27 | github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= 28 | github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 29 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 30 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 32 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 33 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 34 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 35 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 36 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 37 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 38 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 39 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 40 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 41 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 42 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 43 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 44 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 45 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 46 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 47 | github.com/muesli/termenv v0.15.3-0.20240509142007-81b8f94111d5 h1:NiONcKK0EV5gUZcnCiPMORaZA0eBDc+Fgepl9xl4lZ8= 48 | github.com/muesli/termenv v0.15.3-0.20240509142007-81b8f94111d5/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= 49 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 50 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 51 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 52 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 53 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 54 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 55 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 56 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 57 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 58 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 59 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 60 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 61 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 62 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 65 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 66 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 67 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 68 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 69 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 70 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 71 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 72 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Shamelessly copied from https://github.com/charmbracelet/wish/blob/main/examples/exec/main.go 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "net" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/charmbracelet/log" 15 | "github.com/charmbracelet/ssh" 16 | "github.com/charmbracelet/wish" 17 | "github.com/charmbracelet/wish/activeterm" 18 | "github.com/charmbracelet/wish/logging" 19 | ) 20 | 21 | const ( 22 | host = "0.0.0.0" 23 | port = "2222" 24 | ) 25 | 26 | func main() { 27 | s, err := wish.NewServer( 28 | wish.WithAddress(net.JoinHostPort(host, port)), 29 | 30 | // Allocate a pty. 31 | // This creates a pseudoconsole on windows, compatibility is limited in 32 | // that case, see the open issues for more details. 33 | ssh.AllocatePty(), 34 | wish.WithMiddleware( 35 | func(next ssh.Handler) ssh.Handler { 36 | return func(s ssh.Session) { 37 | cmd := wish.Command(s, "bash", "entrypoint.sh", s.User()) 38 | if err := cmd.Run(); err != nil { 39 | wish.Fatalln(s, err) 40 | } 41 | next(s) 42 | } 43 | }, 44 | // ensure the user has requested a tty 45 | activeterm.Middleware(), 46 | logging.Middleware(), 47 | ), 48 | ) 49 | if err != nil { 50 | log.Error("Could not start server", "error", err) 51 | } 52 | 53 | done := make(chan os.Signal, 1) 54 | signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 55 | log.Info("Starting SSH server", "host", host, "port", port) 56 | go func() { 57 | if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) { 58 | log.Error("Could not start server", "error", err) 59 | done <- nil 60 | } 61 | }() 62 | 63 | <-done 64 | log.Info("Stopping SSH server") 65 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 66 | defer func() { cancel() }() 67 | if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) { 68 | log.Error("Could not stop server", "error", err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/controllers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./src/visuals.sh 4 | source ./src/logger.sh 5 | source ./src/models.sh 6 | 7 | # The default controller used on user connection. Serves as a starting point to access to the other parts of the system. 8 | main_controller() { 9 | local is_running=true 10 | 11 | welcome_banner 12 | while $is_running; do 13 | case $(main_menu) in 14 | "") 15 | debug "forums selected" 16 | boards_controller 17 | ;; 18 | "") 19 | debug "chat selected" 20 | chat_controller 21 | ;; 22 | "") 23 | debug "config selected" 24 | user_name_controller 25 | ;; 26 | *) 27 | debug "main menu exit" 28 | is_running=false 29 | esac 30 | done 31 | } 32 | 33 | # User name controller 34 | # Controls how to change the username 35 | user_name_controller() { 36 | 37 | local ok_username=false 38 | local error="" 39 | local new_username="" 40 | 41 | while ! $ok_username; do 42 | new_username="$(user_name_form "$error" | xargs)" 43 | 44 | if [ -z "$new_username" ];then 45 | return 46 | fi 47 | 48 | # Parse input 49 | new_username="$(echo -e "$new_username" | tr -d '\t')" 50 | new_username="$(echo -e "$new_username" | tr -d '\a')" 51 | new_username="$(echo -e "$new_username" | tr -d '\b')" 52 | new_username="$(echo -e "$new_username" | tr -d '\f')" 53 | new_username="$(echo -e "$new_username" | tr -d '\n')" 54 | new_username="$(echo -e "$new_username" | tr -d '\r')" 55 | new_username="$(echo -e "$new_username" | tr -d '\t')" 56 | new_username="$(echo -e "$new_username" | tr -d '\v')" 57 | 58 | if [ ${#new_username} -lt "$USER_NAME_MIN_LENGTH" ]; then 59 | error="At least $USER_NAME_MIN_LENGTH characters" 60 | continue 61 | fi 62 | 63 | if [ ${#new_username} -gt "$USER_NAME_MAX_LENGTH" ]; then 64 | error="No more than $USER_NAME_MAX_LENGTH characters" 65 | continue 66 | fi 67 | 68 | ok_username=true 69 | done 70 | 71 | export AUTHOR="$new_username" 72 | } 73 | 74 | # Chat controller 75 | # Controller used to control the flow of execution of the chat view. 76 | # Responsible for displaying the chat feed and processing any new message. 77 | chat_controller() { 78 | touch "$CHAT_FILE" 79 | 80 | local exit_chat=false 81 | while ! $exit_chat; do 82 | local new_message 83 | new_message="$(chat_interface)" 84 | 85 | # Exit on empty message 86 | if [ -z "$new_message" ];then 87 | exit_chat=true 88 | continue 89 | fi 90 | 91 | # Parse input 92 | new_message="$(echo -e "$new_message" | tr -d '\t')" 93 | new_message="$(echo -e "$new_message" | tr -d '\a')" 94 | new_message="$(echo -e "$new_message" | tr -d '\b')" 95 | new_message="$(echo -e "$new_message" | tr -d '\f')" 96 | new_message="$(echo -e "$new_message" | tr -d '\n')" 97 | new_message="$(echo -e "$new_message" | tr -d '\r')" 98 | new_message="$(echo -e "$new_message" | tr -d '\t')" 99 | new_message="$(echo -e "$new_message" | tr -d '\v')" 100 | 101 | # Format input 102 | new_message="[$AUTHOR] @ $(date +%x--%X): $new_message" 103 | 104 | # Append to chat feed 105 | echo -e "$new_message\n" >> "$CHAT_FILE" 106 | done 107 | } 108 | 109 | # Boards Controller 110 | # Controller to display list of boards and select one of them for displaying threads 111 | boards_controller() { 112 | 113 | local go_back=false 114 | while ! $go_back;do 115 | local selected_board 116 | 117 | local boards=("<*>" "The most recently bumped threads" "${BOARDS[@]}") 118 | 119 | selected_board=$(board_selector_menu "${boards[@]}") 120 | 121 | if [ -z "$selected_board" ]; then 122 | go_back=true 123 | continue 124 | fi 125 | 126 | debug "$selected_board" 127 | board_controller "$selected_board" 128 | done 129 | } 130 | 131 | # Meta Board Controller 132 | # Controller for the special board <*> which is the most recent threads inside the 133 | # whole of the system 134 | meta_board_controller() { 135 | local go_back=false 136 | while ! $go_back;do 137 | shopt -s nullglob 138 | local thread_files=("./boards/"*"/"*) 139 | shopt -u nullglob 140 | 141 | if [ -n "${thread_files[*]}" ];then 142 | # Sort them by modification date 143 | debug "pre sorted thread files -> ${thread_files[*]}" 144 | IFS=$'\n' thread_files=($(ls -t "${thread_files[@]}")) 145 | debug "sorted thread files -> ${thread_files[*]}" 146 | 147 | thread_files=("${thread_files[@]:0:$MAX_ACTIVE_THREADS_PER_BOARD}") 148 | fi 149 | 150 | # Generate titles out of them 151 | mapfile -t thread_titles <<< "$(paths_to_thread_board_titles "${thread_files[@]}")" 152 | 153 | # Display them and wait for user input 154 | local selected_thread 155 | selected_thread="$(thread_selector_meta_menu "${thread_titles[@]}")" 156 | 157 | # Process input 158 | debug "Thread selection -> $selected_thread" 159 | case "$selected_thread" in 160 | "BACK") go_back=true ;; 161 | "NEW THREAD") thread_creation_controller "$board_dir" ;; 162 | *) 163 | local clean_thread_file="$(xargs <<< "$selected_thread")" 164 | thread_controller "$(dirname "$clean_thread_file")" "$(basename "$clean_thread_file")" 165 | ;; 166 | esac 167 | 168 | done 169 | } 170 | 171 | # Board Controller 172 | # Controls how the threads in a board should be displayed given a board name 173 | board_controller() { 174 | if [ $# != 1 ]; then 175 | error "Called board controller without board argument!!" 176 | return 177 | fi 178 | 179 | 180 | local board="$1" 181 | if [ "$board" = "<*>" ]; then 182 | meta_board_controller 183 | return 184 | fi 185 | 186 | local board_dir="./boards/$board" 187 | debug "Board controller for -> $board" 188 | 189 | if [ ! -d "$board_dir" ];then 190 | debug "Board directory doesnt exist. Creating it! [$board]" 191 | mkdir "$board_dir" 192 | fi 193 | 194 | local go_back=false 195 | while ! $go_back; do 196 | 197 | # Get thread files on board 198 | local thread_files 199 | shopt -s nullglob 200 | thread_files=("$board_dir/"*) 201 | shopt -u nullglob 202 | 203 | if [ -n "${thread_files[*]}" ];then 204 | # Sort them by modification date 205 | debug "pre sorted thread files -> ${thread_files[*]}" 206 | IFS=$'\n' thread_files=($(ls -t "${thread_files[@]}")) 207 | debug "sorted thread files -> ${thread_files[*]}" 208 | 209 | thread_files=("${thread_files[@]:0:$MAX_ACTIVE_THREADS_PER_BOARD}") 210 | fi 211 | 212 | # Generate titles out of them 213 | mapfile -t thread_titles <<< "$(paths_to_thread_titles "${thread_files[@]}")" 214 | 215 | # Display them and wait for user input 216 | local selected_thread 217 | selected_thread="$(thread_selector_menu "${thread_titles[@]}")" 218 | 219 | # Process input 220 | debug "Thread selection -> $selected_thread" 221 | case "$selected_thread" in 222 | "BACK") go_back=true ;; 223 | "NEW THREAD") thread_creation_controller "$board_dir" ;; 224 | *) thread_controller "$board_dir" "$(xargs <<< "$selected_thread")" ;; 225 | esac 226 | done 227 | } 228 | 229 | # Thread creation controller 230 | # Guides and handles the process of thread creation 231 | # Arguments: 232 | # - board directory 233 | thread_creation_controller() { 234 | info "thread_creation_controller" 235 | if [ $# -ne 1 ]; then 236 | error "Called thread creation controller without necessary arguments!!" 237 | error "-- Board dir is missing" 238 | return 239 | fi 240 | 241 | local board_dir="$1" 242 | if [ ! -d "$board_dir" ]; then 243 | error "Board at $board_dir does not exist" 244 | return 245 | fi 246 | 247 | local valid_title=false 248 | local thread_title="" 249 | local new_thread_file="" 250 | local error="" 251 | 252 | while ! $valid_title; do 253 | thread_title="$(thread_title_form "$error" | xargs)" 254 | 255 | if [ -z "$thread_title" ]; then 256 | info "Empty thread title -> going back!" 257 | return 258 | fi 259 | 260 | # Parse input 261 | thread_title="$(echo -e "$thread_title" | tr -d '\t')" 262 | thread_title="$(echo -e "$thread_title" | tr -d '\a')" 263 | thread_title="$(echo -e "$thread_title" | tr -d '\b')" 264 | thread_title="$(echo -e "$thread_title" | tr -d '\f')" 265 | thread_title="$(echo -e "$thread_title" | tr -d '\n')" 266 | thread_title="$(echo -e "$thread_title" | tr -d '\r')" 267 | thread_title="$(echo -e "$thread_title" | tr -d '\t')" 268 | thread_title="$(echo -e "$thread_title" | tr -d '\v')" 269 | thread_title="$(echo -e "$thread_title" | tr -d '*')" 270 | thread_title="$(echo -e "$thread_title" | tr -d '/')" 271 | debug "-> New thread title -> $thread_title" 272 | 273 | if [ ${#thread_title} -gt "$THREAD_TITLE_MAX_LENGTH" ]; then 274 | error "thread title too long!" 275 | error="The title is longer than $THREAD_TITLE_MAX_LENGTH chars" 276 | continue 277 | fi 278 | 279 | if [ ${#thread_title} -lt "$THREAD_TITLE_MIN_LENGTH" ]; then 280 | error "thread title too short" 281 | error="The title is shorter than $THREAD_TITLE_MIN_LENGTH chars" 282 | continue 283 | fi 284 | 285 | new_thread_file="$board_dir/$thread_title" 286 | if [ -f "$new_thread_file" ]; then 287 | error "thread with same name already exists in this board" 288 | error="The thread name is already taken" 289 | continue 290 | fi 291 | 292 | valid_title=true 293 | done 294 | 295 | error="" 296 | local thread_body="" 297 | local valid_body=false 298 | while ! $valid_body; do 299 | thread_body="$(thread_body_form "$thread_title" "$error" "$thread_body")" 300 | if [ -z "$thread_body" ]; then 301 | info "Empty thread body -> going back!" 302 | return 303 | fi 304 | 305 | if [ ${#thread_body} -lt "$THREAD_BODY_MIN_LENGTH" ]; then 306 | error "thread body too short!" 307 | error="Body is too short" 308 | continue 309 | fi 310 | 311 | if [ ${#thread_body} -gt "$THREAD_BODY_MAX_LENGTH" ]; then 312 | error "thread body too long!" 313 | error="Exceeds $THREAD_BODY_MAX_LENGTH characters (by $((${#thread_body} - THREAD_BODY_MAX_LENGTH))" 314 | continue 315 | fi 316 | 317 | thread_body="$(highlight_urls_in_thread_body "$thread_body")" 318 | thread_body="$(highlight_green_text "$thread_body")" 319 | 320 | valid_body=true 321 | done 322 | 323 | touch "$new_thread_file" 324 | echo -e "0\n$AUTHOR\n$(date '+%x %X')\n\n$thread_body\n" >> "$new_thread_file" 325 | 326 | if [ "$(ls | wc -l)" -gt "$MAX_ACTIVE_THREADS_PER_BOARD" ]; then 327 | local oldest_file 328 | oldest_file="$(find "$board_dir" -type f -printf '%T+%p\n' | sort | head -n1)" 329 | oldest_file="${oldest_file:31}" 330 | debug "oldest file -> $oldest_file" 331 | 332 | mkdir -p "$ARCHIVE/$board_dir" 333 | mv "$oldest_file" "$ARCHIVE/$board_dir/$oldest_file" 334 | fi 335 | } 336 | 337 | # Thread controller 338 | # Controlls the display of the thread as well as what to do with user action at this stage 339 | # Arguments: 340 | # - board directory 341 | # - thread filename 342 | thread_controller() { 343 | 344 | if [ $# -ne 2 ]; then 345 | error "Called thread controller without necessary arguments!!" 346 | error "-- Either board dir is missing or thread file is missing or both" 347 | return 348 | fi 349 | 350 | local thread_file="$1/$2" 351 | if [ ! -f "$thread_file" ]; then 352 | error "The thread file '$thread_file' does not exist!" 353 | return 354 | fi 355 | 356 | local go_back=false 357 | while ! $go_back; do 358 | 359 | local title 360 | local thread_body 361 | local replies 362 | 363 | title="[ $(thread_file_to_thread_title "$thread_file") ] :: [ $(thread_file_to_thread_author "$thread_file") ] :: [ $(thread_file_to_thread_creation_date "$thread_file") ] (j-k to scroll)" 364 | thread_body="$(thread_file_to_thread_body "$thread_file")" 365 | replies="$(thread_file_to_thread_replies "$thread_file")" 366 | 367 | local locked_thread=false 368 | if [ "$replies" -gt "$MAX_REPLIES_PER_THREAD" ];then locked_thread=true; fi 369 | 370 | local action 371 | action="$(display_thread "$title" "$thread_body" "$locked_thread")" 372 | 373 | debug "$action" 374 | case "$action" in 375 | "BACK") go_back=true ;; 376 | "NEW REPLY") new_reply_controller "$thread_file";; 377 | *) error "How the fuck did I even end here?!!" ;; 378 | esac 379 | 380 | done 381 | } 382 | 383 | # New Reply controller 384 | # Controls the display of forms to create a new reply and also processes the new reply 385 | # Arguments: 386 | # - The filepath to the thread file 387 | new_reply_controller() { 388 | if [ $# -ne 1 ]; then 389 | error "Called new reply controller with missing argument 'filepath'" 390 | return 391 | fi 392 | 393 | local filepath="$1" 394 | if [ ! -f "$filepath" ]; then 395 | error "Thread file: $filepath does not exist on the system! Unable to add reply to it!" 396 | return 397 | fi 398 | 399 | local thread_title 400 | local thread_replies 401 | thread_title="$(thread_file_to_thread_title "$filepath")" 402 | thread_replies="$(thread_file_to_thread_replies "$filepath")" 403 | 404 | local error="" 405 | local reply_body="" 406 | local valid_reply=false 407 | while ! "$valid_reply"; do 408 | reply_body="$(new_reply_form "$thread_title" "$error" "$reply_body" | cat -s)" 409 | if [ -z "$reply_body" ]; then 410 | info "Empty reply body -> going back!" 411 | return 412 | fi 413 | 414 | if [ ${#reply_body} -lt "$REPLY_MIN_LENGTH" ]; then 415 | error "reply body too short!" 416 | error="Reply is too short" 417 | continue 418 | fi 419 | 420 | if [ ${#reply_body} -gt "$REPLY_MAX_LENGTH" ]; then 421 | error "reply body too long!" 422 | error="Exceeds $REPLY_MAX_LENGTH charactes (by $((${#reply_body} - REPLY_MAX_LENGTH)))" 423 | continue 424 | fi 425 | 426 | reply_body="$(highlight_urls_in_thread_body "$reply_body")" 427 | reply_body="$(highlight_green_text "$reply_body")" 428 | reply_body="$(highlight_references_in_thread_boady "$reply_body")" 429 | 430 | valid_reply=true 431 | done 432 | 433 | # Worth to queue writings to file for race conditions? Meeh 434 | # Update replies 435 | local next_reply_number=$((thread_replies+1)) 436 | sed -i "1s/.*/$next_reply_number/" "$filepath" 437 | 438 | # Add reply 439 | reply_body="$(echo "$reply_body" | sed -e 's/^/ /')" 440 | echo -e "\Z5[[$AUTHOR :: $(date '+%x %X') :: #$next_reply_number]]\Zn\n$reply_body\Zn\n" >> "$filepath" 441 | } 442 | -------------------------------------------------------------------------------- /src/logger.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export LOG_DEBUG=0 4 | export LOG_INFO=1 5 | export LOG_ERROR=2 6 | 7 | function log() { 8 | echo "$(date) :: $*" >> "$LOG_FILE" 9 | } 10 | 11 | function info() { 12 | if (( "$LOG_LEVEL" <= "LOG_INFO" )); then 13 | log "**INFO** :: $*" 14 | fi 15 | } 16 | 17 | function debug() { 18 | if (( "$LOG_LEVEL" <= "$LOG_DEBUG" )); then 19 | log "[[DEBUG]] :: $*" 20 | fi 21 | } 22 | 23 | function error() { 24 | if (( "$LOG_LEVEL" <= "$LOG_ERROR" )); then 25 | log "!!ERROR!! :: $*" 26 | fi 27 | } 28 | -------------------------------------------------------------------------------- /src/models.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./src/logger.sh 4 | 5 | # Transform the path to files containing the threads to the representation needed to display them on screen. 6 | paths_to_thread_titles() { 7 | local paths=("$@") 8 | 9 | for file in "${paths[@]}"; do 10 | if [ ! -f "$file" ]; then 11 | error "Unable to find file -> $file" 12 | continue 13 | fi 14 | 15 | local t_title="$(basename "$file")$(printf ' %.0s' {0..40})" 16 | local t_last_reply="$(date -r "$file" '+%x %X')" 17 | local t_replies="$(head -n1 "$file")" 18 | local t_author="$(head -n2 "$file" | tail -n1) " 19 | 20 | t_title=${t_title:0:40} 21 | t_author=${t_author:0:10} 22 | 23 | echo "$t_title" 24 | echo "$t_author :: $t_replies :: $t_last_reply" 25 | done 26 | } 27 | 28 | paths_to_thread_board_titles() { 29 | local paths=("$@") 30 | 31 | for file in "${paths[@]}"; do 32 | if [ ! -f "$file" ]; then 33 | error "Unable to find file -> $file" 34 | continue 35 | fi 36 | 37 | local t_title="$file$(printf ' %.0s' {0..60})" 38 | local t_last_reply="$(date -r "$file" '+%x %X')" 39 | local t_replies="$(printf "%0.3d" "$(thread_file_to_thread_replies "$file")")" 40 | local t_author="$(thread_file_to_thread_author "$file") " 41 | 42 | t_title=${t_title:0:60} 43 | t_author=${t_author:0:10} 44 | 45 | echo "$t_title" 46 | echo "$t_author :: $t_replies :: $t_last_reply" 47 | done 48 | } 49 | 50 | # Extracts the author name of a given thread file 51 | # Arguments: 52 | # - thread file path 53 | thread_file_to_thread_author() { 54 | if [ $# -ne 1 ] || [ ! -f "$1" ]; then 55 | error "File does not exist! -> $1" 56 | return 57 | fi 58 | head -n2 "$1" | tail -n1 59 | } 60 | 61 | # Extracts the number of replies given a path to a thread file 62 | # Arguments: 63 | # - Thread File Path 64 | thread_file_to_thread_replies() { 65 | if [ $# -ne 1 ] || [ ! -f "$1" ]; then 66 | error "File does not exist! -> $1" 67 | return 68 | fi 69 | head -n1 "$1" 70 | } 71 | 72 | # Extracts the thread title given a path to a thread file 73 | # Arguments: 74 | # - thread file path 75 | thread_file_to_thread_title() { 76 | if [ $# -ne 1 ] || [ ! -f "$1" ]; then 77 | error "File does not exist! -> $1" 78 | return 79 | fi 80 | 81 | basename "$1" 82 | } 83 | 84 | # Extracts the thread contents given a path to the thread file 85 | # Arguments: 86 | # - Thread file path 87 | thread_file_to_thread_body() { 88 | if [ $# -ne 1 ] || [ ! -f "$1" ]; then 89 | error "File does not exist! -> $1" 90 | return 91 | fi 92 | 93 | tail -n+4 "$1" 94 | } 95 | 96 | # Extracts the thread creation date given a path to a thread file 97 | # Arguments: 98 | # - thread file path 99 | thread_file_to_thread_creation_date() { 100 | if [ $# -ne 1 ] || [ ! -f "$1" ]; then 101 | error "File does not exist! -> $1" 102 | return 103 | fi 104 | 105 | head -n3 "$1" | tail -n1 106 | } 107 | 108 | # Highlight URLs given a text 109 | # Arguments: 110 | # - text to highlight 111 | highlight_urls_in_thread_body() { 112 | if [ $# -ne 1 ]; then 113 | error "Missing body to work with" 114 | return 115 | fi 116 | 117 | local url_regex="https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)" 118 | local higlight_pattern="\\\\Zb\\\\Zu\\0\\\\Zn" 119 | 120 | echo -E "$1" | sed -E "s/$url_regex/$higlight_pattern/g" 121 | } 122 | 123 | # Highlight references given a text 124 | # Arguments: 125 | # - text to highlight 126 | highlight_references_in_thread_boady() { 127 | if [ $# -ne 1 ]; then 128 | error "Missing body to work with" 129 | return 130 | fi 131 | 132 | local ref_regex="#[0-9]+" 133 | local higlight_pattern="\\\\Zb\\\\Z5\\0\\\\Zn" 134 | 135 | echo -E "$1" | sed -E "s/$ref_regex/$higlight_pattern/g" 136 | } 137 | 138 | # Highlight green text given a text 139 | # Arguments: 140 | # - text to highlight 141 | highlight_green_text() { 142 | if [ $# -ne 1 ]; then 143 | error "Missing body to work with" 144 | return 145 | fi 146 | 147 | local green_text_regex=">.+" 148 | local higlight_pattern="\\\\Zb\\\\Z2\\0\\\\Zn" 149 | 150 | echo -E "$1" | sed -E "s/$green_text_regex/$higlight_pattern/g" 151 | } 152 | -------------------------------------------------------------------------------- /src/visuals.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Displays the welcome banner. The contents of it are declared inside the file ´assets/welcome_banner´ 4 | welcome_banner() { 5 | local title='[[Welcome]]' 6 | dialog --backtitle "$BANNER"\ 7 | --title "$title"\ 8 | --msgbox "\n$(cat ./assets/welcome_banner)\n"\ 9 | "$WELCOME_HEIGHT" "$WELCOME_WIDTH" 10 | } 11 | 12 | # Displays the main menu and returns the option picked by the user. 13 | # The return values can be: "", "", "" and "". 14 | # The empty string means no option was picked 15 | main_menu() { 16 | local title="[[Where shall we go?]]" 17 | dialog --backtitle "$BANNER"\ 18 | --title "$title"\ 19 | --cancel-label "EXIT"\ 20 | --stdout\ 21 | --menu "UP & DOWN to move selection, TAB to select action"\ 22 | "$MAIN_MENU_HEIGHT"\ 23 | "$MAIN_MENU_WIDTH"\ 24 | "$MAIN_MENU_MENU_HEIGHT"\ 25 | "" "Explore and participate in forums"\ 26 | "" "Chat with other users"\ 27 | "" "Change site settings" 28 | } 29 | 30 | # Displays the interface for the chat. 31 | # One big bacground tail box and a small input box under it. 32 | # Returns whatever is introduced into the input box 33 | chat_interface() { 34 | local title="[[Chat Feed]]" 35 | dialog --backtitle "$BANNER"\ 36 | --title "$title"\ 37 | --begin 2 2 --colors\ 38 | --keep-window\ 39 | --tailboxbg "$CHAT_FILE"\ 40 | "$CHAT_FEED_HEIGHT"\ 41 | "$CHAT_FEED_WIDTH"\ 42 | --and-widget\ 43 | --stdout\ 44 | --begin 32 2\ 45 | --ok-label "SEND"\ 46 | --cancel-label "BACK"\ 47 | --max-input "$CHAT_MSG_MAX_LENGTH"\ 48 | --inputbox "*Use up arrow to chage focus\n*Empty box is treated as BACK"\ 49 | "$CHAT_INPUT_HEIGHT"\ 50 | "$CHAT_INPUT_WIDTH" 51 | } 52 | 53 | # Displays a menu to select a board 54 | # Arguments: 55 | # - The list of boards (shouldnt be empty!) 56 | # Returns: 57 | # - The selected Board or empty when op cancellation 58 | board_selector_menu() { 59 | local title="[[Board Selection]]" 60 | local boards=("$@") 61 | local msg="UP & DOWN to move selection, TAB to select action" 62 | 63 | dialog --backtitle "$BANNER"\ 64 | --title "$title"\ 65 | --stdout\ 66 | --cancel-label "BACK"\ 67 | --ok-label "SELECT"\ 68 | --menu "$msg"\ 69 | "$BOARD_SELECTOR_HEIGHT"\ 70 | "$BOARD_SELECTOR_WIDTH"\ 71 | "$BOARD_SELECTOR_MENU_HEIGHT"\ 72 | "${boards[@]}" 73 | } 74 | 75 | # Displays thread selector menu for the meta board. 76 | # Returns either the selected thread name, BACK on back selected and NEW THREAD on new thread selected. 77 | # Arguments: 78 | # - threads Required 79 | thread_selector_meta_menu() { 80 | local title="[[Thread Selection]]" 81 | local msg="UP & DOWN to move selection, TAB to select action" 82 | local ok_label="SELECT" 83 | local threads=("$@") 84 | 85 | if [ -z "${threads[*]}" ]; then 86 | threads=("EMPTY" "BOARD") 87 | ok_label="BACK" 88 | fi 89 | 90 | local selected_thread 91 | selected_thread="$(dialog --backtitle "$BANNER"\ 92 | --colors\ 93 | --stdout\ 94 | --title "$title"\ 95 | --ok-label "$ok_label"\ 96 | --cancel-label "BACK"\ 97 | --menu "$msg"\ 98 | "$THREAD_SELECTOR_HEIGHT"\ 99 | "$THREAD_SELECTOR_WIDTH"\ 100 | "$THREAD_SELECTOR_MENU_HEIGHT"\ 101 | "${threads[@]}")" 102 | 103 | ret=$? 104 | debug "return code -> $ret" 105 | case "$ret" in 106 | 0) 107 | if [ "EMPTY" = "$selected_thread" ]; then 108 | echo "BACK" 109 | else 110 | echo "$selected_thread" 111 | fi 112 | ;; 113 | *) echo "BACK" ;; 114 | esac 115 | } 116 | # Displays thread selector menu. 117 | # Returns either the selected thread name, BACK on back selected and NEW THREAD on new thread selected. 118 | # Arguments: 119 | # - threads Required 120 | thread_selector_menu() { 121 | local title="[[Thread Selection]]" 122 | local msg="UP & DOWN to move selection, TAB to select action" 123 | local ok_label="SELECT" 124 | local threads=("$@") 125 | 126 | if [ -z "${threads[*]}" ]; then 127 | threads=("EMPTY" "BOARD") 128 | ok_label="BACK" 129 | fi 130 | 131 | local selected_thread 132 | selected_thread="$(dialog --backtitle "$BANNER"\ 133 | --colors\ 134 | --stdout\ 135 | --title "$title"\ 136 | --ok-label "$ok_label"\ 137 | --cancel-label "BACK"\ 138 | --extra-button --extra-label "NEW THREAD"\ 139 | --menu "$msg"\ 140 | "$THREAD_SELECTOR_HEIGHT"\ 141 | "$THREAD_SELECTOR_WIDTH"\ 142 | "$THREAD_SELECTOR_MENU_HEIGHT"\ 143 | "${threads[@]}")" 144 | 145 | ret=$? 146 | exec 3>&- 147 | case "$ret" in 148 | 0) 149 | if [ "EMPTY" = "$selected_thread" ]; then 150 | echo "BACK" 151 | else 152 | echo "$selected_thread" 153 | fi 154 | ;; 155 | 1) echo "BACK" ;; 156 | 3) echo "NEW THREAD" ;; 157 | *) echo "BACK" ;; 158 | esac 159 | } 160 | 161 | # Display Thread 162 | # Formats a thread into a view for the user 163 | # Arguments: 164 | # - Thread title 165 | # - Thread body 166 | # - Wether it is blocked or not 167 | # Returns: 168 | # - BACK 169 | # - NEW REPLY 170 | display_thread() { 171 | if [ $# -ne 3 ]; then 172 | error "Missing arguments to be able to display thread!" 173 | return 174 | fi 175 | 176 | local title="$1" 177 | local body="$2" 178 | local is_blocked="$3" 179 | local new_reply_label 180 | 181 | if $is_blocked; then 182 | new_reply_label="BACK" 183 | else 184 | new_reply_label="NEW REPLY" 185 | fi 186 | 187 | local new_reply=true 188 | dialog --backtitle "$BANNER"\ 189 | --title "$title"\ 190 | --no-collapse\ 191 | --yes-label "$new_reply_label"\ 192 | --no-label "BACK"\ 193 | --colors\ 194 | --stdout\ 195 | --yesno "$body" "$THREAD_FEED_HEIGHT" "$THREAD_FEED_WIDTH" || new_reply=false 196 | 197 | if $new_reply;then 198 | echo "NEW REPLY" 199 | else 200 | echo "BACK" 201 | fi 202 | } 203 | 204 | # Thread title Form 205 | # Prompts the user for a title for a thread 206 | # Arguments: 207 | # - Error to display in case of any 208 | # Returns: 209 | # - Unvalidated title string or empty string on cancelation 210 | thread_title_form() { 211 | local error="" 212 | local title="[ New Thread ] :: [ Set title ]" 213 | 214 | if [ -n "$1" ]; then error="$1"; fi 215 | 216 | dialog --backtitle "$BANNER"\ 217 | --title "$title"\ 218 | --stdout\ 219 | --colors\ 220 | --ok-label "NEXT"\ 221 | --max-input "$THREAD_TITLE_MAX_LENGTH"\ 222 | --inputbox "Between $THREAD_TITLE_MIN_LENGTH and $THREAD_TITLE_MAX_LENGTH characters. Empty string is treated as CANCEL.\n\\Z1\\Zb$error\\Zn"\ 223 | "$THREAD_TITLE_FORM_HEIGHT"\ 224 | "$THREAD_TITLE_FORM_WIDTH" 225 | } 226 | 227 | # User name form 228 | # Prompts the user for a new name to use 229 | # Arguments: 230 | # - Error to display in case of any 231 | # Returns: 232 | # - Unvalidated title string or empty string on cancelation 233 | user_name_form() { 234 | local error="" 235 | local title="[[New Name]]" 236 | 237 | if [ -n "$1" ]; then error="$1"; fi 238 | 239 | dialog --backtitle "$BANNER"\ 240 | --title "$title"\ 241 | --stdout\ 242 | --colors\ 243 | --ok-label "NEXT"\ 244 | --max-input "$USER_NAME_MAX_LENGTH"\ 245 | --inputbox "Between $USER_NAME_MIN_LENGTH and $USER_NAME_MAX_LENGTH characters. Blank treated as Cancel.\n\\Z1\\Zb$error\\Zn"\ 246 | "$USER_NAME_FORM_HEIGHT"\ 247 | "$USER_NAME_FORM_WIDTH" 248 | } 249 | 250 | # Thread body form 251 | # Promts the user for the body of the thread 252 | # Arguments: 253 | # - thread title (Required) 254 | # - error in case of any 255 | # - default contents 256 | # Returns: 257 | # - A string representing the body of the text or empty in case of cancelation 258 | thread_body_form() { 259 | debug "thread_body_form" 260 | if [ -z "$1" ]; then 261 | error "Missing thread title!" 262 | return 263 | fi 264 | 265 | local error="" 266 | local contents="" 267 | local title="[ New Thread ] :: [ $1 ] :: [ Set Body ]" 268 | 269 | if [ -n "$2" ]; then error="$2"; fi 270 | if [ -n "$3" ]; then contents="$3"; fi 271 | 272 | if [ -n "$error" ]; then 273 | title="!! $error !! - $title" 274 | fi 275 | 276 | local target_file="/tmp/$(date +%s%N)" 277 | touch "$target_file" 278 | echo -e "$contents" > "$target_file" 279 | 280 | local result 281 | result="$(dialog --backtitle "$BANNER"\ 282 | --title "$title"\ 283 | --stdout\ 284 | --colors\ 285 | --ok-label "CONFIRM"\ 286 | --editbox "$target_file"\ 287 | "$THREAD_BODY_FORM_HEIGHT" "$THREAD_BODY_FORM_WIDTH")" 288 | 289 | rm "$target_file" 290 | echo -e "$result" 291 | } 292 | 293 | # Displays a text form and the thread. 294 | # Arguments: 295 | # - thread_title (required) 296 | # - error (optional) 297 | # - contents (optional) 298 | # Returns: 299 | # - the inputed contents (Empty is considered operation cancellation) 300 | new_reply_form() { 301 | 302 | debug "new_reply_form" 303 | if [ -z "$1" ]; then 304 | error "Missing thread title!" 305 | return 306 | fi 307 | 308 | local error="" 309 | local contents="" 310 | local title="[ Modify Thread ] :: [ $1 ] :: [ Add Reply ]" 311 | 312 | if [ -n "$2" ]; then error="$2"; fi 313 | if [ -n "$3" ]; then contents="$3"; fi 314 | 315 | if [ -n "$error" ]; then 316 | title="!! $error !! - $title" 317 | fi 318 | 319 | local target_file="/tmp/$(date +%s%N)" 320 | touch "$target_file" 321 | echo -e "$contents" > "$target_file" 322 | 323 | local result 324 | result="$(dialog --backtitle "$BANNER"\ 325 | --title "$title"\ 326 | --stdout\ 327 | --colors\ 328 | --ok-label "CONFIRM"\ 329 | --editbox "$target_file"\ 330 | "$REPLY_FORM_HEIGHT" "$REPLY_FORM_WIDTH")" 331 | 332 | rm "$target_file" 333 | echo -e "$result" 334 | } 335 | 336 | --------------------------------------------------------------------------------