Links: PYTHON - PROGRAMMING
Rel: python 3rd party packages
Ref: home; docs; reStructuredText renderer ;
Tags: #public
kivy (multi-touch apps)
pip install kivy
#media
Rel: sockets
sentdex video tut I believe
chatapp.py
import os
import sys
import kivy
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout # other layout structures exist {: id="other-layout-structures-exist" }
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.uix.scrollview import ScrollView
kivy.require("1.10.1")
import socket_client # socket_client imports the server stuff, but won't need to touch that other than for error handling {: id="socket-client-imports-the-server-stuff,-but-won't-need-to-touch-that-other-than-for-error-handling" }
# potential security issues: {: id="potential-security-issues:" }
# 1. passing through kivy {: id="1.-passing-through-kivy" }
# 2. very little security checks from socket based tut {: id="2.-very-little-security-checks-from-socket-based-tut" }
class ConnectPage(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs) # GridLayout __init__ {: id="gridlayout---init--" }
self.cols = 2 # num cols in grid {: id="num-cols-in-grid" }
if os.path.isfile('prev_details.txt'):
with open('prev_details.txt', 'r') as f:
d = f.read().split(',')
prev_ip = d[0].strip()
prev_port = d[1].strip()
prev_username = d[2].strip()
else:
prev_ip = ''
prev_port = ''
prev_username = ''
self.add_widget(Label(text='IP:')) # top left {: id="top-left" }
self.ip = TextInput(text=prev_ip, multiline=False)
self.add_widget(self.ip) # top right {: id="top-right" }
self.add_widget(Label(text='Port:')) # next left {: id="next-left" }
self.port = TextInput(text=prev_port, multiline=False)
self.add_widget(self.port) # etc. {: id="etc." }
self.add_widget(Label(text='Username:'))
self.username = TextInput(text=prev_username, multiline=False)
self.add_widget(self.username)
self.add_widget(Label()) # blank space {: id="blank-space" }
self.join = Button(text='Join')
self.join.bind(on_press=self.join_button) # on_hover? hold? {: id="on-hover?-hold?" }
self.add_widget(self.join)
def join_button(self, instance):
# can get stuff at any time, not just scripty / button click {: id="can-get-stuff-at-any-time,-not-just-scripty-/-button-click" }
port = self.port.text
ip = self.ip.text
username = self.username.text
with open('prev_details.txt', 'w') as f:
f.write(f"{ip}, {port}, {username}")
info = f"Attempting to join {ip}:{port} as {username}"
chatapp.info_page.update_info(info)
chatapp.screen_manager.current = "Info"
Clock.schedule_once(self.connect, 1) # 1 second into the future {: id="1-second-into-the-future" }
def connect(self, _): # instance, how many s gone by since schedule. Always 1 in this case, so _ to denote it's useless to us. {: id="instance,-how-many-s-gone-by-since-schedule.-always-1-in-this-case,-so---to-denote-it's-useless-to-us." }
port = int(self.port.text)
ip = self.ip.text
username = self.username.text
if not socket_client.connect(ip, port, username, show_error):
return
chatapp.create_chat_page()
chatapp.screen_manager.current = "Chat"
class ScrollableLabel(ScrollView):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.layout = GridLayout(cols=1, size_hint_y=None)
self.add_widget(self.layout)
self.chat_history = Label(size_hint_y=None, markup=True) # markup to change colors for ea user {: id="markup-to-change-colors-for-ea-user" }
self.scroll_to_point = Label() # target to scroll to {: id="target-to-scroll-to" }
self.layout.add_widget(self.chat_history)
self.layout.add_widget(self.scroll_to_point)
def update_chat_history(self, message):
self.chat_history.text += '\n' + message # not sanitized {: id="not-sanitized" }
self.layout.height = self.chat_history.texture_size[1] + 15 # 15 pixels for space at bottom {: id="15-pixels-for-space-at-bottom" }
self.chat_history.height = self.chat_history.texture_size[1]
self.chat_history.text_size = (self.chat_history.width*0.98, None)
self.scroll_to(self.scroll_to_point)
def update_chat_history_layout(self, _=None):
""" calling layout changes again from above again to maintain layout """
self.layout.height = self.chat_history.texture_size[1] + 15
self.chat_history.height = self.chat_history.texture_size[1]
self.chat_history.text_size = (self.chat_history.width*0.98, None)
class ChatPage(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.cols = 1
self.rows = 2
self.history = ScrollableLabel(height=Window.size[1]*0.9, size_hint_y=None)
self.add_widget(self.history)
self.new_message = TextInput(width=Window.size[0]*0.8, size_hint_x=None, multiline=False)
self.send = Button(text='Send')
self.send.bind(on_press=self.send_message)
bottom_line = GridLayout(cols=2)
bottom_line.add_widget(self.new_message)
bottom_line.add_widget(self.send)
self.add_widget(bottom_line)
Window.bind(on_key_down=self.on_key_down)
Clock.schedule_once(self.focus_text_input, 1)
socket_client.start_listening(self.incoming_message, show_error)
self.bind(size=self.adjust_fields)
def adjust_fields(self, *_):
# chat history height @ 90%, but leaving min 50px at bottom for new msg / send button: {: id="chat-history-height-@-90%,-but-leaving-min-50px-at-bottom-for-new-msg-/-send-button:" }
if Window.size[1] * 0.1 < 50:
new_height = Window.size[1] - 50
else:
new_height = Window.size[1] * 0.9
self.history.height = new_height
# new message width @ 80%, but leaving min 160px for send button: {: id="new-message-width-@-80%,-but-leaving-min-160px-for-send-button:" }
if Window.size[0] * 0.2 < 160:
new_width = Window.size[0] - 160
else:
new_width = Window.size[0] * 0.8
self.new_message.width = new_width
# update chat history layout: {: id="update-chat-history-layout:" }
Clock.schedule_once(self.history.update_chat_history_layout, 0.01)
def send_message(self, _): # return whether you hit send button or enter key - we don't care {: id="return-whether-you-hit-send-button-or-enter-key---we-don't-care" }
message = self.new_message.text
self.new_message.text = '' # remove message from box once sent {: id="remove-message-from-box-once-sent" }
if message:
self.history.update_chat_history(f"[color=dd2020]{chatapp.connect_page.username.text}[/color] > {message}")
socket_client.send(message)
Clock.schedule_once(self.focus_text_input, 0.1)
def on_key_down(self, instance, keyboard, keycode, text, modifiers):
if keycode == 40:
self.send_message(None)
def focus_text_input(self, _):
self.new_message.focus = True
def incoming_message(self, username, message):
self.history.update_chat_history(f"[color=20dd20]{username}[/color] > {message}")
class InfoPage(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.cols = 1
self.message = Label(halign='center', valign='middle', font_size=30) # or .kv files {: id="or-.kv-files" }
self.message.bind(width=self.update_text_width) # update size dynamically {: id="update-size-dynamically" }
self.add_widget(self.message)
def update_info(self, message):
self.message.text = message
def update_text_width(self, *_): # anything else, but we don't care {: id="anything-else,-but-we-don't-care" }
self.message.text_size = (self.message.width * 0.9, None) # none bc not worried about y {: id="none-bc-not-worried-about-y" }
class ChatApp(App):
def build(self):
# return ConnectPage() {: id="return-connectpage()" }
self.screen_manager = ScreenManager()
self.connect_page = ConnectPage()
screen = Screen(name='Connect') # use "Connect" now anytime we want to ref this screen {: id="use-"connect"-now-anytime-we-want-to-ref-this-screen" }
screen.add_widget(self.connect_page)
self.screen_manager.add_widget(screen)
self.info_page = InfoPage()
screen = Screen(name='Info')
screen.add_widget(self.info_page)
self.screen_manager.add_widget(screen)
return self.screen_manager
def create_chat_page(self):
"""
Same logic as above.
Separated out bc we need to connect before we can run this / build page.
"""
self.chat_page = ChatPage()
screen = Screen(name='Chat')
screen.add_widget(self.chat_page)
self.screen_manager.add_widget(screen)
def show_error(message):
chatapp.info_page.update_info(message)
chatapp.screen_manager.current = "Info"
Clock.schedule_once(sys.exit, 10)
if __name__ == "__main__":
chatapp = ChatApp()
chatapp.run()
socket_client.py
"""
Copied directly. Slightly modified code from chatapp in sockets.
"""
import socket
import errno
from threading import Thread
HEADER_LENGTH = 10
client_socket = None
# Connects to the server {: id="connects-to-the-server" }
def connect(ip, port, my_username, error_callback):
global client_socket
# Create a socket {: id="create-a-socket" }
# socket.AF_INET - address family, IPv4, some otehr possible are AF_INET6, AF_BLUETOOTH, AF_UNIX {: id="socket.af-inet---address-family,-ipv4,-some-otehr-possible-are-af-inet6,-af-bluetooth,-af-unix" }
# socket.SOCK_STREAM - TCP, conection-based, socket.SOCK_DGRAM - UDP, connectionless, datagrams, socket.SOCK_RAW - raw IP packets {: id="socket.sock-stream---tcp,-conection-based,-socket.sock-dgram---udp,-connectionless,-datagrams,-socket.sock-raw---raw-ip-packets" }
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# Connect to a given ip and port {: id="connect-to-a-given-ip-and-port" }
client_socket.connect((ip, port))
except Exception as e:
# Connection error {: id="connection-error" }
error_callback('Connection error: {}'.format(str(e)))
return False
# Prepare username and header and send them {: id="prepare-username-and-header-and-send-them" }
# We need to encode username to bytes, then count number of bytes and prepare header of fixed size, that we encode to bytes as well {: id="we-need-to-encode-username-to-bytes,-then-count-number-of-bytes-and-prepare-header-of-fixed-size,-that-we-encode-to-bytes-as-well" }
username = my_username.encode('utf-8')
username_header = f"{len(username):<{HEADER_LENGTH}}".encode('utf-8')
client_socket.send(username_header + username)
return True
# Sends a message to the server {: id="sends-a-message-to-the-server" }
def send(message):
# Encode message to bytes, prepare header and convert to bytes, like for username above, then send {: id="encode-message-to-bytes,-prepare-header-and-convert-to-bytes,-like-for-username-above,-then-send" }
message = message.encode('utf-8')
message_header = f"{len(message):<{HEADER_LENGTH}}".encode('utf-8')
client_socket.send(message_header + message)
# Starts listening function in a thread {: id="starts-listening-function-in-a-thread" }
# incoming_message_callback - callback to be called when new message arrives {: id="incoming-message-callback---callback-to-be-called-when-new-message-arrives" }
# error_callback - callback to be called on error {: id="error-callback---callback-to-be-called-on-error" }
def start_listening(incoming_message_callback, error_callback):
Thread(target=listen, args=(incoming_message_callback, error_callback), daemon=True).start()
# Listens for incomming messages {: id="listens-for-incomming-messages" }
def listen(incoming_message_callback, error_callback):
while True:
try:
# Now we want to loop over received messages (there might be more than one) and print them {: id="now-we-want-to-loop-over-received-messages-(there-might-be-more-than-one)-and-print-them" }
while True:
# Receive our "header" containing username length, it's size is defined and constant {: id="receive-our-"header"-containing-username-length,-it's-size-is-defined-and-constant" }
username_header = client_socket.recv(HEADER_LENGTH)
# If we received no data, server gracefully closed a connection, for example using socket.close() or socket.shutdown(socket.SHUT_RDWR) {: id="if-we-received-no-data,-server-gracefully-closed-a-connection,-for-example-using-socket.close()-or-socket.shutdown(socket.shut-rdwr)" }
if not len(username_header):
error_callback('Connection closed by the server')
# Convert header to int value {: id="convert-header-to-int-value" }
username_length = int(username_header.decode('utf-8').strip())
# Receive and decode username {: id="receive-and-decode-username" }
username = client_socket.recv(username_length).decode('utf-8')
# Now do the same for message (as we received username, we received whole message, there's no need to check if it has any length) {: id="now-do-the-same-for-message-(as-we-received-username,-we-received-whole-message,-there's-no-need-to-check-if-it-has-any-length)" }
message_header = client_socket.recv(HEADER_LENGTH)
message_length = int(message_header.decode('utf-8').strip())
message = client_socket.recv(message_length).decode('utf-8')
# Print message {: id="print-message" }
incoming_message_callback(username, message)
except Exception as e:
# Any other exception - something happened, exit {: id="any-other-exception---something-happened,-exit" }
error_callback('Reading error: {}'.format(str(e)))
socketserver.py
"""
Copied directly. Exact code from chatapp in sockets.
"""
import socket
import select
HEADER_LENGTH = 10
IP = "127.0.0.1"
PORT = 1234
# Create a socket {: id="create-a-socket" }
# socket.AF_INET - address family, IPv4, some otehr possible are AF_INET6, AF_BLUETOOTH, AF_UNIX {: id="socket.af-inet---address-family,-ipv4,-some-otehr-possible-are-af-inet6,-af-bluetooth,-af-unix" }
# socket.SOCK_STREAM - TCP, conection-based, socket.SOCK_DGRAM - UDP, connectionless, datagrams, socket.SOCK_RAW - raw IP packets {: id="socket.sock-stream---tcp,-conection-based,-socket.sock-dgram---udp,-connectionless,-datagrams,-socket.sock-raw---raw-ip-packets" }
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# SO_ - socket option {: id="so----socket-option" }
# SOL_ - socket option level {: id="sol----socket-option-level" }
# Sets REUSEADDR (as a socket option) to 1 on socket {: id="sets-reuseaddr-(as-a-socket-option)-to-1-on-socket" }
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Bind, so server informs operating system that it's going to use given IP and port {: id="bind,-so-server-informs-operating-system-that-it's-going-to-use-given-ip-and-port" }
# For a server using 0.0.0.0 means to listen on all available interfaces, useful to connect locally to 127.0.0.1 and remotely to LAN interface IP {: id="for-a-server-using-0.0.0.0-means-to-listen-on-all-available-interfaces,-useful-to-connect-locally-to-127.0.0.1-and-remotely-to-lan-interface-ip" }
server_socket.bind((IP, PORT))
# This makes server listen to new connections {: id="this-makes-server-listen-to-new-connections" }
server_socket.listen()
# List of sockets for select.select() {: id="list-of-sockets-for-select.select()" }
sockets_list = [server_socket]
# List of connected clients - socket as a key, user header and name as data {: id="list-of-connected-clients---socket-as-a-key,-user-header-and-name-as-data" }
clients = {}
print(f'Listening for connections on {IP}:{PORT}...')
# Handles message receiving {: id="handles-message-receiving" }
def receive_message(client_socket):
try:
# Receive our "header" containing message length, it's size is defined and constant {: id="receive-our-"header"-containing-message-length,-it's-size-is-defined-and-constant" }
message_header = client_socket.recv(HEADER_LENGTH)
# If we received no data, client gracefully closed a connection, for example using socket.close() or socket.shutdown(socket.SHUT_RDWR) {: id="if-we-received-no-data,-client-gracefully-closed-a-connection,-for-example-using-socket.close()-or-socket.shutdown(socket.shut-rdwr)" }
if not len(message_header):
return False
# Convert header to int value {: id="convert-header-to-int-value" }
message_length = int(message_header.decode('utf-8').strip())
# Return an object of message header and message data {: id="return-an-object-of-message-header-and-message-data" }
return {'header': message_header, 'data': client_socket.recv(message_length)}
except:
# If we are here, client closed connection violently, for example by pressing ctrl+c on his script {: id="if-we-are-here,-client-closed-connection-violently,-for-example-by-pressing-ctrl+c-on-his-script" }
# or just lost his connection {: id="or-just-lost-his-connection" }
# socket.close() also invokes socket.shutdown(socket.SHUT_RDWR) what sends information about closing the socket (shutdown read/write) {: id="socket.close()-also-invokes-socket.shutdown(socket.shut-rdwr)-what-sends-information-about-closing-the-socket-(shutdown-read/write)" }
# and that's also a cause when we receive an empty message {: id="and-that's-also-a-cause-when-we-receive-an-empty-message" }
return False
while True:
# Calls Unix select() system call or Windows select() WinSock call with three parameters: {: id="calls-unix-select()-system-call-or-windows-select()-winsock-call-with-three-parameters:" }
# - rlist - sockets to be monitored for incoming data {: id="--rlist---sockets-to-be-monitored-for-incoming-data" }
# - wlist - sockets for data to be send to (checks if for example buffers are not full and socket is ready to send some data) {: id="--wlist---sockets-for-data-to-be-send-to-(checks-if-for-example-buffers-are-not-full-and-socket-is-ready-to-send-some-data)" }
# - xlist - sockets to be monitored for exceptions (we want to monitor all sockets for errors, so we can use rlist) {: id="--xlist---sockets-to-be-monitored-for-exceptions-(we-want-to-monitor-all-sockets-for-errors,-so-we-can-use-rlist)" }
# Returns lists: {: id="returns-lists:" }
# - reading - sockets we received some data on (that way we don't have to check sockets manually) {: id="--reading---sockets-we-received-some-data-on-(that-way-we-don't-have-to-check-sockets-manually)" }
# - writing - sockets ready for data to be send thru them {: id="--writing---sockets-ready-for-data-to-be-send-thru-them" }
# - errors - sockets with some exceptions {: id="--errors----sockets-with-some-exceptions" }
# This is a blocking call, code execution will "wait" here and "get" notified in case any action should be taken {: id="this-is-a-blocking-call,-code-execution-will-"wait"-here-and-"get"-notified-in-case-any-action-should-be-taken" }
read_sockets, _, exception_sockets = select.select(sockets_list, [], sockets_list)
# Iterate over notified sockets {: id="iterate-over-notified-sockets" }
for notified_socket in read_sockets:
# If notified socket is a server socket - new connection, accept it {: id="if-notified-socket-is-a-server-socket---new-connection,-accept-it" }
if notified_socket == server_socket:
# Accept new connection {: id="accept-new-connection" }
# That gives us new socket - client socket, connected to this given client only, it's unique for that client {: id="that-gives-us-new-socket---client-socket,-connected-to-this-given-client-only,-it's-unique-for-that-client" }
# The other returned object is ip/port set {: id="the-other-returned-object-is-ip/port-set" }
client_socket, client_address = server_socket.accept()
# Client should send his name right away, receive it {: id="client-should-send-his-name-right-away,-receive-it" }
user = receive_message(client_socket)
# If False - client disconnected before he sent his name {: id="if-false---client-disconnected-before-he-sent-his-name" }
if user is False:
continue
# Add accepted socket to select.select() list {: id="add-accepted-socket-to-select.select()-list" }
sockets_list.append(client_socket)
# Also save username and username header {: id="also-save-username-and-username-header" }
clients[client_socket] = user
print('Accepted new connection from {}:{}, username: {}'.format(*client_address, user['data'].decode('utf-8')))
# Else existing socket is sending a message {: id="else-existing-socket-is-sending-a-message" }
else:
# Receive message {: id="receive-message" }
message = receive_message(notified_socket)
# If False, client disconnected, cleanup {: id="if-false,-client-disconnected,-cleanup" }
if message is False:
print('Closed connection from: {}'.format(clients[notified_socket]['data'].decode('utf-8')))
# Remove from list for socket.socket() {: id="remove-from-list-for-socket.socket()" }
sockets_list.remove(notified_socket)
# Remove from our list of users {: id="remove-from-our-list-of-users" }
del clients[notified_socket]
continue
# Get user by notified socket, so we will know who sent the message {: id="get-user-by-notified-socket,-so-we-will-know-who-sent-the-message" }
user = clients[notified_socket]
print(f'Received message from {user["data"].decode("utf-8")}: {message["data"].decode("utf-8")}')
# Iterate over connected clients and broadcast message {: id="iterate-over-connected-clients-and-broadcast-message" }
for client_socket in clients:
# But don't sent it to sender {: id="but-don't-sent-it-to-sender" }
if client_socket != notified_socket:
# Send user and message (both with their headers) {: id="send-user-and-message-(both-with-their-headers)" }
# We are reusing here message header sent by sender, and saved username header send by user when he connected {: id="we-are-reusing-here-message-header-sent-by-sender,-and-saved-username-header-send-by-user-when-he-connected" }
client_socket.send(user['header'] + user['data'] + message['header'] + message['data'])
# It's not really necessary to have this, but will handle some socket exceptions just in case {: id="it's-not-really-necessary-to-have-this,-but-will-handle-some-socket-exceptions-just-in-case" }
for notified_socket in exception_sockets:
# Remove from list for socket.socket() {: id="remove-from-list-for-socket.socket()" }
sockets_list.remove(notified_socket)
# Remove from our list of users {: id="remove-from-our-list-of-users" }
del clients[notified_socket]