From 95d90e807bb5667115bb3fdbf4c139da71ab2de7 Mon Sep 17 00:00:00 2001 From: khushal Date: Mon, 10 Feb 2025 19:34:52 +0530 Subject: [PATCH] (20250210) Worked on the IP addr util a bit. --- controllers_v2/core/software.py | 477 ++++++++++++++++++ controllers_v2/software/__init__.py | 0 controllers_v2/software/mikrotik/__init__.py | 0 .../software/mikrotik/all_mikrotik.py | 272 ++++++++++ controllers_v2/software/mikrotik/base.py | 256 ++++++++++ .../software/mikrotik/mikrotik_pppoe_1000.py | 203 ++++++++ models/software/__init__.py | 0 models/software/mikrotik/__init__.py | 0 models/software/mikrotik/auth.py | 142 ++++++ models/software/mikrotik/configure.py | 312 ++++++++++++ models/software/tcaoff_ai/__init__.py | 0 models/software/tcaoff_ai/auth.py | 150 ++++++ readme/MikroTik-PPPoE-1000.md | 29 ++ utils_v2/network/ip.py | 55 ++ utils_v2/rest/controllers/async_base.py | 32 +- 15 files changed, 1918 insertions(+), 10 deletions(-) create mode 100644 controllers_v2/core/software.py create mode 100644 controllers_v2/software/__init__.py create mode 100644 controllers_v2/software/mikrotik/__init__.py create mode 100644 controllers_v2/software/mikrotik/all_mikrotik.py create mode 100644 controllers_v2/software/mikrotik/base.py create mode 100644 controllers_v2/software/mikrotik/mikrotik_pppoe_1000.py create mode 100644 models/software/__init__.py create mode 100644 models/software/mikrotik/__init__.py create mode 100644 models/software/mikrotik/auth.py create mode 100644 models/software/mikrotik/configure.py create mode 100644 models/software/tcaoff_ai/__init__.py create mode 100644 models/software/tcaoff_ai/auth.py create mode 100644 readme/MikroTik-PPPoE-1000.md diff --git a/controllers_v2/core/software.py b/controllers_v2/core/software.py new file mode 100644 index 0000000..f2d44fa --- /dev/null +++ b/controllers_v2/core/software.py @@ -0,0 +1,477 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Thursday, 19th Dec., 2024 + + OBJECTIVE: + + To handle all messages from one place. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# My async utils: +from utils_v2.database.async_mongo_v2 import AsyncMongo +from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache + +# Controllers: +from controllers_v2.core.base import CoreBaseModel +from controllers_v2.core.auth_token import CoreAuthTokenController + +# Models: +from models.core.message import CoreMessageModel + +# To work with MongoDB: +from bson import ObjectId + +# To work with datatypes: +from typing import List + +# To make HTTP requests: +import httpx + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class CoreMessageController(CoreAuthTokenController): + + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + # For MongoDB: + MESSAGES_COLLECTION = "_messages" + + # ┏┓ + # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ + # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ + + def __init__( + self, + cache: AsyncRedisCache = None, + http_client: httpx.AsyncClient = None, + alert_url: str = None, + base_filter: dict = None, + debug: bool = True, + debug_prefix: str = "Message (C) | ", + debug_only_errors: bool = True + ): + + """ + This is the foundational controller of all message controllers. You must structure individual message + controllers through this structure. Individual message controllers would be for things like mails, SMS messages, + chat app messages, etc. + :param cache: The object to use for caching results from database calls. + :param http_client: The HTTP client + :param base_filter: The basic filter that will be applied to all fetching/updating queries. WARNING: THE BASE + FILTER WILL ALWAYS BE APPLIED AUTOMATICALLY. SET THIS UP WISELY. + :param debug: Whether, or not, you would like to print debugging messages: + :param debug_prefix: The prefix to print with the debugging messages. + :param debug_only_errors: Whether you would like to print only error messages or all messages. + :return: None. + """ + + # Accept the base filter: + self._base_filter = base_filter or {} + + # Invoke the parent's constructor: + CoreAuthTokenController.__init__( + self, + cache = cache, + http_client = http_client, + alert_url = alert_url, + base_filter = base_filter, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + # ┏┓┳┓┳┳┳┓ ┏┓ + # ┃ ┣┫┃┃┃┃ ━━ ┃ ┏┓┏┓┏┓╋┏┓ + # ┗┛┛┗┗┛┻┛ ┗┛┛ ┗ ┗┻┗┗ + + async def save_one_message( + self, + mongo_data_conn: AsyncMongo, + message: CoreMessageModel, + session = None + ) -> ObjectId: + + """ + Simply insert one message document into the database. + :param mongo_data_conn: The instance of the database connector to use for the operation. + :param message: The message to save into the database. + :param session: In case you need to perform this operation as a transaction, pass a session here. + :return: The object id of the inserted document. + """ + + # Simply insert the document: + return await mongo_data_conn.insert_one( + collection = self.MESSAGES_COLLECTION, + document = message.model_dump(), + raise_exception = True, + session = session + ) + + async def bulk_operate_messages( + self, + mongo_data_conn: AsyncMongo, + mongo_operations: list + ) -> int: + + """ + Needed in cases like forcing re-sync of mails where you need to perform actions like bulk replacements of + existing documents. Not recommended to use. Please use very carefully to ensure document integrity. + :param mongo_data_conn: The instance of the database connector to use for the operation. + :param mongo_operations: The list operations that are supported by MongoDB's Bulk Write system. + :return: The no. of documents affected. + """ + + return await mongo_data_conn.bulk_write( + collection = self.MESSAGES_COLLECTION, + requests = mongo_operations, + raise_exception = True + ) + + # ┏┓┳┓┳┳┳┓ ┳┓ • + # ┃ ┣┫┃┃┃┃ ━━ ┣┫┏┓╋┏┓┓┏┓┓┏┏┓ + # ┗┛┛┗┗┛┻┛ ┛┗┗ ┗┛ ┗┗ ┗┛┗ + + async def count_messages( + self, + mongo_data_conn: AsyncMongo, + token_ids: List[ObjectId | str] = None, + additional_filter: dict = None + ) -> int: + + """ + Just counts the no. of messages that match a given set of conditions. + :param mongo_data_conn: The instance of the database connector to use for the operation. + :param token_ids: The token ids of the accounts from which these messages must be fetched. + :param additional_filter: Any addition filters to use. + :return: The no. of messages that match the given conditions. + """ + + # We cannot allow counting without any filter whatsoever: + if token_ids is None and not additional_filter: + raise ValueError("Cannot operate without some filter.") + + # Prepare the filter: + filter_json = {} + if token_ids is not None: + if not isinstance(token_ids, list): token_ids = [token_ids] + filter_json["tokenId"] = {"$in": token_ids} + if self._base_filter: + for k, v in self._base_filter.items(): filter_json[k] = v + if additional_filter: + for k, v in additional_filter.items(): filter_json[k] = v + + # Get the count of the documents that match the criteria: + count = await mongo_data_conn.count( + collection = self.MESSAGES_COLLECTION, + filter = filter_json, + raise_exception = True + ) + + # Done here: + return count + + async def get_message_previews( + self, + mongo_data_conn: AsyncMongo, + token_ids: List[ObjectId | str] = None, + limit: int = 100, + skip: int = 0, + additional_filter: dict = None, + projection: dict = None + ) -> List[CoreMessageModel] | None: + + """ + Fetches many messages in one call, but leaves out the full payloads. + :param mongo_data_conn: The instance of the database connector to use for the operation. + :param token_ids: The token ids of the accounts from which these messages must be fetched. + :param limit: The max. no. of messages to retrieve in this call. + :param skip: The no. of initial messages to skip. Useful for pagination. + :param additional_filter: Any addition filters to use. + :param projection: To decide what is picked from each document. WARNING: THIS MAY BREAK THE BEHAVIOUR OF THE + CORE MESSAGE MODEL. USE CAREFULLY. + :return: The list of messages (as the message model). This list can be empty. + """ + + # We cannot allow counting without any filter whatsoever: + if token_ids is None and not additional_filter: + raise ValueError("Cannot operate without some filter.") + + # Prepare the filter: + filter_json = {} + if token_ids is not None: + if not isinstance(token_ids, list): token_ids = [token_ids] + filter_json["tokenId"] = {"$in": token_ids} + if self._base_filter: + for k, v in self._base_filter.items(): filter_json[k] = v + if additional_filter: + for k, v in additional_filter.items(): filter_json[k] = v + + # We fetch the messages that are identified by a specific token id, + # with the specified fetching limits, while enforcing the sorting condition: + records = await mongo_data_conn.find_many( + collection = self.MESSAGES_COLLECTION, + filter = filter_json, + limit = limit, + skip = skip, + sort = {"ts": -1}, + projection = projection or { + "_id": True, + "ts": True, + "syncTs": True, + "tokenId": True, + "serviceType": True, + "client": True, + "clientMessageId": True, + "clientThreadId": True, + "isSent": True, + "isBroadcast": True, + "sentSuccessfully": True, + "sender": True, + "chat": True, + "snippet": True, + "aiSnippet": True, + "tags": True + }, + raise_exception = True + ) + + # Convert the fetched records to instances of the data model and return: + for record in records: record["message"] = {} + return [CoreMessageModel(**record) for record in records] + + async def get_messages( + self, + mongo_data_conn: AsyncMongo, + token_ids: List[ObjectId | str] = None, + limit: int = 100, + skip: int = 0, + additional_filter: dict = None, + projection: dict = None + ) -> List[CoreMessageModel] | None: + + """ + Fetches many full messages in one call. + :param mongo_data_conn: The instance of the database connector to use for the operation. + :param token_ids: The token ids of the accounts from which these messages must be fetched. + :param limit: The max. no. of messages to retrieve in this call. + :param skip: The no. of initial messages to skip. Useful for pagination. + :param additional_filter: Any addition filters to use. + :param projection: To decide what is picked from each document. WARNING: THIS MAY BREAK THE BEHAVIOUR OF THE + CORE MESSAGE MODEL. USE CAREFULLY. + :return: The list of messages (as the message model). This list can be empty. + """ + + # We cannot allow counting without any filter whatsoever: + if token_ids is None and not additional_filter: + raise ValueError("Cannot operate without some filter.") + + # Prepare the filter: + filter_json = {} + if token_ids is not None: + if not isinstance(token_ids, list): token_ids = [token_ids] + filter_json["tokenId"] = {"$in": token_ids} + if self._base_filter: + for k, v in self._base_filter.items(): filter_json[k] = v + if additional_filter: + for k, v in additional_filter.items(): filter_json[k] = v + + # We fetch the messages that are identified by a specific token id, + # with the specified fetching limits, while enforcing the sorting condition: + records = await mongo_data_conn.find_many( + collection = self.MESSAGES_COLLECTION, + filter = filter_json, + limit = limit, + skip = skip, + sort = {"ts": -1}, + projection = projection, + raise_exception = True + ) + + # Convert the fetched records to instances of the data model and return: + return [CoreMessageModel(**record) for record in records] + + async def get_message( + self, + mongo_data_conn: AsyncMongo, + message_id: ObjectId | str, + additional_filter: dict = None, + projection: dict = None + ) -> CoreMessageModel | None: + + """ + Gets one message if you know its message id. + :param mongo_data_conn: The instance of the database connector to use for the operation. + :param message_id: The id of the message that needs to be read. + :param additional_filter: Any addition filters to use. + :param projection: To decide what is picked from each document. WARNING: THIS MAY BREAK THE BEHAVIOUR OF THE + CORE MESSAGE MODEL. USE CAREFULLY. + :return: The contents of that one message in a structured format. + """ + + # Start by preparing the filter: + filter_json = {"_id": ObjectId(message_id)} + if self._base_filter: + for k, v in self._base_filter.items(): filter_json[k] = v + if additional_filter: + for k, v in additional_filter.items(): filter_json[k] = v + + # We fetch the whole payload of that one message: + record = await mongo_data_conn.find_one( + collection = self.MESSAGES_COLLECTION, + filter = filter_json, + projection = projection, + raise_exception = True + ) + + # If no such message was found: + if record is None: return None + + # If a record was found, + # we return it as our data model: + return CoreMessageModel(**record) + + # ┏┓┳┓┳┳┳┓ ┳┳ ┓ + # ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┏┫┏┓╋┏┓ + # ┗┛┛┗┗┛┻┛ ┗┛┣┛┗┻┗┻┗┗ + # ┛ + + # We don't support updating messages themselves, + # but we will allow updating fields like tags, marking as read or unread, etc. + + async def update_message_tags( + self, + mongo_data_conn: AsyncMongo, + message_id: ObjectId | str, + unset_tags: List[str] = None, + set_tags: List[str] = None, + additional_filter: dict = None + ) -> bool: + + """ + Updates the tags on one message. The tags to remove are processed first, the ones to add are processed later. + :param mongo_data_conn: The instance of the database connector to use for the operation. + :param message_id: The id of the message that needs to be read. + :param unset_tags: The tags to remove from the message. + :param set_tags: The tags to add to the message. + :param additional_filter: Any addition filters to use. + :return: True if the update was successful, else False. + """ + + # Start by preparing the filter: + filter_json = {"_id": ObjectId(message_id)} + if self._base_filter: + for k, v in self._base_filter.items(): filter_json[k] = v + if additional_filter: + for k, v in additional_filter.items(): filter_json[k] = v + + # Update the tags: + return await mongo_data_conn.update_one( + collection = self.MESSAGES_COLLECTION, + filter = filter_json, + update = [{ + "$set": { + "tags": { + "$let": { + "vars": { + "removed_tags": { + "$setDifference": [ + "$tags", + unset_tags + ] + } + }, + "in": { + "$setUnion": [ + "$$removed_tags", + set_tags + ] + } + } + } + } + }], + raise_exception = True + ) + + # ┏┓┳┓┳┳┳┓ ┳┓ ┓ + # ┃ ┣┫┃┃┃┃ ━━ ┃┃┏┓┃┏┓╋┏┓ + # ┗┛┛┗┗┛┻┛ ┻┛┗ ┗┗ ┗┗ + + # No support whatsoever for deleting messages. + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/controllers_v2/software/__init__.py b/controllers_v2/software/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controllers_v2/software/mikrotik/__init__.py b/controllers_v2/software/mikrotik/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controllers_v2/software/mikrotik/all_mikrotik.py b/controllers_v2/software/mikrotik/all_mikrotik.py new file mode 100644 index 0000000..0af1e28 --- /dev/null +++ b/controllers_v2/software/mikrotik/all_mikrotik.py @@ -0,0 +1,272 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 10th Feb., 2025. + + OBJECTIVE: + + To handle all MikroTik configuration from one place. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# My async utils: +from utils_v2.database.async_mysql_v2 import AsyncMySQL +from utils_v2.database.async_mongo_v2 import AsyncMongo +from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache + +# Controllers: +from controllers_v2.core.software import CoreSoftwareController + +# To make very controlled API calls: +from utils_v2.rest.controllers.async_base import AsyncREST +from utils_v2.rest.models.api_call import ApiResponse + +# Models: +from models.software.mikrotik.auth import ( + MikroTikPPPoE1000Auth, + MikroTikHotspot1000Auth, + MikroTikAuthResponse +) + +# To work with datatypes: +from typing import List, Any + +# To make HTTP requests: +import httpx + +# to work with MongoDB: +from bson.objectid import ObjectId + +# To make abstract classes: +from abc import ABC, abstractmethod + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class MikroTikController(CoreSoftwareController, ABC): + + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + SERVICE_TYPE = "mikrotik" + + # ┏┓ + # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ + # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ + + def __init__( + self, + cache: AsyncRedisCache = None, + http_client: httpx.AsyncClient = None, + alert_url: str = None, + base_filter: dict = None, + debug: bool = True, + debug_prefix: str = "MikroTik (C) | ", + debug_only_errors: bool = True + ): + + """ + This is the foundational controller for all MikroTik services. This is built on top of the core message + controller, and, in turn, all individual MikroTik client controllers must be built on top of this. + :param cache: The object to use for caching results from database calls. + :param http_client: The HTTP client + :param base_filter: The basic filter that will be applied to all fetching/updating queries. WARNING: THE BASE + FILTER WILL ALWAYS BE APPLIED AUTOMATICALLY. SET THIS UP WISELY. + :param debug: Whether, or not, you would like to print debugging messages: + :param debug_prefix: The prefix to print with the debugging messages. + :param debug_only_errors: Whether you would like to print only error messages or all messages. + :return: None. + """ + + # Prepare the combined base filter: + mikrotik_filter = {} + for k, v in (base_filter or {}).items(): mikrotik_filter[k] = v + mikrotik_filter["serviceType"] = self.SERVICE_TYPE + + # Invoke the parent's constructor: + CoreSoftwareController.__init__( + self, + cache = cache, + alert_url = alert_url, + http_client = http_client, + base_filter = mikrotik_filter, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + # Init a variable in a parent: + self._service_type = self.SERVICE_TYPE + + # For controlled REST-ful calls: + self._rest = AsyncREST( + http_client = http_client, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + # ┓┏ ┓ + # ┣┫┏┓┃┏┓┏┓┏┓┏ + # ┛┗┗ ┗┣┛┗ ┛ ┛ + # ┛ + + @staticmethod + def get_mikrotik_url( + nas_ip: str, + path: str, + port_no: int | str = None, + use_https: bool = True + ) -> str: + + """ + Simply creates the base URL for hitting the MikroTik server. + :param nas_ip: The IP address of the MikroTik device. + :param path: The path of the REST API to hit. + :param port_no: The port no. to hit the MikroTik device on. + :param use_https: Whether to use HTTPS, or HTTP. + :return: The Base URL string. + """ + + base_url = r"https://" if use_https else r"http://" + base_url += nas_ip + if port_no is not None: base_url += f":{port_no}" + base_url += "/rest" + if not path.startswith("/"): path = "/" + path + return base_url + path + + # ┏┓ + # ┗┓┓┏┏╋┏┓┏┳┓ + # ┗┛┗┫┛┗┗ ┛┗┗ + # ┛ + + async def get_system_resource( + self, + nas_ip: str, + username: str, + password: str, + port_no: int | str = None, + use_https: bool = True + ) -> ApiResponse: + + """ + To get a summary of the hardware resources available in the MikroTik device. This also becomes a great way to + quickly check if any given device is valid, and up and running. + :param nas_ip: The IP address of the MikroTik device. + :param username: The username to get access to the MikroTik device. + :param password: The password to get access to the MikroTik device. + :param port_no: The port no. to hit the MikroTik device on. + :param use_https: Whether to use HTTPS, or HTTP. + :return: A structured API response. + """ + + # Make the API call and return the response: + return await self._rest.get( + url = self.get_mikrotik_url( + nas_ip = nas_ip, + path = r"/system/resource", + port_no = port_no, + use_https = use_https + ), + auth = httpx.BasicAuth( + username = username, + password = password + ) + ) + + # ┏┓ ┓ + # ┣┫┓┏╋┣┓ + # ┛┗┗┻┗┛┗ + + @abstractmethod + async def save_auth( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth + ) -> MikroTikAuthResponse: + + """ + Checks if a particular set of incoming credentials give access to a valid server and then stores the + credentials. + :param sql_conn: The database connection to use to perform this task. + :param mongo_data_conn: The database connection to use to perform this task. + :param mikrotik_auth: The set of credentials as received from the UI/API. + :return: A structured response to indicate what happened during authorization. + """ + + pass + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/controllers_v2/software/mikrotik/base.py b/controllers_v2/software/mikrotik/base.py new file mode 100644 index 0000000..6eb2bbc --- /dev/null +++ b/controllers_v2/software/mikrotik/base.py @@ -0,0 +1,256 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Thursday, 19th Dec., 2024 + + OBJECTIVE: + + To handle all SMS related behaviour from one place. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# My async utils: +from utils_v2.database.async_mongo_v2 import AsyncMongo +from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache + +# Controllers: +from controllers_v2.core.message import CoreMessageController + +# Models: +from models.core.auth_token import CoreAuthTokenModel +from models.message.sms.send import ( + NimbusSMSIndiaMessage, + SavvyBulkSMSKenyaMessage, + SMSSendOneResult, + SMSSendManyResults +) + +# SMS clients: +from utils_v2.sms.india.nimbus.controllers.async_nimbus import AsyncNimbusSMS +from utils_v2.sms.kenya.savvy_bulk_sms.controllers.async_savvy_bulk_sms import AsyncSavvyBulkSMS + +# To work with datatypes: +from typing import List, Any + +# To make HTTP requests: +import httpx + +# to work with MongoDB: +from bson.objectid import ObjectId + +# To make abstract classes: +from abc import ABC, abstractmethod + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class SMSController(CoreMessageController, ABC): + + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + SERVICE_TYPE = "sms" + + # ┏┓ + # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ + # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ + + def __init__( + self, + cache: AsyncRedisCache = None, + http_client: httpx.AsyncClient = None, + alert_url: str = None, + base_filter: dict = None, + debug: bool = True, + debug_prefix: str = "SMS (C) | ", + debug_only_errors: bool = True + ): + + """ + This is the foundational controller for all SMS services. This is built on top of the core message controller, + and, in turn, all individual SMS client controllers must be built on top of this. + :param cache: The object to use for caching results from database calls. + :param http_client: The HTTP client + :param base_filter: The basic filter that will be applied to all fetching/updating queries. WARNING: THE BASE + FILTER WILL ALWAYS BE APPLIED AUTOMATICALLY. SET THIS UP WISELY. + :param debug: Whether, or not, you would like to print debugging messages: + :param debug_prefix: The prefix to print with the debugging messages. + :param debug_only_errors: Whether you would like to print only error messages or all messages. + :return: None. + """ + + # Prepare the combined base filter: + sms_filter = {} + for k, v in (base_filter or {}).items(): sms_filter[k] = v + sms_filter["serviceType"] = self.SERVICE_TYPE + + # Invoke the parent's constructor: + CoreMessageController.__init__( + self, + cache = cache, + alert_url = alert_url, + http_client = http_client, + base_filter = sms_filter, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + # Init a variable in a parent: + self._service_type = self.SERVICE_TYPE + + # ┏┓┳┳┓┏┓ ┏┓ ┓• + # ┗┓┃┃┃┗┓ ┗┓┏┓┏┓┏┫┓┏┓┏┓ + # ┗┛┛ ┗┗┛ ┗┛┗ ┛┗┗┻┗┛┗┗┫ + # ┛ + + @abstractmethod + async def send_one_sms( + self, + mongo_data_conn: AsyncMongo, + auth_token: CoreAuthTokenModel, + client: AsyncNimbusSMS | AsyncSavvyBulkSMS, + message: NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage, + tags: List[Any] + ) -> SMSSendOneResult: + + """ + To send one SMS message through the third-party client. + :param mongo_data_conn: The database connection to use to perform this task. + :param auth_token: The auth token that will be used to send this message. + :param client: The third-party SMS client to use to send this message. + :param message: The actual message that needs to be sent. + :param tags: Any tags to attach with this SMS for filtering when querying in the listing service. + :return: The structured result of sending one message. + """ + + pass + + @abstractmethod + async def send_many_sms( + self, + mongo_data_conn: AsyncMongo, + auth_token: CoreAuthTokenModel, + messages: List[NimbusSMSIndiaMessage | SavvyBulkSMSKenyaMessage], + tags: List[Any] + ) -> SMSSendManyResults: + + """ + To send multiple SMS messages through the third-party client. + individual message, and then aggregates the results. + :param mongo_data_conn: The database connection to use to perform this task. + :param auth_token: The auth token that will be used to send this message. + :param messages: The list of messages to send out. + :param tags: Any tags to attach with these SMS for filtering when querying in the listing service. The same tags + will be applied to all messages. Do not call this method if you need to have different tags for all of them. + :return: The structured result of sending many SMS messages. + """ + + pass + + # ┏┓┳┳┓┏┓ ┳┳ ┓ • + # ┗┓┃┃┃┗┓ ┃┃┏┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┛ ┗┗┛ ┗┛┣┛┗┻┗┻┗┗┛┗┗┫ + # ┛ ┛ + + # We cannot modify the SMS messages themselves, but we can set/unset tags on them for internal referencing and + # filtering. This will help the users organize their inboxes well. + + async def update_sms_tags( + self, + mongo_data_conn: AsyncMongo, + message_id: ObjectId | str, + unset_tags: List[str] = None, + set_tags: List[str] = None + ) -> bool: + + """ + To set and unset tags on an SMS message. + :param mongo_data_conn: The database connection to use to perform this action. + :param message_id: The ObjectId of the document in MongoDb that holds the message. + :param unset_tags: The list of tags to unset (done before setting new tags). + :param set_tags: The list of tags to set (done after unsetting old tags). + :return: True if successful, else False. + """ + + # Simply call the core model: + return await self.update_message_tags( + mongo_data_conn = mongo_data_conn, + message_id = message_id, + unset_tags = unset_tags, + set_tags = set_tags + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/controllers_v2/software/mikrotik/mikrotik_pppoe_1000.py b/controllers_v2/software/mikrotik/mikrotik_pppoe_1000.py new file mode 100644 index 0000000..5820163 --- /dev/null +++ b/controllers_v2/software/mikrotik/mikrotik_pppoe_1000.py @@ -0,0 +1,203 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 10th Feb., 2025. + + OBJECTIVE: + + To handle all MikroTik configuration from one place. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# My async utils: +from utils_v2.database.async_mysql_v2 import AsyncMySQL +from utils_v2.database.async_mongo_v2 import AsyncMongo +from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache + +# Controllers: +from controllers_v2.software.mikrotik.base import MikroTikController + +# To make very controlled API calls: +from utils_v2.rest.controllers.async_base import AsyncREST +from utils_v2.rest.models.api_call import ApiResponse + +# Models: +from models.software.mikrotik.auth import ( + MikroTikPPPoE1000Auth, + MikroTikHotspot1000Auth, + MikroTikAuthResponse +) + +# To work with datatypes: +from typing import List, Any + +# To make HTTP requests: +import httpx + +# to work with MongoDB: +from bson.objectid import ObjectId + +# To make abstract classes: +from abc import ABC, abstractmethod + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class AllMikroTikController(MikroTikController): + + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + SERVICE_TYPE = "mikrotik" + + # ┏┓ + # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ + # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ + + def __init__( + self, + cache: AsyncRedisCache = None, + http_client: httpx.AsyncClient = None, + alert_url: str = None, + base_filter: dict = None, + debug: bool = True, + debug_prefix: str = "All MikroTik (C) | ", + debug_only_errors: bool = True + ): + + """ + This is the foundational controller for all MikroTik services. This is built on top of the core message + controller, and, in turn, all individual MikroTik client controllers must be built on top of this. + :param cache: The object to use for caching results from database calls. + :param http_client: The HTTP client + :param base_filter: The basic filter that will be applied to all fetching/updating queries. WARNING: THE BASE + FILTER WILL ALWAYS BE APPLIED AUTOMATICALLY. SET THIS UP WISELY. + :param debug: Whether, or not, you would like to print debugging messages: + :param debug_prefix: The prefix to print with the debugging messages. + :param debug_only_errors: Whether you would like to print only error messages or all messages. + :return: None. + """ + + # Prepare the combined base filter: + mikrotik_filter = {} + for k, v in (base_filter or {}).items(): mikrotik_filter[k] = v + mikrotik_filter["serviceType"] = self.SERVICE_TYPE + + # Invoke the parent's constructor: + MikroTikController.__init__( + self, + cache = cache, + alert_url = alert_url, + http_client = http_client, + base_filter = mikrotik_filter, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + # Init a variable in a parent: + self._service_type = self.SERVICE_TYPE + + # For controlled REST-ful calls: + self._rest = AsyncREST( + http_client = http_client, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + # ┏┓ ┓ + # ┣┫┓┏╋┣┓ + # ┛┗┗┻┗┛┗ + + async def save_auth( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth + ) -> MikroTikAuthResponse: + + """ + Checks if a particular set of incoming credentials give access to a valid server and then stores the + credentials. + :param sql_conn: The database connection to use to perform this task. + :param mongo_data_conn: The database connection to use to perform this task. + :param mikrotik_auth: The set of credentials as received from the UI/API. + :return: A structured response to indicate what happened during authorization. + """ + + raise NotImplementedError + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/software/__init__.py b/models/software/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/software/mikrotik/__init__.py b/models/software/mikrotik/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/software/mikrotik/auth.py b/models/software/mikrotik/auth.py new file mode 100644 index 0000000..ca3b170 --- /dev/null +++ b/models/software/mikrotik/auth.py @@ -0,0 +1,142 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Friday, 6th Dec., 2024. + + OBJECTIVE: + + To provide a structure to receive auth details of various chat apps (like Telegram and WhatsApp). + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For making data behaviour_models: +from pydantic import BaseModel, Field, field_validator, PastDatetime, model_validator +from typing import Optional, Literal, Union + +# My utils: +from utils_v2.string import regex +from utils_v2.date_time import date_time + +# To work with date and time: +import datetime + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class TelegramAuth(BaseModel): + + botId: str = Field( + description = "The id of the bot (that can be invoked with '@').", + min_length = 1, + frozen = True + ) + + botName: str = Field( + description = "The display name of the bot.", + min_length = 1, + frozen = True + ) + + botToken: str = Field( + description = "the token granted by BotFather", + min_length = 1, + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + +class WhatsAppNimbusAuth(BaseModel): + + apiKey: str = Field( + description = "???", + min_length = 1, + frozen = True + ) + + senderId: str = Field( + description = "???", + min_length = 1, + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/software/mikrotik/configure.py b/models/software/mikrotik/configure.py new file mode 100644 index 0000000..5937777 --- /dev/null +++ b/models/software/mikrotik/configure.py @@ -0,0 +1,312 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 10th Feb., 2025. + + OBJECTIVE: + + To provide a structure to receive auth details for MikroTik devices. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For making data behaviour_models: +from pydantic import BaseModel, Field, field_validator, PastDatetime, model_validator +from typing import Optional, Literal, Union, Any + +# My utils: +from utils_v2.string import regex +from utils_v2.date_time import date_time + +# To work with date and time: +import datetime + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class MikroTikPPPoE1000Auth(BaseModel): + + siteName: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + nasIp: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + nasPort: int | None = Field( + description = "Don't know, and don't want to know.", + frozen = True, + default = None + ) + + radius: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + secret: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + username: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + password: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + location: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + snmpCommunity: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + publicIpPool: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("nasPort", mode = "before") + def validate_port_no(cls, value): + if isinstance(value, (str, float)): value = int(value) + return value + + +# --------------------------------------------------------------------------------------------------------------------- + + +class MikroTikHotspot1000Auth(BaseModel): + + siteName: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + nasIp: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + nasPort: int | None = Field( + description = "Don't know, and don't want to know.", + frozen = True, + default = None + ) + + radius: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + secret: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + username: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + password: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + location: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + snmpCommunity: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + vlanRange: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + privateIpPool: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + publicIpPool: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + domainName: str = Field( + description = "Don't know, and don't want to know.", + min_length = 1, + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("nasPort", mode = "before") + def validate_port_no(cls, value): + if isinstance(value, (str, float)): value = int(value) + return value + + +# --------------------------------------------------------------------------------------------------------------------- + + +class MikroTikAuthResponse(BaseModel): + + success: bool = Field( + description = "To indicate whether or not, the action was a success", + frozen = False, + default = False + ) + + message: str = Field( + description = "To explain what happened in the process of handling the OAuth callback.", + frozen = False, + default = "ERR: Message not captured." + ) + + exception: Any = Field( + description = "To pass on any exception that occurred in the process.", + frozen = False, + default = None + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┏┓ ┏┓ + # ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏ + # ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛ + + pass + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + pass + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/software/tcaoff_ai/__init__.py b/models/software/tcaoff_ai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/software/tcaoff_ai/auth.py b/models/software/tcaoff_ai/auth.py new file mode 100644 index 0000000..a4e8619 --- /dev/null +++ b/models/software/tcaoff_ai/auth.py @@ -0,0 +1,150 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 10th Feb., 2025. + + OBJECTIVE: + + To provide a structure to receive auth details for MikroTik devices. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For making data behaviour_models: +from pydantic import BaseModel, Field, field_validator, PastDatetime, model_validator +from typing import Optional, Literal, Union + +# My utils: +from utils_v2.string import regex +from utils_v2.date_time import date_time + +# To work with date and time: +import datetime + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class TheCAOfficeAIAuth(BaseModel): + + authorizeSoftware: bool = Field( + description = "god knows; don't ask", + frozen = True + ) + + authorizeMailMessaging: bool = Field( + description = "god knows; don't ask", + frozen = True + ) + + authorizeCompliancePortal: bool = Field( + description = "god knows; don't ask", + frozen = True + ) + + authorizeFinancialInstitution: bool = Field( + description = "god knows; don't ask", + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + +class MikroTikAuth(BaseModel): + + botId: str = Field( + description = "The id of the bot (that can be invoked with '@').", + min_length = 1, + frozen = True + ) + + botName: str = Field( + description = "The display name of the bot.", + min_length = 1, + frozen = True + ) + + botToken: str = Field( + description = "the token granted by BotFather", + min_length = 1, + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/readme/MikroTik-PPPoE-1000.md b/readme/MikroTik-PPPoE-1000.md new file mode 100644 index 0000000..dbb4b2c --- /dev/null +++ b/readme/MikroTik-PPPoE-1000.md @@ -0,0 +1,29 @@ +# MiktoTik Configuration Steps +### We need to configure MikroTik servers for two end use cases - PPPoE or Hotspot. here are the steps to achieve them through MikroTik's REST API facility. Use the `MiktoTik (EasyFi)` Postman collection for this. +#### Author: Khushal +#### Date: 20250210 + +--- + +## General Notes: + +MikroTik allows access via REST API using Basic-Auth headers which take a `username` and a `password`. The base path +would look something like `http:///rest`. You may need to mention a port no. if the default has been changed. + +--- + +## Steps for PPPoE + +### 1. Select a Physical Interface + +In this step you must pick the first available interface that is not being used elsewhere. Mind you that this interface +is an actual physical connectivity interface (typically ethernet) on the MikroTik device. + +- Enlist the available interfaces using the listing API on the `/interface` path using `GET` method. +- Select the first one that has field `"running"` set to `"false"`, and pick its `".id"` value. +- Rename it to `"easyfi-pppoe"` by its `".id"` on the `/interface/<.id>` path using `PATCH` method. + +**NOTE:** Remember to save the original configuration in the `comment` field (as a JSON string) in case a roll-back is +needed. + +### 2. \ No newline at end of file diff --git a/utils_v2/network/ip.py b/utils_v2/network/ip.py index 134078f..2cde776 100644 --- a/utils_v2/network/ip.py +++ b/utils_v2/network/ip.py @@ -160,6 +160,61 @@ def get_ipv4_range(ip_string, as_string = True): return start_ip, end_ip, count +# --------------------------------------------------------------------------------------------------------------------- + + +def to_hyphen_notation(value: str) -> str | None: + + """ + Takes in an IP pool (range) in either CIDR notation or already in hyphen-separated notation and parses it into the + hyphen-separated notation. + :param value: The IP-range in either of the accepted formats. + :return: The IP range in hyphen-separated notation. + """ + + # Let's start by assuming failure: + success = False + start_ip = None + end_ip = None + + # Ensure that the input is treated as a string: + value = str(value) + + # First we check if the IP has been given in the CIDR notation: + if not success: + try: + + # Try to extract the first and last IP addressed from the input: + ip_net = ipaddress.IPv4Network(value, strict = False) + start_ip = ip_net.network_address + end_ip = ip_net.broadcast_address + + success = True + + # In case the CIDR interpretation doesn't work: + except: + success = False + + # Now we try to parse the input string as a hyphen-separated input: + if not success: + try: + + # Split at the hyphen and take the parts: + parts = value.split("-") + start_ip = parts[0] + end_ip = parts[1] + + success = True + + # In case the hyphen-separated interpretation doesn't work: + except: + success = False + + # Done here: + value = f"{start_ip}-{end_ip}" if success else None + return value + + # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** diff --git a/utils_v2/rest/controllers/async_base.py b/utils_v2/rest/controllers/async_base.py index 560c203..a5704be 100644 --- a/utils_v2/rest/controllers/async_base.py +++ b/utils_v2/rest/controllers/async_base.py @@ -95,13 +95,13 @@ import inspect # ***************************************************************************************************************** -class AsyncRestBase: +class AsyncREST: def __init__( self, http_client: httpx.AsyncClient = None, debug = True, - debug_prefix = "GMail | ", + debug_prefix = "REST (C) | ", debug_only_errors = True ): @@ -155,7 +155,8 @@ class AsyncRestBase: self, url: str, headers: dict = None, - params: dict = None + params: dict = None, + auth: httpx.Auth = None ) -> ApiResponse: """ @@ -163,6 +164,7 @@ class AsyncRestBase: :param url: The URL to call. :param headers: The headers to pass. :param params: The params to send in the query string itself. + :param auth: Any authentication credentials that need to be sent. :return: A structured response that includes the raw response, the exception (if any), and so on. """ @@ -179,7 +181,8 @@ class AsyncRestBase: response = await self._http_client.get( url = url, headers = headers, - params = params + params = params, + auth = auth ) # Note down the results: @@ -202,7 +205,8 @@ class AsyncRestBase: headers: dict = None, json: dict = None, data: dict = None, - content: str | bytes = None + content: str | bytes = None, + auth: httpx.Auth = None ) -> ApiResponse: """ @@ -212,6 +216,7 @@ class AsyncRestBase: :param json: The params to send in the JSON body. :param data: The params to send in the form-data in the body. :param content: The raw content to be sent in the body (typically as an octet-stream). + :param auth: Any authentication credentials that need to be sent. :return: A structured response that includes the raw response, the exception (if any), and so on. """ @@ -230,7 +235,8 @@ class AsyncRestBase: headers = headers, json = json, data = data, - content = content + content = content, + auth = auth ) # Note down the results: @@ -252,7 +258,8 @@ class AsyncRestBase: url: str, headers: dict = None, json: dict = None, - data: dict = None + data: dict = None, + auth: httpx.Auth = None ) -> ApiResponse: """ @@ -261,6 +268,7 @@ class AsyncRestBase: :param headers: The headers to pass. :param json: The params to send in the JSON body. :param data: The params to send in the form-data in the body. + :param auth: Any authentication credentials that need to be sent. :return: A structured response that includes the raw response, the exception (if any), and so on. """ @@ -278,7 +286,8 @@ class AsyncRestBase: url = url, headers = headers, json = json, - data = data + data = data, + auth = auth ) # Note down the results: @@ -298,13 +307,15 @@ class AsyncRestBase: async def delete( self, url: str, - headers: dict = None + headers: dict = None, + auth: httpx.Auth = None ) -> ApiResponse: """ To call an API using the DELETE method. :param url: The URL to call. :param headers: The headers to pass. + :param auth: Any authentication credentials that need to be sent. :return: A structured response that includes the raw response, the exception (if any), and so on. """ @@ -320,7 +331,8 @@ class AsyncRestBase: # Make the API call: response = await self._http_client.delete( url = url, - headers = headers + headers = headers, + auth = auth ) # Note down the results: