├── .gitignore ├── ui.sh ├── README.md └── chat.py /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !chat.py 4 | !README.md 5 | !ui.sh 6 | !.gitignore -------------------------------------------------------------------------------- /ui.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | tmux new-session -d "./chat.py -r $@" 3 | tmux split-window -v "./chat.py $@" 4 | tmux attach \; resize-pane -y 2 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cli-chat 2 | A simple command line chat program for linux and windows written in python 3 | It runs on Window and Linux on both Python 2 and 3 4 | If you're on a Linux system running Python 3 you can use [rtx](https://github.com/ZakSN/rtx) instead; a multithreaded client with more configurability made by @ZakSN 5 | 6 | The messages are stored on: http://waksmemes.x10host.com/mess/ 7 | 8 | ## Usage: 9 | ### Entering chat rooms: 10 | To start chatting in the main chat room: 11 | `python chat.py` 12 | 13 | To start chatting in a chat room called "different_chat_room": 14 | `python chat.py different_chat_room` 15 | 16 | ### Sending messages 17 | After all a chat room's messages are printed a '> ' symbol is printed. This final line of text is called the _input line_. 18 | To send a message simply type your message on the input line and press enter. 19 | If the user presses enter without typing a message, no message is sent. 20 | Likewise, when a user issues a command, no message is sent. 21 | 22 | ### Reading messages 23 | New messages are fetched every time the user presses enter on the input line, regardless of whether or not they sent a message 24 | To automatically reload the chat (fetch messages without having to press enter) you can start the script in _auto-reloading read-only mode_. 25 | 26 | To start in auto-reloading read-only mode: 27 | `python chat.py -r` 28 | 29 | To start in auto-reloading read-only mode in a chat room called "different_chat_room": 30 | `python chat.py -r different_chat_room` 31 | 32 | ### Commands 33 | Commands begin with a backslash (\) and are issued on the input line in a similar fashion to shell commands. Commands can take any number of arguments. 34 | #### The set command 35 | set is used to configure your user settings. User settings are saved in a local configuration file that is automatically generated when the set command is used. These settings are sent along with every message you send so that other clients can use them. 36 | 37 | Usage: 38 | `\set setting value` 39 | 40 | Types of settings: 41 | 42 | |Setting name | Purpose | 43 | | ----------- |:------------------------------------------------------------------------------------------------------------:| 44 | | name | Sets your name so you're no longer identified by your IP | 45 | | color | Sets the color of your name. The only possible colors so far are red, green, purple, blue, yellow and cyan | 46 | 47 | #### The key command 48 | key is used to encrypt your messages in the current room with a specified key. Any other users in the same room with the same key can decrypt the messages that you send with that key. 49 | 50 | Usage: 51 | `\key secret_key` 52 | 53 | Once you set your key you can switch to a different key by using the key command agian, however you can only save one key per room at any one time. Using the key command without specifying a key will remove your key for that room, causing all your messages to be unencrypted until you set a new key. 54 | 55 | Encrypted messages contain a red colon after the name of the sender instead of the usual white (or whatever your default terminal color is) colon. This is so that users can easily distinguish between encrypted and unencrypted messages. 56 | 57 | #### Other commands 58 | 59 | |Command name | Args | Explanation | Example | 60 | | ----------- |:-------------------:|:-------------------------------------------------------:|:------------------: 61 | | room | (1) roomname | Switches the room to _roomname_ | \room other_room | 62 | | read | (0) No arguments | Swithces into auto-reloading read-only mode | \read | 63 | | flush | (1) m (optional) | Spams the room with _m_ messages, Defaults to _m_ = 300 | \flush 100 | 64 | | nokey | (1) message | Sends _message_ with no encryption | \nokey hello world| 65 | -------------------------------------------------------------------------------- /chat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import urllib 4 | import sys 5 | import json 6 | import os 7 | from base64 import b64encode, b64decode 8 | import json 9 | import time 10 | import datetime 11 | 12 | import hashlib 13 | try: 14 | from Crypto import Random 15 | from Crypto.Cipher import AES 16 | except: 17 | AES = None 18 | 19 | #AES crypto================================== 20 | 21 | class AESCipher(object): 22 | # basic class to provide AES cryptography, shamelessly ripped from stack exchange: 23 | # https://stackoverflow.com/questions/12524994/encrypt-decrypt-using-pycrypto-aes-256 24 | 25 | def __init__(self, key): 26 | self.bs = 32 27 | self.key = hashlib.sha256(key.encode()).digest() 28 | 29 | def encrypt(self, raw): 30 | raw = self._pad(raw) 31 | iv = Random.new().read(AES.block_size) 32 | cipher = AES.new(self.key, AES.MODE_CBC, iv) 33 | return b64encode(iv + cipher.encrypt(raw)) 34 | 35 | def decrypt(self, enc): 36 | enc = b64decode(enc) 37 | iv = enc[:AES.block_size] 38 | cipher = AES.new(self.key, AES.MODE_CBC, iv) 39 | return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8', errors = 'ignore') 40 | 41 | def _pad(self, s): 42 | return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs) 43 | 44 | @staticmethod 45 | def _unpad(s): 46 | return s[:-ord(s[len(s)-1:])] 47 | 48 | def get_key(): 49 | keys = SETTINGS.private('keys') 50 | if keys is not None: 51 | return keys.get(ROOM, None) 52 | return None 53 | 54 | 55 | #Colors====================================== 56 | COLORS = { 57 | 'red': "\x1b[1;31m", 58 | 'green': "\x1b[1;32m", 59 | 'yellow': "\x1b[1;33m", 60 | 'blue': "\x1b[1;34m", 61 | 'purple': "\x1b[1;35m", 62 | 'cyan': "\x1b[1;36m" 63 | } 64 | STOP_COLOR = "\x1b[0m" 65 | DEFAULT_NAME_COLOR = "\x1b[1;37m" 66 | TEXT_COLOR = "\x1b[3;38;2;230;230;230m" 67 | TIME_COLOR = "\x1b[38;2;160;160;160m" 68 | def color(string, color_string): 69 | if color_string != '' and color_string[0] != '\x1b': 70 | color_string = COLORS.get(color_string, STOP_COLOR) 71 | return color_string + string + STOP_COLOR 72 | 73 | #User Settings=============================== 74 | class Settings: 75 | 76 | def __init__(self): 77 | self._config_file = os.path.join(os.path.expanduser('~'), '.cli_chat_config') 78 | try: 79 | with open(self._config_file) as f: 80 | all = json.loads(f.read()) 81 | self._public, self._private = all['public'], all['private'] 82 | except IOError: 83 | print("No user configs found") 84 | self._public, self._private = {},{} 85 | 86 | def all(self, setting_type): 87 | return getattr(self, '_' + setting_type) 88 | 89 | def public(self, setting): 90 | return self._public.get(setting, None) 91 | 92 | def private(self, setting): 93 | return self._private.get(setting, None) 94 | 95 | def set(self, setting, value, visibility = 'public'): 96 | settings = self._public if visibility == 'public' else self._private 97 | settings[setting] = value 98 | with open(self._config_file, 'w') as f: 99 | f.write(self.to_json()) 100 | 101 | def to_json(self): 102 | return json.dumps({'public':self._public, 'private': self._private}) 103 | 104 | SETTINGS = Settings() 105 | 106 | #Commands==================================== 107 | def switch_room(new_room): 108 | global ROOM 109 | ROOM = new_room 110 | 111 | def enter_read_mode(_): 112 | global MODE 113 | MODE = 'read' 114 | 115 | def flush_pipes(times): 116 | if times == '': 117 | flush_depth = 300 118 | else: 119 | flush_depth = int(times) 120 | for a in range(flush_depth): 121 | send_msg("flushing the pipes: " + str(flush_depth - a)) 122 | send_msg("the pipes are clean!") 123 | 124 | def set_public_setting(param_str): 125 | setting, val = param_str.split(' ', 1) 126 | SETTINGS.set(setting, val, 'public') 127 | 128 | def set_key(new_key): 129 | keys = SETTINGS.private('keys') 130 | if keys is None: 131 | keys = {} 132 | if new_key == '': 133 | new_key = None 134 | keys[ROOM] = new_key 135 | SETTINGS.set('keys', keys, 'private') 136 | 137 | def send_unencrypted(msg): 138 | send_msg(msg) 139 | 140 | CMDS = { 141 | '\\set': set_public_setting, 142 | '\\room': switch_room, 143 | '\\read': enter_read_mode, 144 | '\\flush': flush_pipes, 145 | '\\key': set_key, 146 | '\\nokey': send_unencrypted 147 | } 148 | 149 | #Parsing============================== 150 | def parse_msg(msg): 151 | if ' ' in msg: 152 | cmd, arg = msg.split(' ', 1) 153 | else: 154 | cmd, arg = (msg, '') 155 | if cmd in CMDS: 156 | CMDS[cmd](arg) 157 | else: 158 | key = get_key() 159 | if key is not None and AES is not None: 160 | cipher = AESCipher(key) 161 | msg = cipher.encrypt(msg) 162 | msg = msg.decode('UTF-8') 163 | send_msg(msg, True) 164 | else: 165 | send_msg(msg) 166 | 167 | def errorless_print(string): 168 | try: 169 | print(string) 170 | except UnicodeEncodeError: 171 | try: #try to print 0th plane chars 172 | print(''.join(s for s in string if ord(s) < 65536)) 173 | except UnicodeEncodeError: #if that's broken just print ascii chars 174 | print(string.encode('ascii', errors = 'ignore').decode('ascii')) 175 | 176 | def parse_shell_args(): 177 | mode = 'chat' 178 | room = "main" 179 | for arg in sys.argv[1:]: 180 | if arg[0] == '-': 181 | if arg == '-r': 182 | mode = 'read' 183 | else: 184 | room = arg 185 | return room, mode 186 | 187 | #Platform independance======================= 188 | def clear_screen(): 189 | os.system('cls') if sys.platform[:3] == 'win' else os.system('clear') 190 | 191 | if sys.version_info[0] < 3: 192 | bytes = lambda s, encoding: s.encode('utf-8') #so that you can't send malformed unicode 193 | input = raw_input 194 | else: 195 | from urllib import request as urllib 196 | 197 | #Network===================================== 198 | def send_msg(msg, encrypted = False): 199 | msg = b64encode(bytes(msg, encoding = 'UTF-8')) 200 | settings = b64encode(bytes(json.dumps(SETTINGS.all('public')), encoding = 'UTF-8')) 201 | payload = { 202 | 'msg': msg.decode('utf-8'), 203 | 'settings': settings.decode('utf-8'), 204 | 'encrypted': encrypted 205 | } 206 | urllib.urlopen("http://waksmemes.x10host.com/mess/?" + ROOM + '!post', 207 | bytes(json.dumps(payload), encoding = 'UTF-8')) 208 | 209 | def fetch_msgs(min_id = 1, max_msgs = 100): 210 | payload = {'MAX_MSGS': max_msgs, 'id': {'min': min_id}} 211 | res = urllib.urlopen("http://waksmemes.x10host.com/mess/?" + ROOM, 212 | bytes(json.dumps(payload), encoding = 'UTF-8')) 213 | raw = res.read().decode('utf-8') 214 | data = json.loads(raw) 215 | data.reverse() 216 | return data 217 | 218 | #Main======================================== 219 | def print_msgs(data, clear): 220 | if clear: 221 | clear_screen() 222 | for d in data: 223 | last_id = d['id'] 224 | timestamp = d['time'] 225 | timestr = '[' + datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M") + '] ' 226 | settings = {} 227 | if 'settings' in d: 228 | raw_settings = b64decode(d['settings']).decode('utf-8') 229 | settings = json.loads(raw_settings) 230 | name = settings.get('name', d['ip']) 231 | name_color = settings.get('color', DEFAULT_NAME_COLOR) 232 | encrypted = d.get('encrypted', False) 233 | msg = b64decode(d.get('msg', '')).decode('utf-8') 234 | colon_color = STOP_COLOR 235 | if encrypted: 236 | colon_color = 'red' 237 | key = get_key() 238 | if key is not None and AES is not None: 239 | cipher = AESCipher(key) 240 | msg = cipher.decrypt(str(msg)) 241 | errorless_print(color(timestr, TIME_COLOR) + color(name, name_color) 242 | + color(': ', colon_color) + color(msg, TEXT_COLOR)) 243 | 244 | 245 | if __name__ == "__main__": 246 | if AES is None: 247 | print(color("WARNING: ", "red") + "pycrypto is not installed, you will only be able to send/recieve plaintext messages") 248 | print("Press enter to continue") 249 | input() 250 | ROOM, MODE = parse_shell_args() 251 | LAST_ID = 0 252 | clear_screen() 253 | while 1: 254 | if MODE == 'chat': 255 | print_msgs(fetch_msgs(), True) 256 | new_msg = input('> ') 257 | if new_msg != '': 258 | parse_msg(new_msg) 259 | else: 260 | msg_data = fetch_msgs(LAST_ID + 1) 261 | if len(msg_data) > 0: 262 | print_msgs(msg_data, False) 263 | LAST_ID = msg_data[-1]['id'] 264 | time.sleep(0.5) 265 | else: 266 | ROOM, MODE, LAST_ID = 'main', 'chat', 0 267 | 268 | #IF EVERYTHING BREAKS: HEAD ON OVER TO THE disaster ROOM 269 | --------------------------------------------------------------------------------