Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Telegram cannot parse goes again as plain text. The gestures go after the text,
so one Telegram refuses never loses the answer. The bot's voice (`telegram.style`) is read by the
assistant after the formatting rules.

Every offer says «Si no respondes, no hago nada.» and, untapped, gives way to «Sin respuesta: nada».
An offer nobody taps gives way to «Sin respuesta: nada».

## Approvals and calendars

Expand Down
2 changes: 1 addition & 1 deletion supermedicaldoctor/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""SuperMedicalDoctor: a household doctor assistant that lives in a Telegram group."""

__version__ = "0.2.1"
__version__ = "0.2.2"
9 changes: 4 additions & 5 deletions supermedicaldoctor/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2970,15 +2970,14 @@ def say(
thread: int | None = None,
) -> bool:
"""An assistant answer: plain text, a reply to the message, split at Telegram's limit.
With choices its last part carries them as buttons, for `users` to tap, and a line
saying nothing happens without an answer; without, one that ends with a question
With choices its last part carries them as buttons, for `users` to tap (a Sí/No pair with
a line saying how to answer something else); without, one that ends with a question
carries a selective force_reply. False when a part could not be sent."""
key = secrets.token_hex(4) if offered else None
asks = text.rstrip().endswith("?")
text = render.brief(text) # a few lines, the rest folded: no page holds the detail here
if offered:
hint = self.t("yes_no_hint" if self.yes_no(offered) else "options_hint")
text = f"{text}\n\n{hint}"
if offered and self.yes_no(offered):
text = f"{text}\n\n{self.t('yes_no_hint')}"
parts = render.split(text)
sent = None
for i, part in enumerate(parts):
Expand Down
13 changes: 6 additions & 7 deletions supermedicaldoctor/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ appt_line: "{when}, {what}"
appt_place: " ({place})"
at_time: "{date} at {time}"
prep_line: "• {text} ({hours} h before)"
appt_offer: "{m}, shall I put it in your calendar? If you do not answer, I do nothing."
appt_offer: "{m}, shall I put it in your calendar?"
appt_calendar: "In the calendar"
event_title: "Appointment: {what}"
appt_calendar_turn: "[asked with the «In the calendar» button of an appointment] Put in my calendar one event «{title}» on {when} ({iso}), at {place}, with the preparation in the description ({prep}) and a reminder 24 h before."
Expand All @@ -61,7 +61,7 @@ remind_prep_from: "⏰ {m}: from {start}, {text}: {what}, {day} at {time}."
today: "today"
tomorrow: "tomorrow"
on_date: "on {date}"
followup: "{m}, how did the {what} appointment go? If they gave you a report or changes, tell me or send me the PDF.\n\nIf you do not answer, I do nothing."
followup: "{m}, how did the {what} appointment go? If they gave you a report or changes, tell me or send me the PDF."
followup_yes: "Yes, I'll tell you"
followup_no_log: "Appointment marked as done, with nothing new."
tell_me: "{m}, tell me. I am listening."
Expand Down Expand Up @@ -122,8 +122,7 @@ button_yes: "Yes"
button_no: "No"
button_approve: "Approve"
button_reject: "Reject"
options_hint: "If you do not answer, I do nothing."
yes_no_hint: "Tap Yes or No. If you want something else, reply to this message. If you do not answer, I do nothing."
yes_no_hint: "Tap Yes or No. If you want something else, reply to this message."
tap_gone: "This is no longer waiting for an answer."
tap_not_yours: "Only {name} can answer this."
ask_confirm: "{name}, do you approve?\n{summary}"
Expand All @@ -134,7 +133,7 @@ ask_edited: "You edited your message, so I did not make this change. Nothing cha
ask_preapproved: "Approved with the «In the calendar» button: {what}\n{result}"

# --- reminders offered with buttons; /recordatorios ----------------------------------------
remind_offer: "{name}, shall I remind you?\n{lines}\n\nIf you do not answer, I do nothing."
remind_offer: "{name}, shall I remind you?\n{lines}"
remind_here: "Reminder here"
remind_calendar: "In the calendar"
remind_nothing: "No"
Expand All @@ -158,15 +157,15 @@ settings_day_before_at: "the evening-before reminder, at {time}"
settings_followup_on: "I ask after each appointment"
settings_followup_off: "I no longer ask after each appointment"
settings_instructions: "my standing instructions"
instr_ask: "{name}, this removes or changes lines of my instructions. Shall I leave them like this?\n{lines}\n\nIf you do not answer, I do nothing."
instr_ask: "{name}, this removes or changes lines of my instructions. Shall I leave them like this?\n{lines}"
instr_kept: "OK, I keep my instructions as they were."
edit_refused: "I could not make that change to the files: it is not allowed here or I did not understand it. I touched nothing."
edit_check_failed: "That change left the data with errors, so I touched nothing."
edited: "Updated:"
edit_file: "• {path}"
edit_deleted: "• {path}: deleted"
edit_undo_said: "changes to {paths}"
edit_ask: "{name}, shall I make this change?\n{lines}\n\nIf you do not answer, I do nothing."
edit_ask: "{name}, shall I make this change?\n{lines}"
edit_kept: "OK, I leave it as it was."
edit_moved: "Those files changed since I asked, so I touched nothing. Ask me again."
instr_moved: "My instructions changed since I asked, so I touched nothing. Ask me again."
Expand Down
13 changes: 6 additions & 7 deletions supermedicaldoctor/locales/es.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ appt_line: "{when}, {what}"
appt_place: " ({place})"
at_time: "{date} a las {time}"
prep_line: "• {text} ({hours} h antes)"
appt_offer: "{m}, ¿la pongo en tu calendario? Si no respondes, no hago nada."
appt_offer: "{m}, ¿la pongo en tu calendario?"
appt_calendar: "En el calendario"
event_title: "Cita: {what}"
appt_calendar_turn: "[pedido con el botón «En el calendario» de una cita] Pon en mi calendario un evento «{title}» el {when} ({iso}), en {place}, con la preparación en la descripción ({prep}) y un aviso 24 h antes."
Expand All @@ -61,7 +61,7 @@ remind_prep_from: "⏰ {m}: a partir de las {start}, {text}: {what}, {day} a las
today: "hoy"
tomorrow: "mañana"
on_date: "el {date}"
followup: "{m}, ¿qué tal la cita de {what}? Si te dieron un informe o cambios, cuéntamelo o mándame el PDF.\n\nSi no respondes, no hago nada."
followup: "{m}, ¿qué tal la cita de {what}? Si te dieron un informe o cambios, cuéntamelo o mándame el PDF."
followup_yes: "Sí, te cuento"
followup_no_log: "Cita marcada como hecha, sin más novedades."
tell_me: "{m}, cuéntame. Te leo."
Expand Down Expand Up @@ -122,8 +122,7 @@ button_yes: "Sí"
button_no: "No"
button_approve: "Aprobar"
button_reject: "Rechazar"
options_hint: "Si no respondes, no hago nada."
yes_no_hint: "Toca Sí o No. Si quieres otra cosa, responde a este mensaje. Si no respondes, no hago nada."
yes_no_hint: "Toca Sí o No. Si quieres otra cosa, responde a este mensaje."
tap_gone: "Esto ya no está esperando respuesta."
tap_not_yours: "Esto solo lo puede responder {name}."
ask_confirm: "{name}, ¿lo apruebas?\n{summary}"
Expand All @@ -134,7 +133,7 @@ ask_edited: "Has editado tu mensaje, así que no he hecho este cambio. No he cam
ask_preapproved: "Aprobado con el botón «En el calendario»: {what}\n{result}"

# --- recordatorios ofrecidos con botones; /recordatorios -----------------------------------
remind_offer: "{name}, ¿quieres que te lo recuerde?\n{lines}\n\nSi no respondes, no hago nada."
remind_offer: "{name}, ¿quieres que te lo recuerde?\n{lines}"
remind_here: "Recordatorio aquí"
remind_calendar: "En el calendario"
remind_nothing: "No"
Expand All @@ -158,15 +157,15 @@ settings_day_before_at: "el aviso de la víspera, a las {time}"
settings_followup_on: "pregunto después de cada cita"
settings_followup_off: "ya no pregunto después de cada cita"
settings_instructions: "mis instrucciones"
instr_ask: "{name}, esto quita o cambia líneas de mis instrucciones. ¿Lo dejo así?\n{lines}\n\nSi no respondes, no hago nada."
instr_ask: "{name}, esto quita o cambia líneas de mis instrucciones. ¿Lo dejo así?\n{lines}"
instr_kept: "Vale, dejo mis instrucciones como estaban."
edit_refused: "No he podido hacer ese cambio en los archivos: aquí no está permitido o no lo he entendido bien. No he tocado nada."
edit_check_failed: "Ese cambio dejaba los datos con errores, así que no he tocado nada."
edited: "Actualizado:"
edit_file: "• {path}"
edit_deleted: "• {path}: borrado"
edit_undo_said: "cambios en {paths}"
edit_ask: "{name}, ¿hago este cambio?\n{lines}\n\nSi no respondes, no hago nada."
edit_ask: "{name}, ¿hago este cambio?\n{lines}"
edit_kept: "Vale, lo dejo como estaba."
edit_moved: "Esos archivos han cambiado desde que lo pregunté, así que no he tocado nada. Pídemelo otra vez."
instr_moved: "Mis instrucciones han cambiado desde que lo pregunté, así que no he tocado nada. Pídemelo otra vez."
Expand Down
2 changes: 1 addition & 1 deletion supermedicaldoctor/principles/en.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ They hold on every turn. The rules after them say how they apply to this bot; no
if it changes anything, say so. The text with a photo or a file is a message.
7. **Reversible now, irreversible with a tap.** Do at once what was asked and can be undone. Anything
that leaves the chat (a calendar, a file others see, a purchase) waits until someone taps Approve.
An offer nobody answers does nothing ("If you do not answer, I do nothing.").
An offer nobody answers does nothing.
8. **Proactive, not pushy.** When you can do something useful that was not asked (a reminder, a
calendar entry, a re-check), offer it with buttons, once. If nobody takes it, do not repeat that
offer in your next answer.
Expand Down
2 changes: 1 addition & 1 deletion supermedicaldoctor/principles/es.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Valen en cada turno. Las reglas que vienen después dicen cómo se aplican en es
algo, dilo. El texto que acompaña a una foto o a un archivo es un mensaje.
7. **Lo reversible, ya; lo irreversible, con un toque.** Haz en el momento lo que te piden y se puede
deshacer. Lo que sale del chat (un calendario, un archivo que ven otros, una compra) espera a que
alguien pulse Aprobar. Una oferta que nadie contesta no hace nada («Si no respondes, no hago nada.»).
alguien pulse Aprobar. Una oferta que nadie contesta no hace nada.
8. **Proactivo, sin insistir.** Cuando puedas hacer algo útil que no te han pedido (un recordatorio,
una cita en el calendario, volver a comprobar algo), ofrécelo con botones, una vez. Si nadie la
coge, no repitas esa oferta en la respuesta siguiente.
Expand Down
10 changes: 4 additions & 6 deletions tests/test_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,7 @@ def test_a_new_appointment_is_offered_for_its_persons_calendar(make_bot):
plugin(bot).REPLIES[:] = [record("Vale.", ADD)]
say(bot, "x", user=PABLO, mention=True)
offer = last_sent(bot)
assert offer["text"].endswith(
"@lucia_tg, ¿la pongo en tu calendario? Si no respondes, no hago nada."
)
assert offer["text"].endswith("@lucia_tg, ¿la pongo en tu calendario?")
data = buttons(offer)
assert [t for t, _ in data] == ["En el calendario", "No"]
tap(bot, data[0][1], user=PABLO)
Expand Down Expand Up @@ -421,7 +419,7 @@ def test_the_ask_after_an_appointment_and_its_no_marks_it_done(make_bot, data_re
bot.now.at = at(TODAY.replace(day=12), 12, 30)
bot.tick(bot.now())
ask = next(p for p in sent(bot) if "¿qué tal la cita de Analítica de control?" in p["text"])
assert ask["text"].endswith("Si no respondes, no hago nada.")
assert ask["text"].endswith("cuéntamelo o mándame el PDF.")
yes, no = buttons(ask)
assert (yes[0], no[0]) == ("Sí, te cuento", "No")
tap(bot, no[1], user=PABLO)
Expand Down Expand Up @@ -467,7 +465,7 @@ def test_a_reminder_for_someone_else_is_offered_to_them(make_bot):
offer = last_sent(bot)
assert offer["text"] == (
'<a href="tg://user?id=222">Pablo</a>, ¿quieres que te lo recuerde?\n'
"jueves 11 de marzo a las 8:00: Toma el omega\n\nSi no respondes, no hago nada."
"jueves 11 de marzo a las 8:00: Toma el omega"
)
here = buttons(offer)[0][1]
tap(bot, here, user=LUCIA)
Expand Down Expand Up @@ -562,7 +560,7 @@ def test_a_whole_text_that_drops_lines_is_asked_and_nothing_changes_without_si(m
)
assert "− Regla 1 de la casa." in ask["text"] and "− Regla 10 de la casa." in ask["text"]
assert "+ Contesta siempre con la fuente." in ask["text"]
assert ask["text"].endswith("Si no respondes, no hago nada.")
assert ask["text"].endswith("+ Contesta siempre con la fuente.")
yes, no = buttons(ask)
assert (yes[0], no[0]) == ("Sí", "No")
tap(bot, yes[1], user=LUCIA) # not an admin
Expand Down
9 changes: 4 additions & 5 deletions tests/test_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ def test_choices_come_as_buttons_and_a_tap_is_that_persons_next_message(make_bot
say(bot, "me han puesto hierro", mention=True)
answer = last_sent(bot)
assert answer["text"] == (
"¿Lo apunto?\n\nToca Sí o No. Si quieres otra cosa, responde a este mensaje. Si no "
"respondes, no hago nada."
"¿Lo apunto?\n\nToca Sí o No. Si quieres otra cosa, responde a este mensaje."
)
yes = buttons(answer)[0][1]
tap(bot, yes, user=PABLO)
Expand All @@ -64,12 +63,12 @@ def test_choices_come_as_buttons_and_a_tap_is_that_persons_next_message(make_bot
assert contents(plugin(bot).CALLS[-1])[-1] == "Lucía: Sí"


def test_choices_carry_only_the_no_answer_line_under_them(make_bot):
def test_choices_carry_no_line_under_them(make_bot):
bot = make_bot()
plugin(bot).REPLIES[:] = ["¿Cuál?\n\n```options\n- Mañana\n- Tarde\n```"]
say(bot, "x", mention=True)
# the buttons say what they do: no line inviting something else
assert last_sent(bot)["text"] == "¿Cuál?\n\nSi no respondes, no hago nada."
# the buttons say what they do, and untapped they expire: no line under the question
assert last_sent(bot)["text"] == "¿Cuál?"


def test_choices_nobody_taps_expire_into_nothing(make_bot):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_edits.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def test_a_proposed_edit_waits_for_si_and_no_leaves_it(make_bot, data_repo):
say(bot, "hoy he ido al centro", user=LUCIA, mention=True)
ask = last_sent(bot)
assert ask["text"].startswith("@lucia_tg, ¿hago este cambio?\n• people/lucia/notes.md\n+ ")
assert ask["text"].endswith("Si no respondes, no hago nada.")
assert "no respondes" not in ask["text"]
assert data_repo.remote_log() == before
no = buttons(ask)[1][1]
tap(bot, no, user=PABLO)
Expand Down
7 changes: 7 additions & 0 deletions tests/test_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ def test_every_english_key_exists_with_the_same_placeholders(locale):
assert shape(en[key]) == shape(other[key]), key


@pytest.mark.parametrize("locale", ["en", "es"])
def test_no_message_says_what_happens_without_an_answer(locale):
# an untapped offer just expires; no message adds a line saying so
for path, s in walk(_load(locale)):
assert "no respondes" not in s.lower() and "do not answer" not in s.lower(), path


def test_the_shape_check_can_fail():
"""Positive control: a placeholder renamed in a translation is caught."""
assert shape("{m}, cuéntame") != shape("{name}, cuéntame")
Expand Down
4 changes: 2 additions & 2 deletions tests/test_principles.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
# The shared assistant principles, byte for byte. A change to them lands here on purpose, never by
# accident: copy the new text into supermedicaldoctor/principles/ and update the hash.
SHA256 = {
"en": "713784a662236621bf63709f07ac811199b2bf779e95b986a475e2087ab1713a",
"es": "dbd6efd497a264a8dd43e6f5b0ceeb78d3b97c5d61536fceb03505c970937bd1",
"en": "1132dd4ab7570d7bdf96146532e2f8d71052091ef657c9332aff5a9fe3a6cae3",
"es": "1d47e3c8292543d302add5d0caab2ccda46b95ec1ba49d661d035f6f31e8c312",
}
HEADING = {"en": "## Principles", "es": "## Principios"}
PEOPLE = {"lucia": "Lucía", "pablo": "Pablo"}
Expand Down
Loading