Skip to content

feat: add /hive command to create tasks on a remote Hive site - #5

Merged
NagariaHussain merged 5 commits into
bwhtech:developfrom
mainshivam:feat/hive-task-command
Aug 5, 2026
Merged

feat: add /hive command to create tasks on a remote Hive site#5
NagariaHussain merged 5 commits into
bwhtech:developfrom
mainshivam:feat/hive-task-command

Conversation

@mainshivam

@mainshivam mainshivam commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Rewritten after review: tasks are now created on a remote Hive site over its REST API, rather than in the same site. The conversation itself is unchanged.

Why?

The bot runs on our HR site, which has no Hive doctypes. Hive lives on a separate site. Previously /hive wrote to local Hive doctypes, which meant both apps had to be installed together - that is not how our sites are set up.

What?

/hive creates a Hive Task on a configured remote Frappe site.

Configuration is a new Hive Sites table on BWH Bot Settings: one row per remote site with its URL, API key and API secret. The secret is a Password field, so it is encrypted at rest and never rendered back.

The conversation keeps the same shape: title -> optional description -> start date -> end date -> project -> assignees (multi-select, tap to toggle) -> review -> create. Projects and members are fetched from the remote site instead of queried locally, and the chosen project is shown by title rather than id.

If more than one site is configured the flow asks which one first. With a single site it is exactly as many steps as before.

How?

bwh_bot/hive_client.py is a thin REST client:

  • Auth is the standard Authorization: token key:secret header.
  • Requests time out after 10s, since they run inside a Telegram webhook and must not hold the worker open on an unreachable host.
  • Assignees go through frappe.desk.form.assign_to.add, which is how Hive tracks them - they live in _assign, not a child table (see Hive's own migrate_assignees_to_assign patch).
  • Failures raise HiveSiteError carrying the site label and the remote message, never a traceback.
  • Assignment is best effort: if only the assign step fails, the task is still reported as created, with a warning, rather than leaving the user unsure.
  • The site row is re-read on each step, because a conversation outlives the request that started it and the row may have been edited or disabled meanwhile.

BotConversation gains requires_employee (default True, so leave, WFH and petty cash are untouched). Creating a remote task does not need the sender resolved to an Employee.

Also fixed

Command descriptions in the Telegram menu. Every command showed "Apply for Work From Home". Each conversation registers the same bound BotConversation.handle_command, so fn.__func__._description = description wrote to one shared function object and the last registration won for all of them. /ping was unaffected because it is a plain function. Descriptions now live in a dict keyed by command.

install-app on a site without Frappe HR. after_install created a custom field on Attendance Request unconditionally, so installing failed outright on any site without HR - including CI, which installs onto a bare site. Absent doctypes are now skipped; after_migrate re-runs it, so the field still appears if HR is installed later.

Tests

bwh_bot/tests/test_hive.py, 50 tests, the first in this app. Telegram and HTTP are both stubbed, so every test runs on any site with no Hive installed - nothing skips.

  • conversation: all fields, skipping description, assignee toggle on and off, project titles as buttons, chosen project shown by title, date and title validation
  • custom dates: the "Custom date..." replies route through the base class into on_text_input rather than on_action - custom start, custom end, end before start, unparseable input
  • navigation: the full back chain review -> assignees -> project -> end date -> start date -> description, going back and re-picking without losing earlier answers, cancel, and pressing a button with no active session
  • site selection: single site skips the picker, no sites explains itself, inactive rows ignored, multiple sites prompt, site removed mid-conversation
  • remote edge cases: no open projects, no active members, unreachable while listing projects, unreachable while listing members, create failure reported without raising, assign failure still reporting the task created
  • client: token header, missing secret, trailing slash in URL, label falling back to host, HTTP error wrapping, unreachable host, non-JSON response, error text falling back to exc_type without leaking a traceback, query shape for both lists, assignment endpoint
  • install: the HR guard, so it cannot regress silently
bench --site <site> run-tests --app bwh_bot --module bwh_bot.tests.test_hive

Verified

Beyond the mocked tests, the REST layer was exercised between two real sites - the bot on the HR site, Hive on another with bwh_bot not installed at all. Fetched projects and members, created a task and assigned it, then confirmed on the remote side that title, description, project, both dates, status and _assign all landed correctly. The full Telegram flow was also driven end to end through a tunnelled webhook.

Install hooks were additionally verified against a site with neither HR nor the bot's own HR doctypes present.

Note on the style commit

style: applies the ruff formatter and fixes 3 RUF005 lint errors across the app. CI runs pre-commit with --all-files and the committed tree did not satisfy the repo's own ruff config, so every pull request failed the linter before reaching its own changes. The linter job only runs on pull_request, which is why this was never caught on develop. It is formatting only, no behaviour change, and can be split into its own PR if preferred.

Worth noting for contributors: the pre-commit hook was not installed locally anywhere, which is how the drift accumulated. pre-commit install prevents a recurrence.

@mainshivam
mainshivam force-pushed the feat/hive-task-command branch 2 times, most recently from 71587d1 to 4b203f5 Compare July 29, 2026 15:58
@mainshivam
mainshivam requested a review from gajjug004 July 29, 2026 16:06
CI runs pre-commit with --all-files, and the committed tree did not
satisfy the repo's own ruff config, so every pull request failed the
linter before reaching its own changes. The linter job only runs on
pull_request, which is why this drift was never caught on develop.

Formatting only, plus RUF005 (use iterable unpacking instead of list
concatenation) in confirm_buttons and the two _summary_buttons helpers.
No behaviour changes.
The bot runs on our HR site, which has no Hive doctypes. This adds a
/hive command that creates a Hive Task on a separate Frappe site over
its REST API, so neither app has to be installed alongside the other.

Configuration lives in a new Hive Sites table on BWH Bot Settings: one
row per remote site with its URL, API key and API secret. The secret is a
Password field, so it is encrypted at rest and never rendered.

The conversation is unchanged in shape: title, optional description,
start date, end date, project, then a multi-select assignee picker.
Projects and members are fetched from the remote site rather than queried
locally, and the chosen project is labelled by title rather than id.

If more than one site is configured the flow asks which one first, so the
single-site case stays exactly as many steps as before.

Notes on the remote calls (bwh_bot/hive_client.py):

- Auth is the standard `Authorization: token key:secret` header.
- Requests time out after 10s, since they run inside a Telegram webhook
  and must not hold the worker open on an unreachable host.
- Assignees go through frappe.desk.form.assign_to.add, which is how Hive
  tracks them: they live in `_assign`, not a child table.
- Failures raise HiveSiteError carrying the site label and the remote
  message, never a traceback. Assignment is best effort, so the task is
  still reported as created if only the assign step fails.
- The site row is re-read on each step, because a conversation outlives
  the request that started it and the row may be edited or disabled.

Also fixes the command descriptions in the Telegram menu. Every
conversation registers the same bound BotConversation.handle_command, so
storing the description on the function object meant each registration
overwrote the previous one and all commands showed the last one ("Apply
for Work From Home"). Descriptions now live in a dict keyed by command.

BotConversation gains `requires_employee` (default True, so leave, WFH
and petty cash are unchanged) because creating a remote task does not
need the sender resolved to an Employee.

Adds bwh_bot/tests/test_hive.py: 30 tests covering the conversation, site
selection, remote failure handling and the REST client. Both Telegram and
HTTP are stubbed, so they run on any site.
@mainshivam
mainshivam force-pushed the feat/hive-task-command branch from 4b203f5 to 69f3b7c Compare July 29, 2026 20:37
@mainshivam mainshivam changed the title feat: add /hive command to create Hive tasks from Telegram feat: add /hive command to create tasks on a remote Hive site Jul 29, 2026
@mainshivam
mainshivam marked this pull request as draft July 29, 2026 20:38
install-app failed outright on a site without Frappe HR, because
after_install created a custom field on Attendance Request. CI installs
bwh_bot on a bare site, so the Server job never reached the tests.

_make_custom_fields now skips doctypes that are absent. after_migrate
re-runs it, so the field still appears if HR is installed later.

Adds tests for the paths that had none:

- the install guard itself, so this cannot regress silently
- the "Custom date..." replies, which route through the base class into
  on_text_input rather than on_action: custom start, custom end, an end
  date before the start, and an unparseable date
- the unknown-action fallback
- hive_client.list_members query shape and active_sites skipping disabled
  rows
- a non-JSON response, and error text falling back to exc_type without
  leaking the traceback

42 tests, all passing on a site with HR and Hive, and verified that the
install hooks run clean on a site with neither.
The guest-whitelisted-method rule blocks on hook(). Telegram calls the
webhook unauthenticated by design, so it cannot require a session: the
shared secret header is what authenticates it, and the chat whitelist
gates each update before any handler runs. Documented and marked
nosemgrep, as the rule asks.
@mainshivam
mainshivam marked this pull request as ready for review July 29, 2026 21:17
Comment thread bwh_bot/api/telegram.py Outdated
Comment on lines +14 to +16
# Keyed by command rather than stashed on the function: every conversation
# registers the same bound BotConversation.handle_command, so an attribute
# on the function is shared and the last registration wins for all of them.

@gajjug004 gajjug004 Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/api/telegram.py Outdated
Comment on lines +32 to +33
# Importing the handler modules is what registers them: ping registers a plain
# command, and the conversation subclasses self-register via __init_subclass__.

@gajjug004 gajjug004 Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/api/telegram.py Outdated
Comment on lines +45 to +48
# Telegram calls this webhook unauthenticated by design, so it cannot require a
# session. The shared secret checked below (X-Telegram-Bot-Api-Secret-Token) is
# what authenticates the request, and each update is gated again by the chat
# whitelist before any handler runs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6. The # nosemgrep annotation on the decorator stays, since that is what the scanner reads.

Comment thread bwh_bot/api/telegram.py Outdated
"message_date": message_date,
"payload": json.dumps(data, indent=2),
})
doc = frappe.get_doc(

@gajjug004 gajjug004 Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can extract the Telegram Webhook Log into a separate function as it can be used in multiple places. For example, the same logic is repeated at line 115, which violates the DRY principle.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Extracted into _log_update(data, message, telegram_user, update_type, **fields) in 29851b6. Both callers now pass only what differs — command/message_text from hook(), callback_query_id/callback_data from _handle_callback_query() — and the shared chat/user/message/payload fields are built once.

Comment thread bwh_bot/handlers/hive.py Outdated
Comment on lines +16 to +17
# Status a Telegram-created task lands in. Every other Hive Task status is a
# valid transition target from this one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/handlers/hive.py Outdated
Comment on lines +28 to +29
# Tasks are created on a remote Hive site over its API, so this flow needs
# neither Frappe HR nor Hive installed alongside the bot.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/handlers/hive.py Outdated
# neither Frappe HR nor Hive installed alongside the bot.
requires_employee = False

# --- Step 1: pick a site (skipped when only one is configured) ------------

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove this

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/handlers/hive.py Outdated
message_thread_id=message_thread_id,
)

# --- Free-text steps: title, description ---------------------------------

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/handlers/hive.py Outdated
Comment on lines +105 to +108
# --- Date steps ----------------------------------------------------------
# Quick-pick buttons emit `from`/`to` actions (see on_action). The base class
# routes the "Custom date..." buttons through awaiting_from_date /
# awaiting_to_date and hands the parsed date to on_text_input.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/handlers/hive.py Outdated
self.update_state(state, "select_project", {"due_date": date_str})
self._prompt_project(state, chat_id, message_thread_id=message_thread_id)

# --- Button actions ------------------------------------------------------

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this one too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/handlers/hive.py Outdated
self.update_state(state, "select_assignees")
self._prompt_assignees(state, chat_id, message_id=message_id)

# --- Prompts -------------------------------------------------------------

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this one too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/handlers/hive.py Outdated
Comment on lines +273 to +274
# Cache the titles so later steps can label the chosen project without
# another round trip to the remote site.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this please

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/handlers/hive.py Outdated
message_id=message_id,
)

# --- Creation ------------------------------------------------------------

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for this. please remove this comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/handlers/hive.py Outdated
Comment on lines +395 to +396
# Assignment is best-effort: the task exists on the remote site either way,
# so report success rather than leaving the user unsure what happened.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/handlers/hive.py Outdated
parse_mode="HTML",
)

# --- Helpers -------------------------------------------------------------

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/conversation.py Outdated
Comment on lines +23 to +24
# HR-backed flows (leave, WFH, petty cash) need the sender resolved to an
# Employee. Flows that do not touch HR doctypes set this to False.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this comment too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/hive_client.py Outdated
return f"{_base(site.site_url)}/app/hive-task/{task_name}"


# --- internals -----------------------------------------------------------

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

Comment thread bwh_bot/hive_client.py Outdated
Comment on lines +152 to +153
# Frappe reports failures as `exception`, or as `exc_type` plus an `exc`
# traceback. Prefer the human-readable keys and never surface the traceback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be simplified.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified in 29851b6 — the loop now uses a walrus binding:

for key in ("exception", "message", "exc_type", "_server_messages"):
	if value := payload.get(key):
		return str(value)[:200]

The explanatory comment above it is gone too.

Comment thread bwh_bot/install.py Outdated
Comment on lines +32 to +35
# The doctypes we extend ship with Frappe HR, which is optional: the bot is
# installable on a site that only uses the non-HR flows, and CI installs it
# on a bare site. Skip anything absent; after_migrate re-runs this, so the
# fields appear if HR is installed later.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment please!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 29851b6.

- Remove the explanatory and section-divider comments called out in
  review across telegram.py, hive.py, conversation.py, hive_client.py
  and install.py.
- Extract the duplicated Telegram Webhook Log creation in hook() and
  _handle_callback_query() into _log_update(), with the differing fields
  passed by the caller.
- Simplify the error-key lookup in _error_text() with a walrus binding.
@mainshivam

Copy link
Copy Markdown
Collaborator Author

@gajjug004 all review comments are addressed in 29851b6, pushed just now.

Comments removed — every explanatory and section-divider comment you flagged, across api/telegram.py, handlers/hive.py, conversation.py, hive_client.py and install.py. The # nosemgrep annotation on the webhook decorator stays, since that one is read by the scanner rather than by a person.

DRY on the webhook log — the duplicated Telegram Webhook Log creation in hook() and _handle_callback_query() is now a single _log_update(data, message, telegram_user, update_type, **fields). Callers pass only what differs: command/message_text from the message path, callback_query_id/callback_data from the callback path.

_error_text() simplified — the key lookup is a walrus binding now instead of a separate assignment and truthiness check.

Verified: pre-commit run --all-files passes clean, and all 50 tests in bwh_bot.tests.test_hive pass. No behaviour change in any of this.

@mainshivam
mainshivam requested a review from gajjug004 August 5, 2026 04:47
@NagariaHussain
NagariaHussain merged commit 714c3e7 into bwhtech:develop Aug 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants