mirror of
https://github.com/craigerl/aprsd.git
synced 2024-11-21 23:55:17 -05:00
Compare commits
3 Commits
450bacfe99
...
e57a2e2ffc
Author | SHA1 | Date | |
---|---|---|---|
e57a2e2ffc | |||
6a1cea63e4 | |||
592b328956 |
@ -11,6 +11,7 @@ import sys
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
import requests
|
import requests
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, utils
|
from aprsd import cli_helper, utils
|
||||||
@ -21,6 +22,7 @@ from aprsd.aprsd import cli
|
|||||||
# setup the global logger
|
# setup the global logger
|
||||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@ -42,27 +44,31 @@ LOG = logging.getLogger("APRSD")
|
|||||||
@cli_helper.process_standard_options_no_config
|
@cli_helper.process_standard_options_no_config
|
||||||
def healthcheck(ctx, health_url, timeout):
|
def healthcheck(ctx, health_url, timeout):
|
||||||
"""Check the health of the running aprsd server."""
|
"""Check the health of the running aprsd server."""
|
||||||
LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}")
|
console.log(f"APRSD HealthCheck version: {aprsd.__version__}")
|
||||||
|
with console.status(f"APRSD HealthCheck version: {aprsd.__version__}") as status:
|
||||||
try:
|
try:
|
||||||
url = health_url
|
url = health_url
|
||||||
response = requests.get(url, timeout=timeout)
|
response = requests.get(url, timeout=timeout)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(f"Failed to fetch healthcheck url '{url}' : '{ex}'")
|
console.log(f"Failed to fetch healthcheck url '{url}' : '{ex}'")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
else:
|
else:
|
||||||
stats = json.loads(response.text)
|
import time
|
||||||
LOG.debug(stats)
|
time.sleep(2)
|
||||||
|
status.update("PISS")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
stats = json.loads(response.text)
|
||||||
email_thread_last_update = stats["stats"]["email"]["thread_last_update"]
|
email_thread_last_update = stats["stats"]["email"]["thread_last_update"]
|
||||||
|
|
||||||
|
if email_thread_last_update != "never":
|
||||||
delta = utils.parse_delta_str(email_thread_last_update)
|
delta = utils.parse_delta_str(email_thread_last_update)
|
||||||
d = datetime.timedelta(**delta)
|
d = datetime.timedelta(**delta)
|
||||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||||
max_delta = datetime.timedelta(**max_timeout)
|
max_delta = datetime.timedelta(**max_timeout)
|
||||||
if d > max_delta:
|
if d > max_delta:
|
||||||
LOG.error(f"Email thread is very old! {d}")
|
console.log(f"Email thread is very old! {d}")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
aprsis_last_update = stats["stats"]["aprs-is"]["last_update"]
|
aprsis_last_update = stats["stats"]["aprs-is"]["last_update"]
|
||||||
|
@ -227,6 +227,8 @@ def list_plugins(ctx):
|
|||||||
|
|
||||||
# now find any from pypi?
|
# now find any from pypi?
|
||||||
installed_plugins = get_installed_plugins()
|
installed_plugins = get_installed_plugins()
|
||||||
|
with console.status("Fetching pypi.org plugins"):
|
||||||
show_pypi_plugins(installed_plugins, console)
|
show_pypi_plugins(installed_plugins, console)
|
||||||
|
|
||||||
|
with console.status("Looking for installed aprsd plugins"):
|
||||||
show_installed_plugins(installed_plugins, console)
|
show_installed_plugins(installed_plugins, console)
|
||||||
|
@ -10,6 +10,7 @@ import time
|
|||||||
|
|
||||||
import aprslib
|
import aprslib
|
||||||
import click
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
@ -22,6 +23,7 @@ from aprsd.aprsd import cli
|
|||||||
# setup the global logger
|
# setup the global logger
|
||||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
def signal_handler(sig, frame):
|
def signal_handler(sig, frame):
|
||||||
@ -113,7 +115,7 @@ def listen(
|
|||||||
resp = packet.get("response", None)
|
resp = packet.get("response", None)
|
||||||
if resp == "ack":
|
if resp == "ack":
|
||||||
ack_num = packet.get("msgNo")
|
ack_num = packet.get("msgNo")
|
||||||
LOG.info(f"We saw an ACK {ack_num} Ignoring")
|
console.log(f"We saw an ACK {ack_num} Ignoring")
|
||||||
messaging.log_packet(packet)
|
messaging.log_packet(packet)
|
||||||
else:
|
else:
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
@ -125,6 +127,7 @@ def listen(
|
|||||||
message,
|
message,
|
||||||
fromcall=fromcall,
|
fromcall=fromcall,
|
||||||
ack=msg_number,
|
ack=msg_number,
|
||||||
|
console=console,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize the client factory and create
|
# Initialize the client factory and create
|
||||||
@ -148,6 +151,7 @@ def listen(
|
|||||||
# This will register a packet consumer with aprslib
|
# This will register a packet consumer with aprslib
|
||||||
# When new packets come in the consumer will process
|
# When new packets come in the consumer will process
|
||||||
# the packet
|
# the packet
|
||||||
|
with console.status("Listening for packets"):
|
||||||
aprs_client.consumer(rx_packet, raw=False)
|
aprs_client.consumer(rx_packet, raw=False)
|
||||||
except aprslib.exceptions.ConnectionDrop:
|
except aprslib.exceptions.ConnectionDrop:
|
||||||
LOG.error("Connection dropped, reconnecting")
|
LOG.error("Connection dropped, reconnecting")
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from logging import NullHandler
|
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
import sys
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@ -11,6 +9,7 @@ import aprslib
|
|||||||
from aprslib.exceptions import LoginError
|
from aprslib.exceptions import LoginError
|
||||||
import flask
|
import flask
|
||||||
from flask import request
|
from flask import request
|
||||||
|
from flask.logging import default_handler
|
||||||
import flask_classful
|
import flask_classful
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
from flask_socketio import Namespace, SocketIO
|
from flask_socketio import Namespace, SocketIO
|
||||||
@ -21,6 +20,7 @@ from aprsd import client
|
|||||||
from aprsd import config as aprsd_config
|
from aprsd import config as aprsd_config
|
||||||
from aprsd import log, messaging, packets, plugin, stats, threads, utils
|
from aprsd import log, messaging, packets, plugin, stats, threads, utils
|
||||||
from aprsd.clients import aprsis
|
from aprsd.clients import aprsis
|
||||||
|
from aprsd.logging import logging as aprsd_logging
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -552,6 +552,15 @@ class LoggingNamespace(Namespace):
|
|||||||
|
|
||||||
def setup_logging(config, flask_app, loglevel, quiet):
|
def setup_logging(config, flask_app, loglevel, quiet):
|
||||||
flask_log = logging.getLogger("werkzeug")
|
flask_log = logging.getLogger("werkzeug")
|
||||||
|
flask_app.logger.removeHandler(default_handler)
|
||||||
|
flask_log.removeHandler(default_handler)
|
||||||
|
|
||||||
|
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
||||||
|
flask_log.setLevel(log_level)
|
||||||
|
date_format = config["aprsd"].get(
|
||||||
|
"dateformat",
|
||||||
|
aprsd_config.DEFAULT_DATE_FORMAT,
|
||||||
|
)
|
||||||
|
|
||||||
if not config["aprsd"]["web"].get("logging_enabled", False):
|
if not config["aprsd"]["web"].get("logging_enabled", False):
|
||||||
# disable web logging
|
# disable web logging
|
||||||
@ -559,29 +568,31 @@ def setup_logging(config, flask_app, loglevel, quiet):
|
|||||||
flask_app.logger.disabled = True
|
flask_app.logger.disabled = True
|
||||||
return
|
return
|
||||||
|
|
||||||
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
if config["aprsd"].get("rich_logging", False) and not quiet:
|
||||||
LOG.setLevel(log_level)
|
log_format = "%(message)s"
|
||||||
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
|
||||||
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
|
||||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
|
rh = aprsd_logging.APRSDRichHandler(
|
||||||
|
show_thread=True, thread_width=15,
|
||||||
|
rich_tracebacks=True, omit_repeated_times=False,
|
||||||
|
)
|
||||||
|
rh.setFormatter(log_formatter)
|
||||||
|
flask_log.addHandler(rh)
|
||||||
|
|
||||||
log_file = config["aprsd"].get("logfile", None)
|
log_file = config["aprsd"].get("logfile", None)
|
||||||
|
|
||||||
if log_file:
|
if log_file:
|
||||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
log_format = config["aprsd"].get(
|
||||||
else:
|
"logformat",
|
||||||
fh = NullHandler()
|
aprsd_config.DEFAULT_LOG_FORMAT,
|
||||||
|
)
|
||||||
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
|
fh = RotatingFileHandler(
|
||||||
|
log_file, maxBytes=(10248576 * 5),
|
||||||
|
backupCount=4,
|
||||||
|
)
|
||||||
fh.setFormatter(log_formatter)
|
fh.setFormatter(log_formatter)
|
||||||
for handler in flask_app.logger.handlers:
|
|
||||||
handler.setFormatter(log_formatter)
|
|
||||||
print(handler)
|
|
||||||
|
|
||||||
flask_log.addHandler(fh)
|
flask_log.addHandler(fh)
|
||||||
|
|
||||||
if not quiet:
|
|
||||||
sh = logging.StreamHandler(sys.stdout)
|
|
||||||
sh.setFormatter(log_formatter)
|
|
||||||
flask_log.addHandler(sh)
|
|
||||||
|
|
||||||
|
|
||||||
def init_flask(config, loglevel, quiet):
|
def init_flask(config, loglevel, quiet):
|
||||||
global socketio
|
global socketio
|
||||||
|
22
aprsd/log.py
22
aprsd/log.py
@ -18,35 +18,31 @@ logging_queue = queue.Queue()
|
|||||||
def setup_logging(config, loglevel, quiet):
|
def setup_logging(config, loglevel, quiet):
|
||||||
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
||||||
LOG.setLevel(log_level)
|
LOG.setLevel(log_level)
|
||||||
if config["aprsd"].get("rich_logging", False):
|
|
||||||
log_format = "%(message)s"
|
|
||||||
else:
|
|
||||||
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
|
||||||
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
||||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
|
||||||
log_file = config["aprsd"].get("logfile", None)
|
|
||||||
|
|
||||||
rich_logging = False
|
rich_logging = False
|
||||||
if config["aprsd"].get("rich_logging", False):
|
if config["aprsd"].get("rich_logging", False) and not quiet:
|
||||||
|
log_format = "%(message)s"
|
||||||
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
rh = aprsd_logging.APRSDRichHandler(
|
rh = aprsd_logging.APRSDRichHandler(
|
||||||
show_thread=True, thread_width=15,
|
show_thread=True, thread_width=20,
|
||||||
rich_tracebacks=True, omit_repeated_times=False,
|
rich_tracebacks=True, omit_repeated_times=False,
|
||||||
)
|
)
|
||||||
rh.setFormatter(log_formatter)
|
rh.setFormatter(log_formatter)
|
||||||
LOG.addHandler(rh)
|
LOG.addHandler(rh)
|
||||||
rich_logging = True
|
rich_logging = True
|
||||||
|
|
||||||
if log_file:
|
log_file = config["aprsd"].get("logfile", None)
|
||||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
|
||||||
else:
|
|
||||||
fh = NullHandler()
|
|
||||||
|
|
||||||
|
if log_file:
|
||||||
|
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
||||||
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
|
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||||
fh.setFormatter(log_formatter)
|
fh.setFormatter(log_formatter)
|
||||||
LOG.addHandler(fh)
|
LOG.addHandler(fh)
|
||||||
|
|
||||||
imap_logger = None
|
imap_logger = None
|
||||||
if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False):
|
if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False):
|
||||||
|
|
||||||
imap_logger = logging.getLogger("imapclient.imaplib")
|
imap_logger = logging.getLogger("imapclient.imaplib")
|
||||||
imap_logger.setLevel(log_level)
|
imap_logger.setLevel(log_level)
|
||||||
imap_logger.addHandler(fh)
|
imap_logger.addHandler(fh)
|
||||||
|
@ -190,6 +190,7 @@ class Message(metaclass=abc.ABCMeta):
|
|||||||
fromcall,
|
fromcall,
|
||||||
tocall,
|
tocall,
|
||||||
msg_id=None,
|
msg_id=None,
|
||||||
|
allow_delay=True,
|
||||||
):
|
):
|
||||||
self.fromcall = fromcall
|
self.fromcall = fromcall
|
||||||
self.tocall = tocall
|
self.tocall = tocall
|
||||||
@ -199,6 +200,10 @@ class Message(metaclass=abc.ABCMeta):
|
|||||||
msg_id = c.value
|
msg_id = c.value
|
||||||
self.id = msg_id
|
self.id = msg_id
|
||||||
|
|
||||||
|
# do we try and save this message for later if we don't get
|
||||||
|
# an ack? Some messages we don't want to do this ever.
|
||||||
|
self.allow_delay = allow_delay
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def send(self):
|
def send(self):
|
||||||
"""Child class must declare."""
|
"""Child class must declare."""
|
||||||
@ -214,8 +219,8 @@ class RawMessage(Message):
|
|||||||
|
|
||||||
message = None
|
message = None
|
||||||
|
|
||||||
def __init__(self, message):
|
def __init__(self, message, allow_delay=True):
|
||||||
super().__init__(None, None, msg_id=None)
|
super().__init__(fromcall=None, tocall=None, msg_id=None, allow_delay=allow_delay)
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
def dict(self):
|
def dict(self):
|
||||||
@ -269,11 +274,11 @@ class TextMessage(Message):
|
|||||||
msg_id=None,
|
msg_id=None,
|
||||||
allow_delay=True,
|
allow_delay=True,
|
||||||
):
|
):
|
||||||
super().__init__(fromcall, tocall, msg_id)
|
super().__init__(
|
||||||
|
fromcall=fromcall, tocall=tocall,
|
||||||
|
msg_id=msg_id, allow_delay=allow_delay,
|
||||||
|
)
|
||||||
self.message = message
|
self.message = message
|
||||||
# do we try and save this message for later if we don't get
|
|
||||||
# an ack? Some messages we don't want to do this ever.
|
|
||||||
self.allow_delay = allow_delay
|
|
||||||
|
|
||||||
def dict(self):
|
def dict(self):
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
@ -369,6 +374,8 @@ class SendMessageThread(threads.APRSDThread):
|
|||||||
# we reached the send limit, don't send again
|
# we reached the send limit, don't send again
|
||||||
# TODO(hemna) - Need to put this in a delayed queue?
|
# TODO(hemna) - Need to put this in a delayed queue?
|
||||||
LOG.info("Message Send Complete. Max attempts reached.")
|
LOG.info("Message Send Complete. Max attempts reached.")
|
||||||
|
if not msg.allow_delay:
|
||||||
|
tracker.remove(msg.id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Message is still outstanding and needs to be acked.
|
# Message is still outstanding and needs to be acked.
|
||||||
@ -527,6 +534,7 @@ def log_packet(packet):
|
|||||||
def log_message(
|
def log_message(
|
||||||
header, raw, message, tocall=None, fromcall=None, msg_num=None,
|
header, raw, message, tocall=None, fromcall=None, msg_num=None,
|
||||||
retry_number=None, ack=None, packet_type=None, uuid=None,
|
retry_number=None, ack=None, packet_type=None, uuid=None,
|
||||||
|
console=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -565,4 +573,7 @@ def log_message(
|
|||||||
log_list.append(f" UUID : {uuid}")
|
log_list.append(f" UUID : {uuid}")
|
||||||
log_list.append(f"{header} _______________ Complete")
|
log_list.append(f"{header} _______________ Complete")
|
||||||
|
|
||||||
|
if console:
|
||||||
|
console.log("\n".join(log_list))
|
||||||
|
else:
|
||||||
LOG.info("\n".join(log_list))
|
LOG.info("\n".join(log_list))
|
||||||
|
@ -86,7 +86,7 @@ class WatchList(objectstore.ObjectStoreMixin):
|
|||||||
if config:
|
if config:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
ring_size = config["aprsd"]["watch_list"]["packet_keep_count"]
|
ring_size = config["aprsd"]["watch_list"].get("packet_keep_count", 10)
|
||||||
|
|
||||||
for callsign in config["aprsd"]["watch_list"].get("callsigns", []):
|
for callsign in config["aprsd"]["watch_list"].get("callsigns", []):
|
||||||
call = callsign.replace("*", "")
|
call = callsign.replace("*", "")
|
||||||
|
@ -177,7 +177,6 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|||||||
)
|
)
|
||||||
if result:
|
if result:
|
||||||
self.tx_inc()
|
self.tx_inc()
|
||||||
wl.update_seen(packet)
|
|
||||||
else:
|
else:
|
||||||
LOG.warning(f"{self.__class__} plugin is not enabled")
|
LOG.warning(f"{self.__class__} plugin is not enabled")
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aprsd import messaging, packets, plugin
|
from aprsd import messaging, packets, plugin, trace
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -17,6 +17,7 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
|||||||
|
|
||||||
short_description = "Notify me when a CALLSIGN is recently seen on APRS-IS"
|
short_description = "Notify me when a CALLSIGN is recently seen on APRS-IS"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def process(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("NotifySeenPlugin")
|
LOG.info("NotifySeenPlugin")
|
||||||
|
|
||||||
@ -46,10 +47,11 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
|||||||
)
|
)
|
||||||
return msg
|
return msg
|
||||||
else:
|
else:
|
||||||
|
LOG.debug("fromcall and notify_callsign are the same, not notifying")
|
||||||
return messaging.NULL_MESSAGE
|
return messaging.NULL_MESSAGE
|
||||||
else:
|
else:
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"Not old enough to notify callsign '{}' : {} < {}".format(
|
"Not old enough to notify on callsign '{}' : {} < {}".format(
|
||||||
fromcall,
|
fromcall,
|
||||||
age,
|
age,
|
||||||
wl.max_delta(),
|
wl.max_delta(),
|
||||||
|
@ -202,7 +202,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
self.packet = packet
|
self.packet = packet
|
||||||
self.config = config
|
self.config = config
|
||||||
name = self.packet["raw"][:10]
|
name = self.packet["raw"][:10]
|
||||||
super().__init__(f"RX_PACKET-{name}")
|
super().__init__(f"RXPKT-{name}")
|
||||||
|
|
||||||
def process_ack_packet(self, packet):
|
def process_ack_packet(self, packet):
|
||||||
ack_num = packet.get("msgNo")
|
ack_num = packet.get("msgNo")
|
||||||
@ -261,6 +261,8 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
try:
|
try:
|
||||||
results = pm.run(packet)
|
results = pm.run(packet)
|
||||||
|
wl = packets.WatchList()
|
||||||
|
wl.update_seen(packet)
|
||||||
replied = False
|
replied = False
|
||||||
for reply in results:
|
for reply in results:
|
||||||
if isinstance(reply, list):
|
if isinstance(reply, list):
|
||||||
|
Loading…
Reference in New Issue
Block a user