Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c36d6d3fc2 |
+3
-14
@@ -427,11 +427,6 @@ class Events(commands.Cog):
|
|||||||
return
|
return
|
||||||
message_content_lower = message_content_lower.replace("imaginuje sobie: ", "")
|
message_content_lower = message_content_lower.replace("imaginuje sobie: ", "")
|
||||||
self.logger.debug("Wywolanie obrazka: %s", message_content_lower)
|
self.logger.debug("Wywolanie obrazka: %s", message_content_lower)
|
||||||
# Every error branch below must RETURN: otherwise control falls
|
|
||||||
# through to `if response:` with `response` unbound (the call
|
|
||||||
# raised) -> UnboundLocalError, crashing the handler right after
|
|
||||||
# the friendly message was already sent.
|
|
||||||
response = None
|
|
||||||
try:
|
try:
|
||||||
response = await OPENAICLIENT.images.generate(
|
response = await OPENAICLIENT.images.generate(
|
||||||
model="dall-e-3",
|
model="dall-e-3",
|
||||||
@@ -445,12 +440,10 @@ class Events(commands.Cog):
|
|||||||
await discord_friendly_reply(
|
await discord_friendly_reply(
|
||||||
message, f"*Kondziu patrzy na terminal, czeka, czeka, czeka,.... Jeszcze chwile czeka Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai spróbuj od nowa. *Na ekranie pojawia się*: {e}"
|
message, f"*Kondziu patrzy na terminal, czeka, czeka, czeka,.... Jeszcze chwile czeka Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai spróbuj od nowa. *Na ekranie pojawia się*: {e}"
|
||||||
)
|
)
|
||||||
return
|
|
||||||
except openai.APIConnectionError as e:
|
except openai.APIConnectionError as e:
|
||||||
await discord_friendly_reply(
|
await discord_friendly_reply(
|
||||||
message, f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai. *Na ekranie pojawia się*: {e}"
|
message, f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Nie mogę się połączyć z Openai. *Na ekranie pojawia się*: {e}"
|
||||||
)
|
)
|
||||||
return
|
|
||||||
except openai.BadRequestError as e:
|
except openai.BadRequestError as e:
|
||||||
# Handle invalid request error, e.g. validate parameters or log
|
# Handle invalid request error, e.g. validate parameters or log
|
||||||
if message.author.nick:
|
if message.author.nick:
|
||||||
@@ -468,31 +461,27 @@ class Events(commands.Cog):
|
|||||||
await discord_friendly_reply(
|
await discord_friendly_reply(
|
||||||
message, f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}"
|
message, f"Sorki, cenzura: {resp}. Jak chcesz to są kanały na nudle #sexy-foteczky i #kanal-do-fapania *Na ekranie pojawia się: {e}"
|
||||||
)
|
)
|
||||||
return
|
|
||||||
except openai.AuthenticationError as e:
|
except openai.AuthenticationError as e:
|
||||||
# Handle authentication error, e.g. check credentials or log
|
# Handle authentication error, e.g. check credentials or log
|
||||||
await discord_friendly_reply(
|
await discord_friendly_reply(
|
||||||
message, f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z hasłem zjebało. *Na terminalu pojawia się:* {e}"
|
message, f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z hasłem zjebało. *Na terminalu pojawia się:* {e}"
|
||||||
)
|
)
|
||||||
return
|
|
||||||
except openai.PermissionDeniedError as e:
|
except openai.PermissionDeniedError as e:
|
||||||
# Handle permission error, e.g. check scope or log
|
# Handle permission error, e.g. check scope or log
|
||||||
# (was accidentally passing a (message, text) TUPLE as one arg)
|
|
||||||
await discord_friendly_reply(
|
await discord_friendly_reply(
|
||||||
message, f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z uprawnieniami zjebało. *Na terminalu pojawia się:* {e}"
|
(
|
||||||
|
message, f"*Kondziu patrzy na terminal, chwile się zastanawia. Przypierdala w niego pięścią....* Wołaj szefa - coś się z uprawnieniami zjebało. *Na terminalu pojawia się:* {e}"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
|
||||||
except openai.RateLimitError as e:
|
except openai.RateLimitError as e:
|
||||||
await discord_friendly_reply(
|
await discord_friendly_reply(
|
||||||
message, f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}"
|
message, f"*Kondziu patrzy na terminal* Wołaj szefa. Zapłacić rachunki za AI trzeba. Jak chcesz to się na #zebranie dorzuć. {e}"
|
||||||
)
|
)
|
||||||
return
|
|
||||||
except openai.APIError as e:
|
except openai.APIError as e:
|
||||||
# Handle API error, e.g. retry or log
|
# Handle API error, e.g. retry or log
|
||||||
await discord_friendly_reply(
|
await discord_friendly_reply(
|
||||||
message, f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}"
|
message, f"*Kondziu nurkuje za bar, terminal wybucha. Przed tobą ląduje pergamin zapisany pięknym gotykiem a na nim*: {e}"
|
||||||
)
|
)
|
||||||
return
|
|
||||||
if response:
|
if response:
|
||||||
self.logger.info(response)
|
self.logger.info(response)
|
||||||
image_url = response.data[0].url
|
image_url = response.data[0].url
|
||||||
|
|||||||
+2
-4
@@ -541,12 +541,10 @@ async def get_random_cyclic_message(client):
|
|||||||
# trunk-ignore(bandit/B311)
|
# trunk-ignore(bandit/B311)
|
||||||
ai_check = random.randint(0, 10)
|
ai_check = random.randint(0, 10)
|
||||||
logger.info("Losowa wypowiedź")
|
logger.info("Losowa wypowiedź")
|
||||||
if ai_check < 2 and CYCLIC_WORDS:
|
if ai_check < 2:
|
||||||
logger.info("Predefiniowana")
|
logger.info("Predefiniowana")
|
||||||
# randrange(n) is 0..n-1; randint(0, n) was inclusive and could return n
|
|
||||||
# -> list(...)[n] IndexError. Guarded on empty CYCLIC_WORDS above.
|
|
||||||
# trunk-ignore(bandit/B311)
|
# trunk-ignore(bandit/B311)
|
||||||
messnum = random.randrange(len(CYCLIC_WORDS))
|
messnum = random.randint(0, len(CYCLIC_WORDS))
|
||||||
logger.debug(messnum)
|
logger.debug(messnum)
|
||||||
logger.debug(len(CYCLIC_WORDS))
|
logger.debug(len(CYCLIC_WORDS))
|
||||||
mess_key = list(CYCLIC_WORDS.keys())[messnum]
|
mess_key = list(CYCLIC_WORDS.keys())[messnum]
|
||||||
|
|||||||
@@ -128,15 +128,13 @@ SERVICE_EXTENSION_GROUPS = {
|
|||||||
SERVICE_RECHECK_SECONDS = 300
|
SERVICE_RECHECK_SECONDS = 300
|
||||||
|
|
||||||
|
|
||||||
def _service_health(url: str):
|
def _service_alive(url: str) -> bool:
|
||||||
"""Return None when the service answers HTTP at all (any status counts),
|
"""True when the service answers HTTP at all (any status code counts)."""
|
||||||
otherwise the connection error explaining WHY it's unreachable (refused vs
|
|
||||||
timeout vs DNS - the difference points straight at the cause)."""
|
|
||||||
try:
|
try:
|
||||||
requests.get(url, timeout=3)
|
requests.get(url, timeout=3)
|
||||||
return None
|
return True
|
||||||
except requests.exceptions.RequestException as exc:
|
except requests.exceptions.RequestException:
|
||||||
return exc
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def _load_extension_safe(name: str) -> bool:
|
async def _load_extension_safe(name: str) -> bool:
|
||||||
@@ -174,25 +172,16 @@ async def _load_service_groups() -> bool:
|
|||||||
service_headers(),
|
service_headers(),
|
||||||
LIBRARIAN_PING_TIMEOUT,
|
LIBRARIAN_PING_TIMEOUT,
|
||||||
)
|
)
|
||||||
if not alive:
|
|
||||||
logger.warning(
|
|
||||||
"Service 'librarian' ping round-trip failed (%s) - cogs stay disabled: %s",
|
|
||||||
group["health_url"],
|
|
||||||
", ".join(missing),
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
err = await asyncio.to_thread(_service_health, group["health_url"])
|
alive = await asyncio.to_thread(_service_alive, group["health_url"])
|
||||||
if err is not None:
|
if not alive:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Service '%s' unreachable (%s) [%s: %s] - cogs stay disabled: %s",
|
"Service '%s' unreachable (%s) - cogs stay disabled: %s",
|
||||||
service,
|
service,
|
||||||
group["health_url"],
|
group["health_url"],
|
||||||
type(err).__name__,
|
", ".join(missing),
|
||||||
err,
|
)
|
||||||
", ".join(missing),
|
continue
|
||||||
)
|
|
||||||
continue
|
|
||||||
logger.info("Service '%s' is alive - enabling: %s", service, ", ".join(missing))
|
logger.info("Service '%s' is alive - enabling: %s", service, ", ".join(missing))
|
||||||
for extension in missing:
|
for extension in missing:
|
||||||
if await _load_extension_safe(extension):
|
if await _load_extension_safe(extension):
|
||||||
@@ -236,16 +225,6 @@ async def on_ready():
|
|||||||
for extension in CORE_EXTENSIONS:
|
for extension in CORE_EXTENSIONS:
|
||||||
await _load_extension_safe(extension)
|
await _load_extension_safe(extension)
|
||||||
|
|
||||||
# Log the ACTUALLY-resolved service addresses. When one shows the built-in
|
|
||||||
# default (192.168.1.15:5000) it means the matching CONJURER_* env var never
|
|
||||||
# reached the process - the single most common cause of "service unreachable"
|
|
||||||
# confusion. Printing them makes env-vs-default obvious at a glance.
|
|
||||||
logger.info(
|
|
||||||
"Resolved service addresses -> musician(file): %s | librarian: %s | radio: %s",
|
|
||||||
FILE_SERVICE_ADDRESS,
|
|
||||||
LIBRARIAN_SERVICE_ADDRESS,
|
|
||||||
RADIO_SERVICE_ADDRESS,
|
|
||||||
)
|
|
||||||
await _load_service_groups()
|
await _load_service_groups()
|
||||||
logger.info("Sensors: online")
|
logger.info("Sensors: online")
|
||||||
|
|
||||||
|
|||||||
@@ -269,20 +269,14 @@ def scan_incoming(stop_event: Optional[threading.Event] = None):
|
|||||||
awaiting_q.remove(record)
|
awaiting_q.remove(record)
|
||||||
logger.info("PONG matched for %s", pong_uuid)
|
logger.info("PONG matched for %s", pong_uuid)
|
||||||
continue
|
continue
|
||||||
# Collect matched records and drop them from awaiting_q afterwards -
|
record_stored = False
|
||||||
# they used to stay forever (awaiting_q only ever grew), leaking
|
|
||||||
# memory over the bot's uptime and letting a reused UUID re-match a
|
|
||||||
# stale record.
|
|
||||||
matched = []
|
|
||||||
for record in awaiting_q:
|
for record in awaiting_q:
|
||||||
if record.uuid in answer.keys():
|
if record.uuid in answer.keys():
|
||||||
|
record_stored = True
|
||||||
record.stop = True
|
record.stop = True
|
||||||
record.entries = answer[record.uuid]
|
record.entries = answer[record.uuid]
|
||||||
IN_COMM_Q.put(record)
|
IN_COMM_Q.put(record)
|
||||||
matched.append(record)
|
if not record_stored:
|
||||||
for record in matched:
|
|
||||||
awaiting_q.remove(record)
|
|
||||||
if not matched:
|
|
||||||
for key in answer.keys():
|
for key in answer.keys():
|
||||||
record = QueryControl("Orphaned", key, "Orphan", None)
|
record = QueryControl("Orphaned", key, "Orphan", None)
|
||||||
record.stop = True
|
record.stop = True
|
||||||
@@ -359,13 +353,10 @@ def id3(url: str) -> dict:
|
|||||||
resp.read(
|
resp.read(
|
||||||
metaint
|
metaint
|
||||||
) # this isn't seekable so, arbitrarily read to the point we want
|
) # this isn't seekable so, arbitrarily read to the point we want
|
||||||
# Guard the headers: an Icecast stream that omits icy-name / icy-genre
|
|
||||||
# (e.g. while the radio is down) made `.title()` raise AttributeError on
|
|
||||||
# None, 500-ing the /prepped_tracks "next" handler that calls this.
|
|
||||||
tagdata = dict(
|
tagdata = dict(
|
||||||
site_url=resp.headers.get("icy-url"),
|
site_url=resp.headers.get("icy-url"),
|
||||||
name=(resp.headers.get("icy-name") or "").title(),
|
name=resp.headers.get("icy-name").title(),
|
||||||
genre=(resp.headers.get("icy-genre") or "").title(),
|
genre=resp.headers.get("icy-genre").title(),
|
||||||
title=get_stream_title(resp.read(255)),
|
title=get_stream_title(resp.read(255)),
|
||||||
)
|
)
|
||||||
return tagdata
|
return tagdata
|
||||||
|
|||||||
@@ -203,12 +203,6 @@ def wyszukaj(word_list, how_many, _logger=None, write_to=None):
|
|||||||
# ---------------------------------------------------------------- tailer
|
# ---------------------------------------------------------------- tailer
|
||||||
def scan_tracks():
|
def scan_tracks():
|
||||||
"""Tail the radio logs and forward play events to the bot."""
|
"""Tail the radio logs and forward play events to the bot."""
|
||||||
# On a fresh deploy Liquidsoap may not have written its logs yet; wait for
|
|
||||||
# them instead of dying with FileNotFoundError, which used to silently kill
|
|
||||||
# the now-playing forwarder until the container was restarted.
|
|
||||||
while not (RADIOLOG_PATH.exists() and PERSISTENCE_PATH.exists()):
|
|
||||||
logger.info("Waiting for radio logs (%s, %s)...", RADIOLOG_PATH, PERSISTENCE_PATH)
|
|
||||||
time.sleep(5)
|
|
||||||
with open(RADIOLOG_PATH, "r", encoding=ENCODING) as log_file:
|
with open(RADIOLOG_PATH, "r", encoding=ENCODING) as log_file:
|
||||||
log_file.seek(os.stat(RADIOLOG_PATH).st_size)
|
log_file.seek(os.stat(RADIOLOG_PATH).st_size)
|
||||||
prev_size = os.stat(PERSISTENCE_PATH).st_size
|
prev_size = os.stat(PERSISTENCE_PATH).st_size
|
||||||
|
|||||||
@@ -148,11 +148,8 @@ def consumer(in_q, control_q, doi, live_results, result_list, control_dict, expe
|
|||||||
print(f"Consumer thread started: {no} no")
|
print(f"Consumer thread started: {no} no")
|
||||||
empty_counter = 0
|
empty_counter = 0
|
||||||
alive_no = 0
|
alive_no = 0
|
||||||
# DOI -> result item, so a line is matched with one O(1) dict lookup instead
|
|
||||||
# of scanning every queried DOI. Items are shared with result_list, so
|
|
||||||
# setting exists here is seen by everyone.
|
|
||||||
doi_index = {item["DOI"]: item for item in result_list}
|
|
||||||
while True:
|
while True:
|
||||||
|
done_check = True
|
||||||
try:
|
try:
|
||||||
data = in_q.get(block=True, timeout = 1)
|
data = in_q.get(block=True, timeout = 1)
|
||||||
if data is _sentinel:
|
if data is _sentinel:
|
||||||
@@ -164,21 +161,16 @@ def consumer(in_q, control_q, doi, live_results, result_list, control_dict, expe
|
|||||||
alive_no += 1
|
alive_no += 1
|
||||||
print(f"C{no}__{alive_no}\r", end="")
|
print(f"C{no}__{alive_no}\r", end="")
|
||||||
|
|
||||||
# Each DB line is a DOI (optionally followed by metadata). Match
|
for item in result_list:
|
||||||
# the WHOLE first token exactly - the old `item["DOI"] in data`
|
if item["DOI"] in data and not item["exists"]:
|
||||||
# was a substring test, so a DOI that is a prefix of a longer one
|
print(f"HIT in {no} content {data[0]} line {data[1]} file {data[2]} {item['exists']}")
|
||||||
# (10.1/1 vs 10.1/12) produced a false 'exists' hit.
|
_logger.info(data)
|
||||||
parts = data.split()
|
_logger.info("HIT")
|
||||||
line_doi = parts[0] if parts else ""
|
item["exists"] = True
|
||||||
item = doi_index.get(line_doi)
|
live_results.append(item)
|
||||||
if item is not None and not item["exists"]:
|
done_check = done_check and item["exists"]
|
||||||
print(f"HIT in {no}: {line_doi}")
|
if done_check:
|
||||||
_logger.info("HIT %s", line_doi)
|
control_q.put(_sentinel)
|
||||||
item["exists"] = True
|
|
||||||
live_results.append(item)
|
|
||||||
# All found? Signal producers to stop early (rare -> cheap).
|
|
||||||
if all(it["exists"] for it in result_list):
|
|
||||||
control_q.put(_sentinel)
|
|
||||||
except Empty:
|
except Empty:
|
||||||
empty_counter += 1
|
empty_counter += 1
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|||||||
@@ -52,13 +52,8 @@ class DataModule(commands.Cog):
|
|||||||
# check if current path is a file
|
# check if current path is a file
|
||||||
if os.path.isfile(os.path.join(DIR_PATH_SADOX, path)):
|
if os.path.isfile(os.path.join(DIR_PATH_SADOX, path)):
|
||||||
res.append(path)
|
res.append(path)
|
||||||
if not res:
|
|
||||||
await ctx.send("*Conjurer grzebie w pustej skrzyni* Nie ma dziś żadnych komiksów.")
|
|
||||||
return
|
|
||||||
# randrange(len) is 0..len-1; the old randrange(0, len-1) never picked
|
|
||||||
# the last file and raised ValueError('empty range') on a single file.
|
|
||||||
# trunk-ignore(bandit/B311)
|
# trunk-ignore(bandit/B311)
|
||||||
filename = res[random.randrange(len(res))]
|
filename = res[random.randrange(0, len(res) - 1)]
|
||||||
# select random page
|
# select random page
|
||||||
file = open(DIR_PATH_SADOX + filename, "rb")
|
file = open(DIR_PATH_SADOX + filename, "rb")
|
||||||
if True:
|
if True:
|
||||||
|
|||||||
@@ -94,33 +94,6 @@ def test_survives_invalid_utf8_byte_and_still_finds_later_doi(tmp_path, monkeypa
|
|||||||
assert hit, "DOI after the bad byte was not found - the file was aborted mid-read"
|
assert hit, "DOI after the bad byte was not found - the file was aborted mid-read"
|
||||||
|
|
||||||
|
|
||||||
def test_doi_match_is_exact_not_substring(tmp_path, monkeypatch):
|
|
||||||
# A DB line "10.1/12" must NOT satisfy a search for "10.1/1" (the old
|
|
||||||
# `doi in line` substring test did). The exact DOI must still be found.
|
|
||||||
monkeypatch.setattr(search_bot, "DATABASE_PATH", str(tmp_path) + "/")
|
|
||||||
(tmp_path / "0_chunk.txt").write_text(
|
|
||||||
"10.1/12\n10.1/1\n10.2/999\n", encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
finished, result = _run_bounded([("10.1/1", "DATA"), ("10.9/absent", "DATA")])
|
|
||||||
|
|
||||||
assert finished
|
|
||||||
by_doi = {r["DOI"]: r["exists"] for r in result}
|
|
||||||
assert by_doi["10.1/1"] is True # exact line present -> found
|
|
||||||
assert by_doi["10.9/absent"] is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_doi_match_handles_line_with_trailing_metadata(tmp_path, monkeypatch):
|
|
||||||
# Lines of the form "<DOI>\t<metadata>" still match on the first token.
|
|
||||||
monkeypatch.setattr(search_bot, "DATABASE_PATH", str(tmp_path) + "/")
|
|
||||||
(tmp_path / "0_chunk.txt").write_text("10.5/abc\tsome title here\n", encoding="utf-8")
|
|
||||||
|
|
||||||
finished, result = _run_bounded([("10.5/abc", "DATA")])
|
|
||||||
|
|
||||||
assert finished
|
|
||||||
assert result[0]["exists"] is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_discover_chunk_files_sorted_numerically(tmp_path, monkeypatch):
|
def test_discover_chunk_files_sorted_numerically(tmp_path, monkeypatch):
|
||||||
monkeypatch.setattr(search_bot, "DATABASE_PATH", str(tmp_path) + "/")
|
monkeypatch.setattr(search_bot, "DATABASE_PATH", str(tmp_path) + "/")
|
||||||
for n in (0, 2, 10, 1):
|
for n in (0, 2, 10, 1):
|
||||||
|
|||||||
Reference in New Issue
Block a user