From 259255720599b3cdba0564313bd459952e64313f Mon Sep 17 00:00:00 2001 From: Arterialist Date: Fri, 25 May 2018 19:11:06 +0300 Subject: [PATCH] added client base --- .gitignore | 104 +++++++++++++++ client/__init__.py | 0 client/client_base.py | 209 ++++++++++++++++++++++++++++++ client/layers.py | 18 +++ client/models/__init__.py | 0 client/models/actions.py | 31 +++++ client/models/base.py | 18 +++ client/models/messages.py | 31 +++++ client/models/packets.py | 17 +++ client/modules/__init__.py | 0 client/modules/default_modules.py | 17 +++ client/modules/module.py | 23 ++++ tools.py | 18 +++ 13 files changed, 486 insertions(+) create mode 100644 .gitignore create mode 100644 client/__init__.py create mode 100644 client/client_base.py create mode 100644 client/layers.py create mode 100644 client/models/__init__.py create mode 100755 client/models/actions.py create mode 100755 client/models/base.py create mode 100644 client/models/messages.py create mode 100644 client/models/packets.py create mode 100644 client/modules/__init__.py create mode 100644 client/modules/default_modules.py create mode 100644 client/modules/module.py create mode 100644 tools.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..894a44c --- /dev/null +++ b/.gitignore @@ -0,0 +1,104 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/client/__init__.py b/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/client/client_base.py b/client/client_base.py new file mode 100644 index 0000000..6a2d885 --- /dev/null +++ b/client/client_base.py @@ -0,0 +1,209 @@ +import socket +import threading +from json import JSONDecodeError + +from client import layers +from client.models.packets import Packet +from client.modules.default_modules import SendAsJSONModule + +# load modules +loaded_modules = [SendAsJSONModule()] + +nickname = None +local_port = None + +sock = None +server_host = None +server_port = None + +connected = False +listening = False + +has_incoming_connection = False +incoming_connection = None +incoming_connection_address = None + +current_connection = None +current_connection_address = None +current_peer_id = None + +incoming_message_thread = None +incoming_connection_thread = None +message_sending_thread = None + +incoming_connection_callback = None +new_message_callback = None + + +def init_socket(): + global sock, connected + connected = False + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + +def incoming_connections_listener(): + global has_incoming_connection, incoming_connection, incoming_connection_address + while 1: + try: + connection, address = sock.accept() + incoming_connection = connection + incoming_connection_address = address + has_incoming_connection = True + if incoming_connection_callback: + incoming_connection_callback() + except OSError: + continue + + +def p2p_new_message_listener(): + global current_connection, connected + while connected: + data = current_connection.recv(4096) + if data == b'': + print('Connection closed') + connected = False + break + try: + recv_msg = Packet.from_json_obj(layers.socket_handle_received(current_connection, data.decode('utf8'), loaded_modules)) + recv_msg.message.mine = False + if new_message_callback: + new_message_callback(recv_msg) + print(recv_msg.__dict__) + except UnicodeDecodeError: + print('Corrupted message') + except JSONDecodeError: + print('Received non-JSON message (raw connection?)') + except KeyError: + print("Invalid JSON schema") + + +def server_new_message_listener(): + global current_connection + while connected: + data = current_connection.recv(4096) + try: + if new_message_callback: + new_message_callback('\n' + data.decode('utf8')) + print('\n' + data.decode('utf8')) + except UnicodeDecodeError: + print('Corrupted message') + + +def socket_listen_off(): + global listening + sock.close() + if incoming_connection_thread: + incoming_connection_thread.join(0) + init_socket() + listening = False + + +def socket_listen_on(): + global sock, incoming_connection_thread, listening + socket_listen_off() + + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('', local_port)) + except socket.error as e: + print('Bind failed.') + print(e) + return + + sock.listen(256) + + incoming_connection_thread = threading.Thread(target=incoming_connections_listener) + incoming_connection_thread.setDaemon(True) + incoming_connection_thread.start() + listening = True + + +def p2p_connect(remote_host, remote_port): + global current_connection, current_connection_address, incoming_message_thread, connected + socket_listen_off() + + current_connection_address = (remote_host, remote_port) + current_connection = sock + try: + sock.connect(current_connection_address) + except ConnectionRefusedError: + print('Client is offline') + return + + connected = True + incoming_message_thread = threading.Thread(target=p2p_new_message_listener) + incoming_message_thread.setDaemon(True) + incoming_message_thread.start() + print('Connected to client') + + +def server_connect(): + global current_connection_address, current_connection, incoming_message_thread, connected + socket_listen_off() + + current_connection_address = (server_host, server_port) + current_connection = sock + sock.connect(current_connection_address) + + connected = True + incoming_message_thread = threading.Thread(target=server_new_message_listener) + incoming_message_thread.setDaemon(True) + incoming_message_thread.start() + print('Connected to server') + + +def accept_connection(): + global current_connection, current_connection_address, connected, incoming_message_thread + current_connection = incoming_connection + current_connection_address = incoming_connection_address + connected = True + incoming_message_thread = threading.Thread(target=p2p_new_message_listener) + incoming_message_thread.setDaemon(True) + incoming_message_thread.start() + + +def decline_connection(): + global incoming_connection, incoming_connection_address + incoming_connection.close() + incoming_connection = None + incoming_connection_address = None + + +def send_message(message): + # pass your modules here + layers.socket_send_data(current_connection, message, loaded_modules) + + +def disconnect(): + global current_connection, incoming_message_thread, incoming_connection_thread, connected, current_peer_id + connected = False + + current_peer_id = None + + if current_connection: + current_connection.detach() + current_connection.close() + + if incoming_message_thread: + incoming_message_thread.join(0) + if incoming_connection_thread: + incoming_connection_thread.join(0) + + +def finish(): + global sock, current_connection, incoming_message_thread, incoming_connection_thread, connected + + connected = False + sock.detach() + sock.close() + + if current_connection: + current_connection.detach() + current_connection.close() + + if incoming_message_thread: + incoming_message_thread.join(0) + if incoming_connection_thread: + incoming_connection_thread.join(0) + + exit(0) diff --git a/client/layers.py b/client/layers.py new file mode 100644 index 0000000..4a7d84b --- /dev/null +++ b/client/layers.py @@ -0,0 +1,18 @@ +""" +this file is created to make functionality growth fast +""" + + +def socket_send_data(to, what, through=list()): + for action in through: + what = action.process(what) + + if to: + to.sendall(what) + + +def socket_handle_received(from_s, what, through=list()): + for action in through: + what = action.process_s(what, from_s) + + return what diff --git a/client/models/__init__.py b/client/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/client/models/actions.py b/client/models/actions.py new file mode 100755 index 0000000..a4f692e --- /dev/null +++ b/client/models/actions.py @@ -0,0 +1,31 @@ +from client.models.base import Jsonable + + +class Action(Jsonable): + def __init__(self, action="new"): + self.action = action + + +class NewMessageAction(Action): + def __init__(self): + super().__init__(action="new") + + +class EditMessageAction(Action): + def __init__(self): + super().__init__(action="edit") + + +class ReplyMessageAction(Action): + def __init__(self): + super().__init__(action="reply") + + +class ForwardMessageAction(Action): + def __init__(self): + super().__init__(action="forward") + + +class DeleteMessageAction(Action): + def __init__(self): + super().__init__(action="delete") diff --git a/client/models/base.py b/client/models/base.py new file mode 100755 index 0000000..b1644c8 --- /dev/null +++ b/client/models/base.py @@ -0,0 +1,18 @@ +import json + + +class Jsonable: + @staticmethod + def from_json(json_string): + obj = Jsonable() + obj.__dict__ = json.loads(json_string) + return obj + + @staticmethod + def from_json_obj(json_obj): + obj = Jsonable() + obj.__dict__ = json_obj + return obj + + def to_json(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True) diff --git a/client/models/messages.py b/client/models/messages.py new file mode 100644 index 0000000..06c1d30 --- /dev/null +++ b/client/models/messages.py @@ -0,0 +1,31 @@ +import hashlib +import uuid + +from client.models.base import Jsonable +from tools import current_time + + +class Message(Jsonable): + def __init__(self, message_id=None, timestamp=None, text=None, attachments=None, mine=False): + self.message_id = message_id if message_id else hashlib.md5(str(uuid.uuid4()).encode('utf-8')).hexdigest() + self.timestamp = timestamp if timestamp else current_time() + self.text = text + self.attachments = attachments + self.mine = mine + + +class Attachment(Jsonable): + def __init__(self, link=None): + self.link = link + + +class Photo(Attachment): + def __init__(self, link=None, image_format=None): + super().__init__(link) + self.image_format = image_format + + +class Audio(Attachment): + def __init__(self, link, duration=None): + super().__init__(link) + self.duration = duration diff --git a/client/models/packets.py b/client/models/packets.py new file mode 100644 index 0000000..7164c98 --- /dev/null +++ b/client/models/packets.py @@ -0,0 +1,17 @@ +from client.models.actions import Action +from client.models.base import Jsonable +from client.models.messages import Message + + +class Packet(Jsonable): + def __init__(self, action=None, message=None): + self.action = action + self.message = message + + @staticmethod + def from_json_obj(json_obj): + return Packet( + action=Action.from_json_obj( + json_obj=json_obj["action"]), + message=Message.from_json_obj( + json_obj=json_obj["message"])) diff --git a/client/modules/__init__.py b/client/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/client/modules/default_modules.py b/client/modules/default_modules.py new file mode 100644 index 0000000..4092062 --- /dev/null +++ b/client/modules/default_modules.py @@ -0,0 +1,17 @@ +from .module import BaseModule +import json + + +class SendAsJSONModule(BaseModule): + def __init__(self): + super().__init__() + + def process(self, data): + super().process(data) + + return data.to_json().encode('utf8') + + def process_s(self, data, sock): + super().process_s(data, sock) + + return json.loads(data) diff --git a/client/modules/module.py b/client/modules/module.py new file mode 100644 index 0000000..9c5a1c3 --- /dev/null +++ b/client/modules/module.py @@ -0,0 +1,23 @@ +class BaseModule: + def __init__(self): + print("Module {} loaded".format(self.__class__.__name__)) + self.enabled = True + + # method for modifications of data before sending + def process(self, data): + if self.enabled: + # do action + pass + + # method for modifications of data after receiving + def process_s(self, data, sock): + if self.enabled: + # do action + pass + + def enable(self): + self.enabled = True + + def disable(self): + self.enabled = False + diff --git a/tools.py b/tools.py new file mode 100644 index 0000000..f9a3d18 --- /dev/null +++ b/tools.py @@ -0,0 +1,18 @@ +import time + + +def full_strip(string): + while 1: + if len(string): + if string[0] in [' ', '\n'] or string[-1] in [' ', '\n']: + string = string.strip('\n') + string = string.strip(' ') + else: + break + else: + break + return string + + +def current_time(): + return int(round(time.time() * 1000))