(20250210) Worked on the IP addr util a bit.

This commit is contained in:
2025-02-10 19:34:52 +05:30
parent 800d6706ef
commit 95d90e807b
15 changed files with 1918 additions and 10 deletions
+477
View File
@@ -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
View File
@@ -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
+256
View File
@@ -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
@@ -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