diff --git a/nuqql_slixmppd/slixmppd.py b/nuqql_slixmppd/client.py old mode 100755 new mode 100644 similarity index 59% rename from nuqql_slixmppd/slixmppd.py rename to nuqql_slixmppd/client.py index c2da1bf..6e8efbb --- a/nuqql_slixmppd/slixmppd.py +++ b/nuqql_slixmppd/client.py @@ -1,33 +1,24 @@ -#!/usr/bin/env python3 - """ -slixmppd +slixmppd backend client """ import time -import asyncio -import html -import re import unicodedata -import logging -import stat -import os from typing import TYPE_CHECKING, Dict, List, Optional, Tuple -from threading import Thread, Lock, Event +from threading import Lock # slixmpp from slixmpp import ClientXMPP # type: ignore # from slixmpp.exceptions import IqError, IqTimeout # nuqql-based -from nuqql_based.based import Based from nuqql_based.callback import Callback from nuqql_based.message import Message if TYPE_CHECKING: # imports for typing + # TODO: move Lock here? from nuqql_based.account import Account - from nuqql_based.config import Config # slixmppd version VERSION = "0.4" @@ -391,286 +382,3 @@ def _chat_invite(self, chat: str, user: str) -> None: """ self.plugin['xep_0045'].invite(chat, user) - - -class BackendServer: - """ - Backend server class, manages the BackendClients for connections to - IM networks - """ - - def __init__(self) -> None: - self.connections: Dict[int, BackendClient] = {} - self.threads: Dict[int, Tuple[Thread, Event]] = {} - - # register callbacks - callbacks = [ - # based events - (Callback.BASED_CONFIG, self._based_config), - (Callback.BASED_INTERRUPT, self._based_interrupt), - (Callback.BASED_QUIT, self._based_quit), - - # nuqql messages - (Callback.QUIT, self.stop_thread), - (Callback.ADD_ACCOUNT, self.add_account), - (Callback.DEL_ACCOUNT, self.del_account), - (Callback.SEND_MESSAGE, self.send_message), - (Callback.SET_STATUS, self.enqueue), - (Callback.GET_STATUS, self.enqueue), - (Callback.CHAT_LIST, self.enqueue), - (Callback.CHAT_JOIN, self.enqueue), - (Callback.CHAT_PART, self.enqueue), - (Callback.CHAT_SEND, self.chat_send), - (Callback.CHAT_USERS, self.enqueue), - (Callback.CHAT_INVITE, self.enqueue), - ] - - # start based - self.based = Based("slixmppd", VERSION, callbacks) - - def start(self) -> None: - """ - Start server - """ - - self.based.start() - - def enqueue(self, account_id: int, cmd: Callback, params: Tuple) -> str: - """ - Helper for adding commands to the command queue of the account/client - """ - - try: - xmpp = self.connections[account_id] - except KeyError: - # no active connection - return "" - - xmpp.enqueue_command(cmd, params) - - return "" - - def send_message(self, account_id: int, cmd: Callback, - params: Tuple) -> str: - """ - send a message to a jabber id on an account - """ - - # parse parameters - if len(params) > 2: - dest, msg, msg_type = params - else: - dest, msg = params - msg_type = "chat" - - # nuqql sends a html-escaped message; construct "plain-text" version - # and xhtml version using nuqql's message and use them as message body - # later - html_msg = \ - '{}'.format(msg) - msg = html.unescape(msg) - msg = "\n".join(re.split("
", msg, flags=re.IGNORECASE)) - - # send message - self.enqueue(account_id, cmd, (dest, msg, html_msg, msg_type)) - - return "" - - def chat_send(self, account_id: int, _cmd: Callback, params: Tuple) -> str: - """ - Send message to chat on account - """ - - chat, msg = params - return self.send_message(account_id, Callback.SEND_MESSAGE, - (chat, msg, "groupchat")) - - @staticmethod - def _reconnect(xmpp, last_connect: float) -> float: - """ - Try to reconnect to the server if last connect is older than 10 - seconds. - """ - - cur_time = time.time() - if cur_time - last_connect < 10: - return last_connect - - print("Reconnecting:", xmpp.account.user) - xmpp.connect() - return cur_time - - def run_client(self, account: "Account", ready: Event, - running: Event) -> None: - """ - Run client connection in a new thread, - as long as running Event is set to true. - """ - - # get event loop for thread - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - # create a new lock for the thread - lock = Lock() - - # start client connection - xmpp = BackendClient(account, lock) - xmpp.register_plugin('xep_0071') # XHTML-IM - xmpp.register_plugin('xep_0082') # XMPP Date and Time Profiles - xmpp.register_plugin('xep_0203') # Delayed Delivery, time stamps - xmpp.register_plugin('xep_0030') # Service Discovery - xmpp.register_plugin('xep_0045') # Multi-User Chat - xmpp.register_plugin('xep_0199') # XMPP Ping - xmpp.connect() - last_connect = time.time() - - # save client connection in active connections dictionary - self.connections[account.aid] = xmpp - - # thread is ready to enter main loop, inform caller - ready.set() - - # enter main loop, and keep running until "running" is set to false - # by the KeyboardInterrupt - while running.is_set(): - # process xmpp client for 0.1 seconds, then send pending outgoing - # messages and update the (safe copy of the) buddy list - xmpp.process(timeout=0.1) - # if account is offline, skip other steps to avoid issues with - # sending commands/messages over the (uninitialized) xmpp - # connection - if xmpp.account.status == "offline": - last_connect = self._reconnect(xmpp, last_connect) - continue - xmpp.handle_queue() - xmpp.update_buddies() - - def add_account(self, account_id: int, _cmd: Callback, - params: Tuple) -> str: - """ - Add a new account (from based) and run a new slixmpp client thread for - it - """ - - # only handle xmpp accounts - account = params[0] - if account.type != "xmpp": - return "" - - # make sure other loggers do not also write to root logger - account.logger.propagate = False - - # event to signal thread is ready - ready = Event() - - # event to signal if thread should stop - running = Event() - running.set() - - # create and start thread - new_thread = Thread(target=self.run_client, args=(account, ready, - running)) - new_thread.start() - - # save thread in active threads dictionary - self.threads[account_id] = (new_thread, running) - - # wait until thread initialized everything - ready.wait() - - return "" - - def del_account(self, account_id: int, _cmd: Callback, - _params: Tuple) -> str: - """ - Delete an existing account (in based) and - stop slixmpp client thread for it - """ - - # stop thread - thread, running = self.threads[account_id] - running.clear() - thread.join() - - # cleanup - del self.connections[account_id] - del self.threads[account_id] - - return "" - - @staticmethod - def init_logging(config: "Config") -> None: - """ - Configure logging module, so slixmpp logs are written to a file - """ - - # determine logging path from command line parameters and - # make sure it exists - logs_dir = config.get_dir() / "logs" - logs_dir.mkdir(parents=True, exist_ok=True) - log_file = logs_dir / "slixmpp.log" - - # configure logging module to write to file - log_format = "%(asctime)s %(levelname)-5.5s [%(name)s] %(message)s" - loglevel = config.get_loglevel() - logging.basicConfig(filename=log_file, level=loglevel, - format=log_format, datefmt="%s") - os.chmod(log_file, stat.S_IRWXU) - - def stop_thread(self, account_id: int, _cmd: Callback, - _params: Tuple) -> str: - """ - Quit backend/stop client thread - """ - - # stop thread - print("Signalling account thread to stop.") - _thread, running = self.threads[account_id] - running.clear() - return "" - - def _based_config(self, _account_id: int, _cmd: Callback, - params: Tuple) -> str: - """ - Config event in based - """ - - config = params[0] - self.init_logging(config) - return "" - - def _based_interrupt(self, _account_id: int, _cmd: Callback, - _params: Tuple) -> str: - """ - KeyboardInterrupt event in based - """ - - for _thread, running in self.threads.values(): - print("Signalling account thread to stop.") - running.clear() - return "" - - def _based_quit(self, _account_id: int, _cmd: Callback, - _params: Tuple) -> str: - """ - Based shut down event - """ - - print("Waiting for all threads to finish. This might take a while.") - for thread, _running in self.threads.values(): - thread.join() - return "" - - -def main() -> None: - """ - Main function, initialize everything and start server - """ - - server = BackendServer() - server.start() - - -if __name__ == '__main__': - main() diff --git a/nuqql_slixmppd/main.py b/nuqql_slixmppd/main.py new file mode 100755 index 0000000..cac11ea --- /dev/null +++ b/nuqql_slixmppd/main.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +""" +slixmppd main entry point +""" + +# slixmppd +from nuqql_slixmppd.server import BackendServer + + +def main() -> None: + """ + Main function, initialize everything and start server + """ + + server = BackendServer() + server.start() + + +if __name__ == '__main__': + main() diff --git a/nuqql_slixmppd/server.py b/nuqql_slixmppd/server.py new file mode 100644 index 0000000..1a36c2e --- /dev/null +++ b/nuqql_slixmppd/server.py @@ -0,0 +1,298 @@ +""" +slixmppd backend server +""" + +import time +import asyncio +import html +import re +import logging +import stat +import os + +from typing import TYPE_CHECKING, Dict, Tuple +from threading import Thread, Lock, Event + +# slixmppd +from nuqql_slixmppd.client import BackendClient + +# nuqql-based +from nuqql_based.based import Based +from nuqql_based.callback import Callback + +if TYPE_CHECKING: # imports for typing + from nuqql_based.account import Account + from nuqql_based.config import Config + +# slixmppd version +VERSION = "0.4" + + +class BackendServer: + """ + Backend server class, manages the BackendClients for connections to + IM networks + """ + + def __init__(self) -> None: + self.connections: Dict[int, BackendClient] = {} + self.threads: Dict[int, Tuple[Thread, Event]] = {} + + # register callbacks + callbacks = [ + # based events + (Callback.BASED_CONFIG, self._based_config), + (Callback.BASED_INTERRUPT, self._based_interrupt), + (Callback.BASED_QUIT, self._based_quit), + + # nuqql messages + (Callback.QUIT, self.stop_thread), + (Callback.ADD_ACCOUNT, self.add_account), + (Callback.DEL_ACCOUNT, self.del_account), + (Callback.SEND_MESSAGE, self.send_message), + (Callback.SET_STATUS, self.enqueue), + (Callback.GET_STATUS, self.enqueue), + (Callback.CHAT_LIST, self.enqueue), + (Callback.CHAT_JOIN, self.enqueue), + (Callback.CHAT_PART, self.enqueue), + (Callback.CHAT_SEND, self.chat_send), + (Callback.CHAT_USERS, self.enqueue), + (Callback.CHAT_INVITE, self.enqueue), + ] + + # start based + self.based = Based("slixmppd", VERSION, callbacks) + + def start(self) -> None: + """ + Start server + """ + + self.based.start() + + def enqueue(self, account_id: int, cmd: Callback, params: Tuple) -> str: + """ + Helper for adding commands to the command queue of the account/client + """ + + try: + xmpp = self.connections[account_id] + except KeyError: + # no active connection + return "" + + xmpp.enqueue_command(cmd, params) + + return "" + + def send_message(self, account_id: int, cmd: Callback, + params: Tuple) -> str: + """ + send a message to a jabber id on an account + """ + + # parse parameters + if len(params) > 2: + dest, msg, msg_type = params + else: + dest, msg = params + msg_type = "chat" + + # nuqql sends a html-escaped message; construct "plain-text" version + # and xhtml version using nuqql's message and use them as message body + # later + html_msg = \ + '{}'.format(msg) + msg = html.unescape(msg) + msg = "\n".join(re.split("
", msg, flags=re.IGNORECASE)) + + # send message + self.enqueue(account_id, cmd, (dest, msg, html_msg, msg_type)) + + return "" + + def chat_send(self, account_id: int, _cmd: Callback, params: Tuple) -> str: + """ + Send message to chat on account + """ + + chat, msg = params + return self.send_message(account_id, Callback.SEND_MESSAGE, + (chat, msg, "groupchat")) + + @staticmethod + def _reconnect(xmpp, last_connect: float) -> float: + """ + Try to reconnect to the server if last connect is older than 10 + seconds. + """ + + cur_time = time.time() + if cur_time - last_connect < 10: + return last_connect + + print("Reconnecting:", xmpp.account.user) + xmpp.connect() + return cur_time + + def run_client(self, account: "Account", ready: Event, + running: Event) -> None: + """ + Run client connection in a new thread, + as long as running Event is set to true. + """ + + # get event loop for thread + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # create a new lock for the thread + lock = Lock() + + # start client connection + xmpp = BackendClient(account, lock) + xmpp.register_plugin('xep_0071') # XHTML-IM + xmpp.register_plugin('xep_0082') # XMPP Date and Time Profiles + xmpp.register_plugin('xep_0203') # Delayed Delivery, time stamps + xmpp.register_plugin('xep_0030') # Service Discovery + xmpp.register_plugin('xep_0045') # Multi-User Chat + xmpp.register_plugin('xep_0199') # XMPP Ping + xmpp.connect() + last_connect = time.time() + + # save client connection in active connections dictionary + self.connections[account.aid] = xmpp + + # thread is ready to enter main loop, inform caller + ready.set() + + # enter main loop, and keep running until "running" is set to false + # by the KeyboardInterrupt + while running.is_set(): + # process xmpp client for 0.1 seconds, then send pending outgoing + # messages and update the (safe copy of the) buddy list + xmpp.process(timeout=0.1) + # if account is offline, skip other steps to avoid issues with + # sending commands/messages over the (uninitialized) xmpp + # connection + if xmpp.account.status == "offline": + last_connect = self._reconnect(xmpp, last_connect) + continue + xmpp.handle_queue() + xmpp.update_buddies() + + def add_account(self, account_id: int, _cmd: Callback, + params: Tuple) -> str: + """ + Add a new account (from based) and run a new slixmpp client thread for + it + """ + + # only handle xmpp accounts + account = params[0] + if account.type != "xmpp": + return "" + + # make sure other loggers do not also write to root logger + account.logger.propagate = False + + # event to signal thread is ready + ready = Event() + + # event to signal if thread should stop + running = Event() + running.set() + + # create and start thread + new_thread = Thread(target=self.run_client, args=(account, ready, + running)) + new_thread.start() + + # save thread in active threads dictionary + self.threads[account_id] = (new_thread, running) + + # wait until thread initialized everything + ready.wait() + + return "" + + def del_account(self, account_id: int, _cmd: Callback, + _params: Tuple) -> str: + """ + Delete an existing account (in based) and + stop slixmpp client thread for it + """ + + # stop thread + thread, running = self.threads[account_id] + running.clear() + thread.join() + + # cleanup + del self.connections[account_id] + del self.threads[account_id] + + return "" + + @staticmethod + def init_logging(config: "Config") -> None: + """ + Configure logging module, so slixmpp logs are written to a file + """ + + # determine logging path from command line parameters and + # make sure it exists + logs_dir = config.get_dir() / "logs" + logs_dir.mkdir(parents=True, exist_ok=True) + log_file = logs_dir / "slixmpp.log" + + # configure logging module to write to file + log_format = "%(asctime)s %(levelname)-5.5s [%(name)s] %(message)s" + loglevel = config.get_loglevel() + logging.basicConfig(filename=log_file, level=loglevel, + format=log_format, datefmt="%s") + os.chmod(log_file, stat.S_IRWXU) + + def stop_thread(self, account_id: int, _cmd: Callback, + _params: Tuple) -> str: + """ + Quit backend/stop client thread + """ + + # stop thread + print("Signalling account thread to stop.") + _thread, running = self.threads[account_id] + running.clear() + return "" + + def _based_config(self, _account_id: int, _cmd: Callback, + params: Tuple) -> str: + """ + Config event in based + """ + + config = params[0] + self.init_logging(config) + return "" + + def _based_interrupt(self, _account_id: int, _cmd: Callback, + _params: Tuple) -> str: + """ + KeyboardInterrupt event in based + """ + + for _thread, running in self.threads.values(): + print("Signalling account thread to stop.") + running.clear() + return "" + + def _based_quit(self, _account_id: int, _cmd: Callback, + _params: Tuple) -> str: + """ + Based shut down event + """ + + print("Waiting for all threads to finish. This might take a while.") + for thread, _running in self.threads.values(): + thread.join() + return "" diff --git a/setup.py b/setup.py index c94cd40..362b42b 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ url="https://github.com/hwipl/nuqql-slixmppd", packages=["nuqql_slixmppd"], entry_points={ - "console_scripts": ["nuqql-slixmppd = nuqql_slixmppd.slixmppd:main"] + "console_scripts": ["nuqql-slixmppd = nuqql_slixmppd.main:main"] }, classifiers=CLASSIFIERS, python_requires='>=3.6', diff --git a/slixmppd.py b/slixmppd.py index d4c3e65..fe01545 100755 --- a/slixmppd.py +++ b/slixmppd.py @@ -6,8 +6,8 @@ import sys -import nuqql_slixmppd.slixmppd +import nuqql_slixmppd.main import nuqql_slixmppd # start slixmppd -sys.exit(nuqql_slixmppd.slixmppd.main()) +sys.exit(nuqql_slixmppd.main.main())