├── README.md ├── _drawing.py ├── _drawing_example.py ├── client.py ├── client_drawing.py ├── diagram.jpg ├── diagrams ├── output │ ├── en.diagram-start.svg.png │ ├── en.diagram2_1.svg.png │ ├── en.diagram2_2.svg.png │ ├── en.diagram2_3.svg.png │ ├── ru.diagram-start.svg.png │ ├── ru.diagram2_1.svg.png │ ├── ru.diagram2_2.svg.png │ └── ru.diagram2_3.svg.png ├── readme.md ├── run.sh └── text │ ├── en │ ├── diagram-start.sequence │ ├── diagram2_1.sequence │ ├── diagram2_2.sequence │ └── diagram2_3.sequence │ └── ru │ ├── diagram-start.sequence │ ├── diagram2_1.sequence │ ├── diagram2_2.sequence │ └── diagram2_3.sequence ├── general_variables.py ├── out2.mkv └── server.py /README.md: -------------------------------------------------------------------------------- 1 | # ★★★ async-desktop-chat ★★★ 2 | An advanced topic - async GUI with PySimpleGUI. 3 | 4 | If you want "**websockets**" + "***async***" + "*GUI*", then **you** are in the right place. 5 | 6 | ![gif](https://user-images.githubusercontent.com/46163555/81684482-ffd36900-9424-11ea-9ef1-a6015de75e28.gif) 7 | 8 | ## Usage 9 | 10 | ```bash 11 | # _ _ 12 | # (_) | | 13 | # ___ _ _ __ ___ _ __ | | ___ 14 | # / __| | '_ ` _ \| '_ \| |/ _ \ 15 | # \__ \ | | | | | | |_) | | __/ 16 | # |___/_|_| |_| |_| .__/|_|\___| 17 | # | | 18 | # |_| 19 | 20 | # Starts your client app 21 | python3 client.py 22 | 23 | # Starts your server app 24 | python3 server.py 25 | ``` 26 | #### Architecture: 27 | 28 | ![](https://github.com/nngogol/async-desktop-chat/blob/master/diagram.jpg) 29 | 30 | 31 | --- 32 | 33 | ## Couple of diagrams for websocket understading 34 | 35 | Languages\Языки: Русские + English 36 | 37 | ### English 38 | 39 | ##### Main connection diagram 40 | ![en.diagram-start](https://github.com/nngogol/async-desktop-chat/blob/master/diagrams/output/en.diagram-start.svg.png) 41 | ##### How to change your name 42 | ![en.diagram2_1](https://github.com/nngogol/async-desktop-chat/blob/master/diagrams/output/en.diagram2_1.svg.png) 43 | ##### How to send message to public board (public message) 44 | ![en.diagram2_2](https://github.com/nngogol/async-desktop-chat/blob/master/diagrams/output/en.diagram2_2.svg.png) 45 | ##### How to send message to a user (private message) 46 | ![en.diagram2_3](https://github.com/nngogol/async-desktop-chat/blob/master/diagrams/output/en.diagram2_3.svg.png) 47 | 48 | --- 49 | 50 | ### Русские 51 | ##### Основная 52 | ![ru.diagram-start](https://github.com/nngogol/async-desktop-chat/blob/master/diagrams/output/ru.diagram-start.svg.png) 53 | ##### Как сменить имя 54 | ![ru.diagram2_1](https://github.com/nngogol/async-desktop-chat/blob/master/diagrams/output/ru.diagram2_1.svg.png) 55 | ##### Как послать сообщение всем 56 | ![ru.diagram2_2](https://github.com/nngogol/async-desktop-chat/blob/master/diagrams/output/ru.diagram2_2.svg.png) 57 | ##### Как послать сообщение одногому юзеру (приватное сообщение) 58 | ![ru.diagram2_3](https://github.com/nngogol/async-desktop-chat/blob/master/diagrams/output/ru.diagram2_3.svg.png) 59 | -------------------------------------------------------------------------------- /_drawing.py: -------------------------------------------------------------------------------- 1 | import PySimpleGUI as sg 2 | bg = 'grey' 3 | pen_color = 'red' 4 | pen_size = 5 5 | 6 | graph = sg.Graph(canvas_size=(400, 400), graph_bottom_left=(0,0), graph_top_right=(400, 400), background_color=bg, key='graph', change_submits=True, drag_submits=True) 7 | window = sg.Window('Test', [ 8 | [graph], 9 | [ 10 | sg.B('', button_color=('red', 'red'), key='color_red', size=(3,1)) 11 | ,sg.B('', button_color=('green', 'green'), key='color_green', size=(3,1)) 12 | ,sg.B('', button_color=('blue', 'blue'), key='color_blue', size=(3,1)) 13 | ,sg.B('', button_color=('orange', 'orange'), key='color_orange', size=(3,1)) 14 | ,sg.B('', button_color=('brown', 'brown'), key='color_brown', size=(3,1)) 15 | ,sg.B('eraser', button_color=('white', bg), key='color_grey', size=(5,1)) 16 | ,sg.B('X', key='clear', size=(3,1)) 17 | ] 18 | ], finalize=True, return_keyboard_events=True) 19 | 20 | def check_pen_size(): 21 | if pen_size < 0: pen_size = 1 22 | if pen_size > 30: pen_size = 30 23 | 24 | while True: 25 | event, values = window(timeout=50) 26 | if event in ('Exit', None): break 27 | 28 | if '__TIM' not in event: 29 | print(event, values) 30 | 31 | if event in '1 2 3 4 5 6'.split(' '): 32 | color_id = event 33 | pen_color = list(enumerate('red green blue orange brown grey'.split()))[int(color_id)-1][1] 34 | print(f'pen_color = {pen_color}') 35 | print(f'!!!!!!!!!!!') 36 | 37 | if 'w'==event: 38 | pen_size -= 3 39 | check_pen_size() 40 | if 's'==event: 41 | pen_size += 3 42 | check_pen_size() 43 | 44 | if 'F1' in event or '7' in event or 'clear' == event: 45 | # graph.Erase() 46 | graph.DrawRectangle((0,0), (400,400), fill_color=bg, line_color=bg) 47 | graph.Erase() 48 | if 'graph' == event: 49 | mouseX, mouseY = values[event] 50 | circle = graph.DrawCircle((mouseX, mouseY), pen_size, fill_color=pen_color, line_color=pen_color) 51 | if 'color' in event: 52 | pen_color = event[6:] 53 | print(f'pen_color = {pen_color}') 54 | 55 | window.close() 56 | 57 | 58 | -------------------------------------------------------------------------------- /_drawing_example.py: -------------------------------------------------------------------------------- 1 | import PySimpleGUI as sg 2 | bg = 'grey' 3 | pen_color = 'red' 4 | pen_size = 5 5 | 6 | graph = sg.Graph(canvas_size=(400, 400), graph_bottom_left=(0,0), graph_top_right=(400, 400), background_color=bg, key='graph', change_submits=True, drag_submits=True) 7 | window = sg.Window('Test', [ 8 | [graph], 9 | [ 10 | sg.B('', button_color=('red', 'red'), key='color_red', size=(3,1)) 11 | ,sg.B('', button_color=('green', 'green'), key='color_green', size=(3,1)) 12 | ,sg.B('', button_color=('blue', 'blue'), key='color_blue', size=(3,1)) 13 | ,sg.B('', button_color=('orange', 'orange'), key='color_orange', size=(3,1)) 14 | ,sg.B('', button_color=('brown', 'brown'), key='color_brown', size=(3,1)) 15 | ,sg.B('eraser', button_color=('white', bg), key='color_grey', size=(5,1)) 16 | ,sg.B('X', key='clear', size=(3,1)) 17 | ] 18 | ], finalize=True, return_keyboard_events=True) 19 | 20 | def check_pen_size(): 21 | if pen_size < 0: pen_size = 1 22 | if pen_size > 30: pen_size = 30 23 | 24 | while True: 25 | event, values = window(timeout=50) 26 | if event in ('Exit', None): break 27 | 28 | if '__TIM' not in event: 29 | print(event, values) 30 | 31 | if event in '1 2 3 4 5 6'.split(' '): 32 | color_id = event 33 | pen_color = list(enumerate('red green blue orange brown grey'.split()))[int(color_id)-1][1] 34 | print(f'pen_color = {pen_color}') 35 | print(f'!!!!!!!!!!!') 36 | 37 | if 'w'==event: 38 | pen_size -= 3 39 | check_pen_size() 40 | if 's'==event: 41 | pen_size += 3 42 | check_pen_size() 43 | 44 | if 'F1' in event or '7' in event or 'clear' == event: 45 | # graph.Erase() 46 | graph.DrawRectangle((0,0), (400,400), fill_color=bg, line_color=bg) 47 | graph.Erase() 48 | if 'graph' == event: 49 | mouseX, mouseY = values[event] 50 | circle = graph.DrawCircle((mouseX, mouseY), pen_size, fill_color=pen_color, line_color=pen_color) 51 | if 'color' in event: 52 | pen_color = event[6:] 53 | print(f'pen_color = {pen_color}') 54 | 55 | window.close() 56 | 57 | 58 | -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | ''' 4 | Public/Private chat for N users. 5 | 6 | Main things are: 7 | - GET a public board with messages 8 | - POST messages to this public board 9 | - GET a public list of currently connected users 10 | - SENT private messages to selected user 11 | - CHANGE name 12 | 13 | Author : nngogol 14 | Created : 2020-06-08 16:25:51 15 | Origin : https://github.com/nngogol/async-desktop-chat 16 | pip install : pip install pysimplegui websockets 17 | 18 | ''' 19 | 20 | import asyncio, json, websockets, datetime, time, sys, uuid 21 | from collections import namedtuple 22 | from general_variables import PORT 23 | import PySimpleGUI as sg 24 | 25 | global_message_queue = asyncio.Queue() 26 | global_websock = None 27 | GLOBAL_my_name = '' 28 | 29 | def today_date(): return datetime.datetime.now().strftime('%m-%d %H:%M:%S') 30 | enable_print = True 31 | def my_print(*args): 32 | if enable_print: 33 | print(*args) 34 | 35 | 36 | def ui(): 37 | ''' 38 | 39 | return a PySimpleGUI layout 40 | 41 | ''' 42 | global GLOBAL_my_name 43 | 44 | T_css = dict(font=("Helvetica", 12)) 45 | 46 | users = sg.Listbox([], size=(30-5, 16), enable_events=True, key='users') 47 | message_board = sg.ML( size=(50-5, 15), key='messages_board') 48 | pm_board = sg.ML( size=(30-5, 16), key='pm_board') 49 | 50 | users_column = sg.Col([ [sg.T('Users:', **T_css)], [users]]) 51 | message_board_column = sg.Col([ 52 | [sg.T('Message board', **T_css)], [message_board] 53 | ,[sg.I(key='message', size=(15, 1)), sg.B('▲ Public', key='public-msg'), sg.B('▲ User', disabled=True, key='private-msg')] 54 | ]) 55 | pm_column = sg.Col([[sg.T('PM messages', **T_css)], [pm_board] ]) 56 | 57 | layout = [ 58 | [sg.T('Your name'), sg.Input(GLOBAL_my_name, **T_css, disabled=True, use_readonly_for_disable=True, size=(30, 1), key='my_name'), sg.B('Change my name...', key='change-my-name')], 59 | [users_column, message_board_column, pm_column] 60 | ] 61 | return layout 62 | 63 | 64 | async def gui_application(): 65 | global global_message_queue, global_websock 66 | global GLOBAL_my_name 67 | while not GLOBAL_my_name: 68 | await asyncio.sleep(0.1) 69 | break 70 | 71 | try: 72 | window = sg.Window('Chat', ui(), finalize=True) 73 | except Exception as e: 74 | raise e 75 | 76 | while True: 77 | event, values = window(timeout=20) 78 | await asyncio.sleep(0.00001) 79 | if event in ('Exit', None): break 80 | if '__TIMEOUT__' != event: my_print(event)#, values) # print event name 81 | 82 | # print(event) 83 | 84 | #============= 85 | # read a queue 86 | #============= 87 | try: 88 | if not global_message_queue.empty(): 89 | while not global_message_queue.empty(): 90 | item = await global_message_queue.get() 91 | 92 | my_print(f'Handle message ▼▼▼') 93 | 94 | if not item or item is None: 95 | my_print('Bad queue item', item) 96 | break 97 | 98 | elif item['type'] == 'get-your-name': 99 | my_name = item['name'] 100 | window['my_name'](my_name) 101 | values['my_name'] = my_name 102 | my_print(f'❄❄❄ my will be {my_name}') 103 | global_message_queue.task_done(); my_print('Task done -=-=- (get-your-name)') 104 | 105 | elif item['type'] == 'new_public_messages': 106 | if item['messages_board']: 107 | my_print('❄❄❄ new_public_messages') 108 | 109 | mess = sorted(item['messages_board'], key=lambda x: x[0]) 110 | messages = '\n'.join( [ '{}: {}'.format(m[1], m[2]) for m in mess] ) 111 | 112 | # update board 113 | window['messages_board'].update(messages) 114 | global_message_queue.task_done(); my_print('Task done -=-=- (new_public_messages)') 115 | 116 | elif item['type'] == 'new_user_state': 117 | my_print('\n', '❄❄❄ new_user_state') 118 | 119 | my_name_val = values['my_name'] 120 | users_ = item['users'] 121 | filtered_users = [user_name for user_name in item['users'] if user_name != my_name_val] 122 | window['users'].update(values=filtered_users) 123 | 124 | my_print('''my_name: {}\nall_users: {}\nfiltered: {}\n'''.format(my_name_val, ','.join(users_), ','.join(filtered_users))) 125 | global_message_queue.task_done(); my_print('Task done -=-=- (new_user_state)') 126 | 127 | elif item['type'] == 'pm_message': 128 | my_print('\n', '❄❄❄ pm_message') 129 | 130 | params = today_date(), item['author'], item['text'] 131 | window['pm_board'].print("{} {: <30} : {}".format(*params)) 132 | global_message_queue.task_done(); my_print('Task done -=-=- (pm_message)') 133 | 134 | elif item['type'] == 'change-my-name': 135 | if item['status'] == 'ok': 136 | new_name = item['new_name'] 137 | window['my_name'](new_name) 138 | my_print('\n', '❄❄❄ change-my-name', f'\nnew name will be: {new_name}') 139 | 140 | elif item['status'] == 'no': 141 | sg.Popup(item['message']) 142 | 143 | global_message_queue.task_done(); my_print('Task done -=-=- (change-my-name)') 144 | 145 | elif item['type'] == 'exit': 146 | my_print('\n', '❄❄❄ exit') 147 | global_message_queue.task_done(); my_print('Task done exit -=-=- (exit)') 148 | break 149 | my_print(f'▲▲▲') 150 | 151 | except Exception as e: my_print(e, '-'*30) 152 | 153 | 154 | # if event == 'users' and values['users']: 155 | if values['users']: 156 | window['private-msg'](disabled=False) 157 | 158 | if event == 'change-my-name': 159 | new_name = sg.PopupGetText('New Name') 160 | if new_name: 161 | await global_websock.send(json.dumps({'action': 'change-my-name', "new_name": new_name})) 162 | 163 | if event == 'public-msg': 164 | message = json.dumps({'action': 'post-public-message', "text": values['message']}) 165 | 166 | my_print(f"let's send public\nI will send: {message}") 167 | await global_websock.send(message) 168 | 169 | # clear GUI text element 170 | window['message']('') 171 | 172 | if event == 'private-msg': 173 | 174 | # validate 175 | if not values['users']: 176 | window['message'].update('Please, select the user first.') 177 | continue 178 | 179 | if not values['message'].strip(): 180 | window['message'].update('Please, type a non-empty message.') 181 | continue 182 | 183 | my_print("Let's send pm") 184 | text = values['message'] 185 | which_user_name = values['users'][0] 186 | message = json.dumps({ 187 | 'action': 'send-a-pm', 188 | "which_user_name": which_user_name, 189 | 'text': text}) 190 | 191 | my_print(f'I will send: {message}') 192 | await global_websock.send(message) 193 | 194 | # clear GUI text element 195 | window['pm_board'].print("{} {: <30} : {}".format(today_date(), 'to:' + which_user_name, text)) 196 | window['message']('') 197 | 198 | # 199 | # CLOSE 200 | # 201 | # -> psg close 202 | window.close() 203 | 204 | # -> websocket close 205 | if global_websock and not global_websock.closed: 206 | await global_websock.send(json.dumps({'action': 'exit'})) # disconnect me 207 | 208 | async def websocket_reading(): 209 | global PORT, global_message_queue, global_websock, GLOBAL_my_name 210 | try: 211 | # connect to a server 212 | a_ws = await websockets.connect(f"ws://localhost:{PORT}") 213 | global_websock = a_ws 214 | GLOBAL_my_name = json.loads(await a_ws.recv())['name'] 215 | # # send "hello world" message 216 | # await a_ws.send(json.dumps({'action': 'post-public-message', "text": 'hello'})) 217 | 218 | 219 | # read messages, till you catch STOP message 220 | async for result in a_ws: 221 | json_msg = json.loads(result) 222 | 223 | # exit 224 | if json_msg['type'] == 'exit': 225 | break 226 | 227 | # put in global message queue 228 | await global_message_queue.put(json_msg) 229 | 230 | # close socket 231 | try: await a_ws.close() 232 | except Exception as e: print('Exception. cant close ws:', e) 233 | 234 | 235 | except Exception as e: 236 | print('Exception. ws died: ', e) 237 | 238 | async def client(): 239 | await asyncio.wait([websocket_reading(), gui_application()]) 240 | 241 | def main(): 242 | loop = asyncio.get_event_loop() 243 | loop.run_until_complete(client()) 244 | loop.close() 245 | 246 | if __name__ == '__main__': 247 | print('started') 248 | main() 249 | print('ended') 250 | -------------------------------------------------------------------------------- /client_drawing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | ''' 4 | Public/Private chat for N users. 5 | 6 | Main things are: 7 | - GET a public board with messages 8 | - POST messages to this public board 9 | - GET a public list of currently connected users 10 | - SENT private messages to selected user 11 | - CHANGE name 12 | 13 | Author : nngogol 14 | Created : 2020-06-08 16:25:51 15 | Origin : https://github.com/nngogol/async-desktop-chat 16 | pip install : pip install pysimplegui websockets 17 | 18 | ''' 19 | 20 | import asyncio, json, websockets, datetime, time, sys, uuid 21 | from collections import namedtuple 22 | from general_variables import PORT 23 | import PySimpleGUI as sg 24 | 25 | global_message_queue = asyncio.Queue() 26 | global_websock = None 27 | GLOBAL_my_name = '' 28 | 29 | def today_date(): return datetime.datetime.now().strftime('%m-%d %H:%M:%S') 30 | enable_print = True 31 | def my_print(*args): 32 | if enable_print: 33 | print(*args) 34 | 35 | 36 | def ui(): 37 | ''' 38 | 39 | return a PySimpleGUI layout 40 | 41 | ''' 42 | global GLOBAL_my_name 43 | 44 | T_css = dict(font=("Helvetica", 12)) 45 | 46 | users = sg.Listbox([], size=(30-5, 16), enable_events=True, key='users') 47 | message_board = sg.ML( size=(50-5, 15), key='messages_board') 48 | pm_board = sg.ML( size=(30-5, 16), key='pm_board') 49 | 50 | users_column = sg.Col([ [sg.T('Users:', **T_css)], [users]]) 51 | message_board_column = sg.Col([ 52 | [sg.T('Message board', **T_css)], [message_board] 53 | ,[sg.I(key='message', size=(15, 1)), 54 | sg.B('▲ Public', key='public-msg'), sg.B('▲ User', disabled=True, key='private-msg'), 55 | sg.B('View cam', key='view_webcam_btn_tab1'), 56 | ] 57 | ]) 58 | pm_column = sg.Col([[sg.T('PM messages', **T_css)], [pm_board] ]) 59 | 60 | main_tab = [ 61 | [sg.T('Your name'), sg.Input(GLOBAL_my_name, **T_css, disabled=True, use_readonly_for_disable=True, size=(30, 1), key='my_name'), sg.B('Change my name...', key='change-my-name')], 62 | [users_column, message_board_column, pm_column] 63 | ] 64 | 65 | 66 | 67 | # ================= 68 | # |=|=|=|=|=|=|=|=| drawing on canvas 69 | colors = 'red green blue orange brown'.split() 70 | drawing_btns = [ 71 | sg.B('', button_color=(color_name,color_name), key=f'color_{color_name}', size=(3,1)) 72 | for color_name in colors 73 | ]+[ 74 | sg.B('eraser', button_color=('white', 'grey'), key='color_grey', size=(5,1)), 75 | sg.B('X', key='clear_canvas', size=(3,1)) 76 | ] 77 | 78 | drawing_tab = [ 79 | [ 80 | 81 | sg.Graph(key='public_canvas_element', 82 | canvas_size=(400,400), graph_bottom_left=(0,0), graph_top_right=(400,400), 83 | background_color='grey', 84 | change_submits=True, drag_submits=True, 85 | metadata={ 86 | 'bg':'grey' 87 | ,'pen_color':'red' 88 | ,'pen_size':5 89 | } 90 | ) 91 | ], 92 | drawing_btns, 93 | ] 94 | 95 | 96 | NUM_LINES = 32 97 | font_size=6 98 | ml_params = dict( 99 | size=(115,NUM_LINES+5), font=('Courier', font_size), pad=(0,0), background_color='black', text_color='white' 100 | ) 101 | webcam_tab = [ 102 | [sg.ML(**ml_params, key='-client-ascii-image-'), sg.ML(**ml_params, key='-my-ascii-image-')], 103 | 104 | [ 105 | sg.B('X toggle camera', key='send_webcam_btn'), 106 | sg.B('X see picked friend', key='view_webcam_btn'), 107 | ] 108 | ] 109 | 110 | return [[sg.TabGroup([[sg.Tab(title, tab_ui, key=f'tab_{title}') for title, tab_ui in zip('chat canvas webcam'.split(' '), [main_tab, drawing_tab, webcam_tab])]], key='tabs') ] ] 111 | # return [[sg.TabGroup([[sg.Tab(title, tab_ui) for title, tab_ui in zip('chat canvas webcam'.split(' '), [main_tab, drawing_tab, webcam_tab])]])] ] 112 | 113 | 114 | from PIL import Image 115 | import numpy as np, cv2 116 | 117 | async def gui_application(): 118 | global global_message_queue, global_websock 119 | global GLOBAL_my_name 120 | while not GLOBAL_my_name: 121 | await asyncio.sleep(0.1) 122 | break 123 | 124 | 125 | try: 126 | window = sg.Window('Chat', ui(), finalize=True) 127 | except Exception as e: 128 | print('\n'*5, e, '\n'*5) 129 | 130 | # webcam 131 | cap = cv2.VideoCapture(0); cap.set(3, 640); cap.set(4, 360) 132 | is_viewing_ascii_frame = False 133 | is_viewing_ascii_frame_user = None 134 | is_sending_webcam = False 135 | # ascii conversion 136 | chars = np.asarray(list(' .,:;irsXA253hMHGS#9B&@')) 137 | SC, GCF, WCF = .1, 2, 7/4 138 | def toggle_view_user_webcam_ui(state, user=None): 139 | # state can be: 140 | # - bool 141 | # - str: 'inverse' 142 | nonlocal is_viewing_ascii_frame, is_viewing_ascii_frame_user, window 143 | if type(state) is str: is_viewing_ascii_frame = not is_viewing_ascii_frame 144 | elif type(state) is bool: is_viewing_ascii_frame = state 145 | else: 146 | raise TypeError 147 | 148 | is_viewing_ascii_frame_user = user 149 | 150 | # update gui 151 | btn_color = ('white', 'red') if is_viewing_ascii_frame else sg.DEFAULT_BUTTON_COLOR 152 | btn_char = '√' if is_viewing_ascii_frame else 'X' 153 | toggle_btn_key = 'view_webcam_btn' 154 | window[toggle_btn_key](btn_char + window[toggle_btn_key].GetText()[1:], button_color=btn_color) 155 | 156 | def toggle_send_webcam_ui(state): 157 | # state can be: 158 | # - bool 159 | # - str: 'inverse' 160 | nonlocal is_sending_webcam, window 161 | if type(state) is str: is_sending_webcam = not is_sending_webcam 162 | elif type(state) is bool: is_sending_webcam = state 163 | else: 164 | raise TypeError 165 | 166 | # update gui 167 | btn_color = ('white', 'red') if is_sending_webcam else sg.DEFAULT_BUTTON_COLOR 168 | btn_char = '√' if is_sending_webcam else 'X' 169 | toggle_btn_key = 'send_webcam_btn' 170 | window[toggle_btn_key](btn_char + window[toggle_btn_key].GetText()[1:], button_color=btn_color) 171 | 172 | 173 | def img2ascii(frame): 174 | 175 | try: 176 | img = Image.fromarray(frame) # create PIL image from frame 177 | SC = .1 178 | GCF = 1. 179 | WCF = 7/4 180 | 181 | # More magic that coverts the image to ascii 182 | S = (round(img.size[0] * SC * WCF), round(img.size[1] * SC)) 183 | img = np.sum(np.asarray(img.resize(S)), axis=2) 184 | img -= img.min() 185 | img = np.array((1.0 - img / img.max()) ** GCF * (chars.size - 1), dtype=np.uint8) 186 | 187 | # "Draw" the image in the window, one line of text at a time! 188 | str_img = '\n'.join(["".join(r) for r in chars[img.astype(int)]]) 189 | return str_img 190 | except Exception as e: 191 | return False 192 | 193 | def is_sock_open(): 194 | global global_websock 195 | return global_websock is not None and global_websock.state.value == 1 196 | 197 | while True: 198 | event, values = window(timeout=5) 199 | await asyncio.sleep(0.001) 200 | if event in ('Exit', None): break 201 | if event not in ['public_canvas_element', '__TIMEOUT__', 'public_canvas_element+UP'] : my_print(event)#, values) # print event name 202 | 203 | 204 | #============= 205 | # read a queue 206 | #============= 207 | try: 208 | if not global_message_queue.empty(): 209 | while not global_message_queue.empty(): 210 | item = await global_message_queue.get() 211 | 212 | my_print(f'Handle message ▼▼▼') 213 | 214 | if not item or item is None: 215 | my_print('Bad queue item', item) 216 | break 217 | 218 | elif item['type'] == 'view_ascii_frame': 219 | status = item['status'] 220 | if status == 'ok': 221 | try: 222 | window['-client-ascii-image-'](item['ascii_img']) 223 | except Exception as e: 224 | print('\n'*5, e, '\n'*5) 225 | import pdb; pdb.set_trace(); 226 | 227 | else: 228 | toggle_view_user_webcam_ui(False) 229 | 230 | global_message_queue.task_done(); my_print('Task done -=-=- (view_ascii_frame)') 231 | 232 | 233 | elif item['type'] == 'update_public_canvas': 234 | a_graph = window['public_canvas_element'] 235 | try: 236 | if item['do_reset_canvas']: 237 | a_graph.DrawRectangle((0,0), (400,400), fill_color='grey', line_color='grey') 238 | a_graph.Erase() 239 | else: 240 | mx,my = item['mouseXY'] 241 | a_graph.DrawCircle((mx,my), item['pen_size'], 242 | fill_color=item['pen_color'], line_color=item['pen_color']) 243 | 244 | except Exception as e: 245 | print('\n'*5, e, '\n'*5) 246 | import pdb; pdb.set_trace(); 247 | 248 | global_message_queue.task_done(); my_print('Task done -=-=- (update_public_canvas)') 249 | 250 | elif item['type'] == 'get-your-name': 251 | my_name = item['name'] 252 | window['my_name'](my_name) 253 | values['my_name'] = my_name 254 | my_print(f'❄❄❄ my will be {my_name}') 255 | global_message_queue.task_done(); my_print('Task done -=-=- (get-your-name)') 256 | 257 | elif item['type'] == 'new_public_messages': 258 | if item['messages_board']: 259 | my_print('❄❄❄ new_public_messages') 260 | 261 | mess = sorted(item['messages_board'], key=lambda x: x[0]) 262 | messages = '\n'.join( [ '{}: {}'.format(m[1], m[2]) for m in mess] ) 263 | 264 | # update board 265 | window['messages_board'].update(messages) 266 | global_message_queue.task_done(); my_print('Task done -=-=- (new_public_messages)') 267 | 268 | elif item['type'] == 'new_user_state': 269 | my_print('\n', '❄❄❄ new_user_state') 270 | 271 | my_name_val = values['my_name'] 272 | users_ = item['users'] 273 | filtered_users = [user_name for user_name in item['users'] if user_name != my_name_val] 274 | 275 | 276 | if is_viewing_ascii_frame_user not in filtered_users: 277 | toggle_view_user_webcam_ui(False) 278 | window['users'].update(values=filtered_users) 279 | 280 | my_print('''my_name: {}\nall_users: {}\nfiltered: {}\n'''.format(my_name_val, ','.join(users_), ','.join(filtered_users))) 281 | global_message_queue.task_done(); my_print('Task done -=-=- (new_user_state)') 282 | 283 | elif item['type'] == 'pm_message': 284 | my_print('\n', '❄❄❄ pm_message') 285 | 286 | params = today_date(), item['author'], item['text'] 287 | window['pm_board'].print("{} {: <30} : {}".format(*params)) 288 | global_message_queue.task_done(); my_print('Task done -=-=- (pm_message)') 289 | 290 | elif item['type'] == 'change-my-name': 291 | if item['status'] == 'ok': 292 | new_name = item['new_name'] 293 | window['my_name'](new_name) 294 | my_print('\n', '❄❄❄ change-my-name', f'\nnew name will be: {new_name}') 295 | 296 | elif item['status'] == 'no': 297 | sg.Popup(item['message']) 298 | 299 | global_message_queue.task_done(); my_print('Task done -=-=- (change-my-name)') 300 | 301 | elif item['type'] == 'exit': 302 | my_print('\n', '❄❄❄ exit') 303 | global_message_queue.task_done(); my_print('Task done exit -=-=- (exit)') 304 | break 305 | my_print(f'▲▲▲') 306 | 307 | except Exception as e: my_print(e, '-'*30) 308 | 309 | if values['users']: 310 | window['private-msg'](disabled=False) 311 | 312 | 313 | # _ 314 | # | | 315 | # __ _____| |__ ___ __ _ _ __ ___ 316 | # \ \ /\ / / _ \ '_ \ / __/ _` | '_ ` _ \ 317 | # \ V V / __/ |_) | (_| (_| | | | | | | 318 | # \_/\_/ \___|_.__/ \___\__,_|_| |_| |_| 319 | if is_viewing_ascii_frame: 320 | if not is_sock_open(): 321 | continue 322 | await global_websock.send(json.dumps({'action': 'view_ascii_frame', "which_user_name": is_viewing_ascii_frame_user})) 323 | if event == 'view_webcam_btn': 324 | # No user selected. 325 | if not values['users']: 326 | sg.popup('Select user first!') 327 | continue 328 | # change state 329 | which_user_name = values['users'][0] 330 | toggle_view_user_webcam_ui('inv', which_user_name) 331 | 332 | 333 | if is_sending_webcam: 334 | 335 | # 1. send image to server 336 | # 2. plot image in my gui 337 | 338 | ret, img = cap.read() 339 | 340 | if not ret: continue 341 | 342 | 343 | ret, frame = cap.read() 344 | ascii_image = img2ascii(frame) 345 | if not ascii_image: continue 346 | # 347 | # step 1 348 | # 349 | window['-my-ascii-image-'](ascii_image) 350 | # 351 | # step 2 352 | # 353 | if is_sock_open(): # OPEN 354 | await global_websock.send(json.dumps({ 355 | 'action': 'update_my_ascii_frame', 356 | "ascii_img": ascii_image})) 357 | 358 | if event == 'send_webcam_btn': 359 | toggle_send_webcam_ui('inv') 360 | if not is_sending_webcam and is_sock_open(): 361 | await global_websock.send(json.dumps({'action': 'close_my_ascii_frame'})) 362 | if event == 'view_webcam_btn_tab1': 363 | # view_webcam_btn_tab1 364 | if not values['users']: 365 | sg.popup('Select user first!') 366 | continue 367 | 368 | which_user_name = values['users'][0] 369 | window['tab_webcam'].Select() 370 | 371 | 372 | # _____ 373 | # / ____| 374 | # | | __ _ _ ____ ____ _ ___ 375 | # | | / _` | '_ \ \ / / _` / __| 376 | # | |___| (_| | | | \ V / (_| \__ \ 377 | # \_____\__,_|_| |_|\_/ \__,_|___/ drawing 378 | # on mouse clicked 379 | if event == 'public_canvas_element' and is_sock_open(): 380 | # import pdb; pdb.set_trace(); 381 | 382 | graph = window['public_canvas_element'] 383 | mouseXY = values['public_canvas_element'] 384 | pen_color = graph.metadata['pen_color'] 385 | pen_size = graph.metadata['pen_size'] 386 | graph.DrawCircle(mouseXY, pen_size, fill_color=pen_color, line_color=pen_color) 387 | 388 | await global_websock.send(json.dumps({ 389 | 'action': 'update_public_canvas' 390 | ,'do_reset_canvas' : False 391 | ,'mouseXY' : mouseXY 392 | ,'pen_color' : pen_color 393 | ,'pen_size' : pen_size 394 | })) 395 | if event == 'clear_canvas' and is_sock_open(): 396 | graph = window['public_canvas_element'] 397 | graph.DrawRectangle((0,0), (400,400), fill_color='grey', line_color='grey') 398 | graph.Erase() 399 | await global_websock.send(json.dumps({'action': 'update_public_canvas', 'do_reset_canvas' :True})) 400 | if event.startswith('color_'): 401 | graph.metadata['pen_color'] = event.split('_')[1] 402 | print(f'new color/') 403 | 404 | 405 | # ========== 406 | 407 | if event == 'change-my-name': 408 | new_name = sg.PopupGetText('New Name') 409 | if new_name: 410 | await global_websock.send(json.dumps({'action': 'change-my-name', "new_name": new_name})) 411 | 412 | if event == 'public-msg': 413 | message = json.dumps({'action': 'post-public-message', "text": values['message']}) 414 | 415 | my_print(f"let's send public\nI will send: {message}") 416 | await global_websock.send(message) 417 | 418 | # clear GUI text element 419 | window['message']('') 420 | 421 | if event == 'private-msg': 422 | 423 | # validate 424 | if not values['users']: 425 | window['message'].update('Please, select the user first.') 426 | continue 427 | 428 | if not values['message'].strip(): 429 | window['message'].update('Please, type a non-empty message.') 430 | continue 431 | 432 | my_print("Let's send pm") 433 | text = values['message'] 434 | which_user_name = values['users'][0] 435 | message = json.dumps({ 436 | 'action': 'send-a-pm', 437 | "which_user_name": which_user_name, 438 | 'text': text}) 439 | 440 | my_print(f'I will send: {message}') 441 | await global_websock.send(message) 442 | 443 | # clear GUI text element 444 | window['pm_board'].print("{} {: <30} : {}".format(today_date(), 'to:' + which_user_name, text)) 445 | window['message']('') 446 | 447 | # 448 | # CLOSE 449 | # 450 | # -> psg close 451 | window.close() 452 | 453 | # -> websocket close 454 | if global_websock and not global_websock.closed: 455 | await global_websock.send(json.dumps({'action': 'exit'})) # disconnect me 456 | 457 | async def websocket_reading(): 458 | global PORT, global_message_queue, global_websock, GLOBAL_my_name 459 | try: 460 | # connect to a server 461 | a_ws = await websockets.connect(f"ws://localhost:{PORT}") 462 | global_websock = a_ws 463 | GLOBAL_my_name = json.loads(await a_ws.recv())['name'] 464 | # # send "hello world" message 465 | # await a_ws.send(json.dumps({'action': 'post-public-message', "text": 'hello'})) 466 | 467 | 468 | # read messages, till you catch STOP message 469 | async for result in a_ws: 470 | json_msg = json.loads(result) 471 | 472 | # exit 473 | if json_msg['type'] == 'exit': 474 | break 475 | 476 | # put in global message queue 477 | await global_message_queue.put(json_msg) 478 | 479 | # close socket 480 | try: await a_ws.close() 481 | except Exception as e: print('Exception. cant close ws:', e) 482 | 483 | 484 | except Exception as e: 485 | print('Exception. ws died: ', e) 486 | 487 | async def client(): 488 | await asyncio.wait([websocket_reading(), gui_application()]) 489 | 490 | def main(): 491 | loop = asyncio.get_event_loop() 492 | loop.run_until_complete(client()) 493 | loop.close() 494 | 495 | if __name__ == '__main__': 496 | print('started') 497 | main() 498 | print('ended') 499 | -------------------------------------------------------------------------------- /diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nngogol/async-desktop-chat/70ea3561cb22f84993128af976e07f8e96d4e7df/diagram.jpg -------------------------------------------------------------------------------- /diagrams/output/en.diagram-start.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nngogol/async-desktop-chat/70ea3561cb22f84993128af976e07f8e96d4e7df/diagrams/output/en.diagram-start.svg.png -------------------------------------------------------------------------------- /diagrams/output/en.diagram2_1.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nngogol/async-desktop-chat/70ea3561cb22f84993128af976e07f8e96d4e7df/diagrams/output/en.diagram2_1.svg.png -------------------------------------------------------------------------------- /diagrams/output/en.diagram2_2.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nngogol/async-desktop-chat/70ea3561cb22f84993128af976e07f8e96d4e7df/diagrams/output/en.diagram2_2.svg.png -------------------------------------------------------------------------------- /diagrams/output/en.diagram2_3.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nngogol/async-desktop-chat/70ea3561cb22f84993128af976e07f8e96d4e7df/diagrams/output/en.diagram2_3.svg.png -------------------------------------------------------------------------------- /diagrams/output/ru.diagram-start.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nngogol/async-desktop-chat/70ea3561cb22f84993128af976e07f8e96d4e7df/diagrams/output/ru.diagram-start.svg.png -------------------------------------------------------------------------------- /diagrams/output/ru.diagram2_1.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nngogol/async-desktop-chat/70ea3561cb22f84993128af976e07f8e96d4e7df/diagrams/output/ru.diagram2_1.svg.png -------------------------------------------------------------------------------- /diagrams/output/ru.diagram2_2.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nngogol/async-desktop-chat/70ea3561cb22f84993128af976e07f8e96d4e7df/diagrams/output/ru.diagram2_2.svg.png -------------------------------------------------------------------------------- /diagrams/output/ru.diagram2_3.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nngogol/async-desktop-chat/70ea3561cb22f84993128af976e07f8e96d4e7df/diagrams/output/ru.diagram2_3.svg.png -------------------------------------------------------------------------------- /diagrams/readme.md: -------------------------------------------------------------------------------- 1 | Все диаграмы очень просты + они написане на простом языке. Есть 2 языка: ru, en. 2 | > в папке `text` 3 | 4 | Из диаграм я делаю svg-шки 5 | И перевожу их в png - мне так легче смотреть на них; И удаляю svg-шки 6 | > в папке `output` 7 | 8 | Чтобы сгенерировать: 9 | ```bash 10 | $ ./run.sh 11 | ``` -------------------------------------------------------------------------------- /diagrams/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Package: 4 | # origin: https://github.com/francoislaberge/diagrams 5 | # Installation in shell 6 | # $ sudo npm install -g diagrams 7 | 8 | 9 | 10 | main(){ 11 | 12 | echo start - `date` 13 | 14 | # clear previous session 15 | rm -rf svg 16 | mkdir svg 17 | 18 | ###################### 19 | # # 20 | # # 21 | # ___ _ __ # 22 | # / _ \ '_ \ # 23 | # | __/ | | | # 24 | # \___|_| |_| # 25 | # # 26 | # # 27 | ###################### 28 | 29 | echo en version 30 | echo . 31 | diagrams sequence text/en/diagram-start.sequence output/en.diagram-start.svg 32 | echo .. 33 | diagrams sequence text/en/diagram2_1.sequence output/en.diagram2_1.svg 34 | echo ... 35 | diagrams sequence text/en/diagram2_2.sequence output/en.diagram2_2.svg 36 | echo .... 37 | diagrams sequence text/en/diagram2_3.sequence output/en.diagram2_3.svg 38 | 39 | 40 | ###################### 41 | # # 42 | # # 43 | # _ __ _ _ # 44 | # | '__| | | | # 45 | # | | | |_| | # 46 | # |_| \__,_| # 47 | # # 48 | # # 49 | ###################### 50 | 51 | 52 | echo ru version 53 | echo . 54 | diagrams sequence text/ru/diagram-start.sequence output/ru.diagram-start.svg 55 | echo .. 56 | diagrams sequence text/ru/diagram2_1.sequence output/ru.diagram2_1.svg 57 | echo ... 58 | diagrams sequence text/ru/diagram2_2.sequence output/ru.diagram2_2.svg 59 | echo .... 60 | diagrams sequence text/ru/diagram2_3.sequence output/ru.diagram2_3.svg 61 | 62 | 63 | # convert svg to png 64 | for i in output/*.svg; do 65 | convert "$i" "$i.png" 66 | done 67 | # rm all svg 68 | rm output/*.svg 69 | 70 | echo end - `date` 71 | } 72 | 73 | time main -------------------------------------------------------------------------------- /diagrams/text/en/diagram-start.sequence: -------------------------------------------------------------------------------- 1 | # Basic gist 2 | 3 | title: Async chat on WebSockets 4 | 5 | note left of Server: listen port 8000 6 | note left of Client: run GUI, try to connect with server's WebSocket 7 | 8 | 9 | Client->Server: conn via sock 10 | note left of Server: Yup, we have a new ws connection! 11 | note left of Server: remember user in set() 12 | Server->Client: (via ws) send user's name on server 13 | Server->Client: (optionaly) send board state 14 | 15 | 16 | note right of Server: on_message listen... 17 | note right of Server: no be continued\nin next diagrams... 18 | -------------------------------------------------------------------------------- /diagrams/text/en/diagram2_1.sequence: -------------------------------------------------------------------------------- 1 | # on change-my-name event 2 | title: handle on.message messages 3 | note right of Server: listen .on_message\nfrom WebSocket... 4 | 5 | # 1 change name 6 | # 2 client 2 all (public message) 7 | # 3 client 2 client (private message) 8 | 9 | 10 | # 11 | # 1. change name 12 | # 13 | 14 | Alice-->Server: Send new name 15 | note left of Server: case if data["action"] == "change-my-name": 16 | note left of Server: Okay, my steps:\nupdate name AND tell other users about my new name\n1. update_user_name(user.name, json['new_name'])\n2. for ws in users: ws.send(state) 17 | 18 | Server->Joe: update user's name 19 | Joe->Joe: show new name 20 | Server->Bill: update user's name 21 | Bill->Bill: show new name 22 | Server->Tom: update user's name 23 | Tom->Tom: show new name 24 | 25 | -------------------------------------------------------------------------------- /diagrams/text/en/diagram2_2.sequence: -------------------------------------------------------------------------------- 1 | # on change-my-name event 2 | title: handle on.message messages 3 | note right of Server: listen .on_message\nfrom WebSocket... 4 | 5 | # 1 change name 6 | # 2 client 2 all (public message) 7 | # 3 client 2 client (private message) 8 | 9 | 10 | # 11 | # 2. client 2 all (public message) 12 | # 13 | 14 | Alice-->Server: Send TEXT_MSG\nin public board 15 | note left of Server: словил if data["action"] == "post-public-message": 16 | note left of Server: Okay, my steps:\nupdate board AND notify others. \n1. board += TEXT_MSG + '\\n'\n2. for ws in users: ws.send(TEXT_MSG) 17 | 18 | Server->Joe: update board 19 | Joe->Joe: new text in board! 20 | Server->Bill: update board 21 | Bill->Bill: new text in board! 22 | Server->Tom: update board 23 | Tom->Tom: new text in board! 24 | 25 | -------------------------------------------------------------------------------- /diagrams/text/en/diagram2_3.sequence: -------------------------------------------------------------------------------- 1 | # on change-my-name event 2 | title: handle on.message messages 3 | note right of Server: listen .on_message\nfrom WebSocket... 4 | 5 | # 1 change name 6 | # 2 client 2 all (public message) 7 | # 3 client 2 client (private message) 8 | 9 | 10 | # 11 | # 3. client 2 client (private message) 12 | # 13 | 14 | Alice-->Server: Send TEXT_MSG to user 15 | note left of Server: case if data["action"] == "send-a-pm": 16 | note left of Server: Okay, my steps:\nsend msg to user\n1. targ_user = finduser(json['target_user'])\n2. targ_user.send_message(json['msgtext']) 17 | 18 | Server->Joe: send msg 19 | Joe->Joe: show's Alice message 20 | 21 | -------------------------------------------------------------------------------- /diagrams/text/ru/diagram-start.sequence: -------------------------------------------------------------------------------- 1 | # Basic gist 2 | 3 | title: Асинхронный чат на ВебСокетах 4 | 5 | note left of Server: слушаем порт 8000 6 | note left of Client: запускаем GUI, пытаемся сделать\nсоединение с вебсокетом 7 | 8 | 9 | Client->Server: подкл via сокет 10 | note left of Server: Опа, у нас новый ws подключение! 11 | note left of Server: запомнить юзера в set() 12 | Server->Client: (via ws) выслать имя юзера на серваке 13 | Server->Client: (optionaly) выслать состояние доски 14 | 15 | 16 | note right of Server: on_message слушаем... 17 | note right of Server: no be continued\nв след диаграммах... 18 | 19 | -------------------------------------------------------------------------------- /diagrams/text/ru/diagram2_1.sequence: -------------------------------------------------------------------------------- 1 | # on change-my-name event 2 | title: handle on.message messages 3 | note right of Server: слушаем .on_message\nот вебсокета... 4 | 5 | # 1 change name 6 | # 2 client 2 all (public message) 7 | # 3 client 2 client (private message) 8 | 9 | 10 | # 11 | # 1. change name 12 | # 13 | 14 | Alice-->Server: Прислать свое новое имя 15 | note left of Server: словил if data["action"] == "change-my-name": 16 | note left of Server: Окей, мои шаги:\nобнови имя И скажи другим о твоем новом имени\n1. update_user_name(user.name, json['new_name'])\n2. for ws in users: ws.send(state) 17 | 18 | Server->Joe: обновление имени у юзера 19 | Joe->Joe: показывается новое имя 20 | Server->Bill: обновление имени у юзера 21 | Bill->Bill: показывается новое имя 22 | Server->Tom: обновление имени у юзера 23 | Tom->Tom: показывается новое имя 24 | 25 | -------------------------------------------------------------------------------- /diagrams/text/ru/diagram2_2.sequence: -------------------------------------------------------------------------------- 1 | # on change-my-name event 2 | title: handle on.message messages 3 | note right of Server: слушаем .on_message\nот вебсокета... 4 | 5 | # 1 change name 6 | # 2 client 2 all (public message) 7 | # 3 client 2 client (private message) 8 | 9 | 10 | # 11 | # 2. client 2 all (public message) 12 | # 13 | 14 | Alice-->Server: Послать TEXT_MSG\nв публичную доску 15 | note left of Server: словил if data["action"] == "post-public-message": 16 | note left of Server: Окей, мои шаги:\nобнови доску И пошли другим новое сообщение\n1. board += TEXT_MSG + '\\n'\n2. for ws in users: ws.send(state) 17 | #note left of Server: Окей, мои шаги:\nupdate board AND notify others. \n1. board += TEXT_MSG + '\\n'\n2. for ws in users: ws.send(TEXT_MSG) 18 | 19 | Server->Joe: обновление доски 20 | Joe->Joe: новый текст в доске! 21 | Server->Bill: обновление доски 22 | Bill->Bill: новый текст в доске! 23 | Server->Tom: обновление доски 24 | Tom->Tom: новый текст в доске! 25 | 26 | -------------------------------------------------------------------------------- /diagrams/text/ru/diagram2_3.sequence: -------------------------------------------------------------------------------- 1 | # on change-my-name event 2 | title: handle on.message messages 3 | note right of Server: слушаем .on_message\nот вебсокета... 4 | 5 | # 1 change name 6 | # 2 client 2 all (public message) 7 | # 3 client 2 client (private message) 8 | 9 | 10 | # 11 | # 3. client 2 client (private message) 12 | # 13 | 14 | Alice-->Server: Послать TEXT_MSG в пользователю 15 | note left of Server: словил if data["action"] == "send-a-pm": 16 | note left of Server: Окей, мои шаги: послать сообщение юзера\n1. targ_user = finduser(json['target_user'])\n2. targ_user.send_message(json['msgtext']) 17 | 18 | Server->Joe: посылка сообщение 19 | Joe->Joe: показывается текст\nот юзера Алисы 20 | 21 | -------------------------------------------------------------------------------- /general_variables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | PORT = 8050 -------------------------------------------------------------------------------- /out2.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nngogol/async-desktop-chat/70ea3561cb22f84993128af976e07f8e96d4e7df/out2.mkv -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | ''' 4 | Public/Private chat for N users. 5 | 6 | Main things are: 7 | - GET a public board with messages 8 | - POST messages to this public board 9 | - GET a public list of currently connected users 10 | - SENT private messages to selected user 11 | - CHANGE name 12 | 13 | Author : nngogol 14 | Created : 2020-06-08 16:25:53 15 | Origin : https://github.com/nngogol/async-desktop-chat 16 | pip install : pip install pysimplegui websockets 17 | 18 | ''' 19 | 20 | import asyncio, json, websockets, datetime, time, sys, uuid 21 | from collections import namedtuple 22 | from general_variables import PORT 23 | 24 | class User(object): 25 | def __init__(self, name, ws, uuid): 26 | self.name = name 27 | self.ws = ws 28 | self.uuid = uuid 29 | self.curr_ascii_img = '' 30 | USERS = set() 31 | 32 | Message = namedtuple('Message', 'time author_name text'.split(' ')) 33 | STATE = { 34 | "messages_board": [], 35 | "video_frames" : {} 36 | } 37 | 38 | def mk_uuid4(): return str(uuid.uuid4()) 39 | 40 | def state_event(): 41 | global STATE 42 | if not STATE['messages_board']: 43 | return 44 | return json.dumps({"type": "new_public_messages", 'messages_board' : STATE['messages_board']}) 45 | 46 | def get_available_name(): 47 | global USERS 48 | names = [user.name for user in USERS] 49 | 50 | myname = f'user#{str(len(USERS)+0)}' 51 | for i in range(10_000_000): 52 | if myname not in names: break 53 | myname = f'user#{str(len(USERS)+i)}' 54 | return myname 55 | 56 | async def notify_state(): 57 | global USERS, STATE 58 | 59 | if USERS: # asyncio.wait doesn't accept an empty list 60 | message = state_event() 61 | if message: 62 | await asyncio.wait([user.ws.send(message) for user in USERS]) 63 | return 64 | print('no messages to sent') 65 | 66 | else: 67 | USERS = set() 68 | STATE['messages_board'] = [] 69 | 70 | def get_user_names(): return [user.name for user in USERS] 71 | 72 | async def notify_users(skip_user=None): 73 | global USERS 74 | if not USERS: return 75 | 76 | users_with_himself = USERS - set([skip_user]) if skip_user else USERS 77 | 78 | if len(users_with_himself) != 0: # asyncio.wait doesn't accept an empty list 79 | 80 | message_json = {'type': 'new_user_state', "users": get_user_names() } 81 | message = json.dumps(message_json) 82 | print('I will send messages to: ', ','.join(message_json['users'])) 83 | await asyncio.wait([ user.ws.send(message) 84 | for user in users_with_himself]) 85 | 86 | async def notify_users_msg(msg, skip_user=None): 87 | global USERS 88 | if not USERS: return 89 | 90 | users_with_himself = USERS - set([skip_user]) if skip_user else USERS 91 | 92 | if len(users_with_himself) != 0: # asyncio.wait doesn't accept an empty list 93 | message = json.dumps(msg) 94 | await asyncio.wait([ user.ws.send(message) 95 | for user in users_with_himself]) 96 | 97 | # for i in USERS: 98 | # await i.ws.send(json.dumps(data)) 99 | 100 | async def register(user): 101 | global USERS 102 | USERS.add(user) 103 | await notify_users() 104 | 105 | async def unregister(user): 106 | global USERS 107 | 108 | if len(USERS) == 1 and list(USERS)[0] == user: 109 | USERS = set() 110 | STATE['messages_board'] = [] 111 | if len(USERS) > 1: 112 | for auser in USERS: # set([i for id_, websocket in USERS if id_ == name]): 113 | if auser.name == user.name: 114 | USERS.remove(user) 115 | await notify_users() 116 | break 117 | 118 | print(f'No users left.' if not USERS else f'# {len(USERS)} users online: ' + ', '.join(get_user_names())) 119 | 120 | async def on_ws_connected(websocket, path): 121 | # register(websocket) sends user_event() to websocket 122 | 123 | # add user to a list 124 | curr_user = User(get_available_name(), websocket, mk_uuid4()) 125 | await websocket.send(json.dumps({'type' : 'get-your-name', 'name' : curr_user.name})) 126 | await register(curr_user) 127 | 128 | try: 129 | # show public board 130 | state = state_event() 131 | if state: 132 | await websocket.send(state) 133 | 134 | # READ messages from user 135 | # this for loop is live "infinite while true" loop. 136 | # It will end, end connection is dropped. 137 | 138 | async for message in websocket: 139 | data = json.loads(message) 140 | 141 | if data["action"] == "exit": 142 | # EXITING MESSAGE from user 143 | await curr_user.ws.send(json.dumps({'type': 'exit'})) 144 | await websocket.close() 145 | break 146 | 147 | ################################################################################## 148 | # _ _ # 149 | # | | (_) # 150 | # _ _ ___ ___ _ __ ___ _ __ ___ _ __ __ _| |_ _ ___ _ __ ___ # 151 | # | | | / __|/ _ \ '__| / _ \| '_ \ / _ \ '__/ _` | __| |/ _ \| '_ \/ __| # 152 | # | |_| \__ \ __/ | | (_) | |_) | __/ | | (_| | |_| | (_) | | | \__ \ # 153 | # \__,_|___/\___|_| \___/| .__/ \___|_| \__,_|\__|_|\___/|_| |_|___/ # 154 | # | | # 155 | # |_| # 156 | ################################################################################## 157 | elif data["action"] == "change-my-name": 158 | new_name = data["new_name"] 159 | 160 | # if there are user with name like our user wants: 161 | # -(stratergy)-> modify name a little bit 162 | # -(stratergy)-> ✔say no (all users must have uniq name) 163 | # -(stratergy)-> say yes (all users are identified by uuid on a server) 164 | filtered_users = [user_name for user_name in get_user_names() if user_name == new_name] 165 | if filtered_users: 166 | await curr_user.ws.send(json.dumps({'type': 'change-my-name', 'status': 'no', 'message': 'name is taken'})) 167 | continue 168 | 169 | curr_user.name = new_name 170 | await curr_user.ws.send( json.dumps({'type': 'change-my-name', 'status': 'ok', 'new_name': new_name}) ) 171 | await notify_users(curr_user) 172 | 173 | ############################################################################ 174 | # _ _ # 175 | # | | | | # 176 | # | |_ _____ _| |_ _ __ ___ ___ ___ ___ __ _ __ _ ___ ___ # 177 | # | __/ _ \ \/ / __| | '_ ` _ \ / _ \/ __/ __|/ _` |/ _` |/ _ \/ __| # 178 | # | || __/> <| |_ | | | | | | __/\__ \__ \ (_| | (_| | __/\__ \ # 179 | # \__\___/_/\_\\__| |_| |_| |_|\___||___/___/\__,_|\__, |\___||___/ # 180 | # __/ | # 181 | # |___/ # 182 | ############################################################################ 183 | elif data["action"] == "post-public-message": 184 | # This is "user to users" message 185 | STATE["messages_board"].append( 186 | Message(time=time.time(), 187 | author_name=curr_user.name, 188 | text=data["text"]) ) 189 | await notify_state() 190 | 191 | elif data["action"] == "send-a-pm": 192 | # This is "user to user" message 193 | # Don't record messages in servers logs 194 | target_user_name = data["which_user_name"] 195 | message_eater = [user for user in USERS if user.name == target_user_name][0] 196 | await message_eater.ws.send(json.dumps({'type': 'pm_message', 197 | 'text': data["text"], 198 | 'author': curr_user.name})) 199 | 200 | 201 | # _ _ _ 202 | # (_|_) (_) 203 | # __ _ ___ ___ _ _ _ _ __ ___ __ _ __ _ ___ 204 | # / _` / __|/ __| | | | | '_ ` _ \ / _` |/ _` |/ _ \ 205 | # | (_| \__ \ (__| | | | | | | | | | (_| | (_| | __/ 206 | # \__,_|___/\___|_|_| |_|_| |_| |_|\__,_|\__, |\___| 207 | # __/ | 208 | # |___/ 209 | # ON 210 | elif data["action"] == "update_my_ascii_frame": curr_user.curr_ascii_img = data["ascii_img"] 211 | # OFF 212 | elif data["action"] == "close_my_ascii_frame": curr_user.curr_ascii_img = '' 213 | # SEND 214 | elif data["action"] == "view_ascii_frame": 215 | target_user_name = data["which_user_name"] 216 | message_eater = [user for user in USERS if user.name == target_user_name][0] 217 | ascii_img = message_eater.curr_ascii_img 218 | if ascii_img: 219 | await curr_user.ws.send(json.dumps({'type': 'view_ascii_frame', 'status' : 'ok', 'ascii_img': ascii_img})) 220 | else: 221 | await curr_user.ws.send(json.dumps({'type': 'view_ascii_frame', 'status' : 'empty'})) 222 | # ___ __ _ _ ____ ____ _ ___ 223 | # / __/ _` | '_ \ \ / / _` / __| 224 | # | (_| (_| | | | \ V / (_| \__ \ 225 | # \___\__,_|_| |_|\_/ \__,_|___/ 226 | 227 | elif data["action"] == "update_public_canvas": 228 | data['type'] = data['action'] 229 | 230 | # forwarding 231 | await notify_users_msg(data) 232 | 233 | 234 | else: 235 | print("unsupported event: {}".format(data)) 236 | print(f'Connection with user {curr_user.name} is done.') 237 | except Exception as e: 238 | print(f'Error with user {curr_user.name} >', e) 239 | 240 | print(f'Unregistering user {curr_user.name}') 241 | await unregister(curr_user) 242 | print(f'Bye, {curr_user.name}!') 243 | 244 | def main(): 245 | global PORT 246 | loop = asyncio.get_event_loop() 247 | loop.run_until_complete(websockets.serve(on_ws_connected, "localhost", PORT)) 248 | loop.run_forever() 249 | 250 | if __name__ == '__main__': 251 | print('started') 252 | main() 253 | print('ended') --------------------------------------------------------------------------------