You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
@ViditBot opens a request for a coordinate-less mirror post (backend/app/services/tweet_ingest/resolve.py, backend/app/services/tweet_ingest/urls.py, backend/app/services/detection.py, backend/app/services/events.py, backend/app/services/bot.py, backend/app/models/bot_mention.py, frontend/src/app/import/page.tsx, docs/ingestion.md, docs/data-model.md) (#342). Analysts who mirror a Telegram channel or another account's post, re-uploading the clip and linking the original, got a flat coords_missing refusal: the post carried everything a request needs except the geolocation, which is the one thing a request asks for. The engine now emits a RequestDraft beside that refusal, read off the same source, media, title and date derivations the detections use, when the thread carries footage, it resolved a source, and no coordinate appears anywhere in the thread: a coordinate, own post or quoted post, is a geolocation, never a request. The source's host decides what gets fetched, never whether a request opens: an X status and a public t.me post are chased, so the original's clip and post date fill the row, and a link on any other host, a YouTube video or a news page, opens a request over the analyst's own attached clip with the source's date left unknown. The bot writes it through detection.open_request, which calls the same events.create_request a person's request goes through: the row is owned by and credited to the tagging analyst, stamped requested_at, carrying the post it points at as its source_url and the thread's footage as its one role=source media, with no coordinate. It also carries the same five provenance columns a detection carries, detected_via reading bot, stamped by one shared events.stamp_provenance. A coordinate-less re-tag, of the same post or of a repost of it, lands on that row and moves nothing, so the bot stays silent; a tag carrying the coordinate the analyst has since worked out is a geolocation, so it lands a detected row beside the open request, which stays its owner's to withdraw. A soft-deleted row never matches, so a takedown does not fence the owner off from mirroring that footage again. An X status and a t.me post are stored in one canonical spelling, so two spellings of one of them open one request; every other host is stored as the post wrote it. The footage is an ordered candidate list, the source's media then the analyst's own video, and the first that fetches fills the slot; a chase that failed transiently drafts nothing at all, so the next tag retries it. A clip the evidence intake refuses earns a new footage_unusable refusal in the reply rather than one naming a missing coordinate, and a mirror the branch can never serve, one pointing at a source with no footage to store, earns a new request_not_possible one telling the analyst to attach the clip. The coordinate-less shapes a re-tag or a rewrite still answers keep coords_missing, and so do the paste and the archive, which never ask for a request. The tag earns a linkless ✅ Geolocation request opened · ref … reply under the same per-hour and per-author budget every other reply spends, and the mention is ledgered requested. Nothing else moves: the refusal still travels with the draft, so the paste and the archive, which read detections alone, answer exactly what they did. The reply carries the two warnings a request can earn, source_date_unknown and duplicate_media, read off the same _write_warnings pass the detections run; a request is always born with footage, the analyst's own copy filling the slot when the chase served nothing, so source_fetch_failed never reaches one. The admin reject rate (backend/app/services/admin.py) now reads one Event.is_machine_detection predicate on the model, an imported row that is not a request (requested_at NULL), so a bot-opened request never enters the cohort; the admin panel copy and the event page's provenance row, which reads Requested from on a request, say the same. No migration, since bot_mentions.outcome carries no database domain.
An open request is edited by its owner, and listed on their profile (backend/app/services/events.py, backend/app/routers/events/item.py, frontend/src/app/events/[id]/edit/page.tsx, frontend/src/components/geolocations/edit/RequestEditForm.tsx, frontend/src/components/event/useEventActions.tsx, frontend/src/components/profile/OpenRequests.tsx, frontend/src/lib/events.ts, docs/api.md, docs/data-model.md, docs/ingestion.md) (#342). The bot opens requests on the analyst's behalf and answers Edit it from your profile, and neither half of that was reachable: the owner of a requested row could view it, answer it through the submit form or close it, and the edit address turned every other status away, while the profile listed published work and machine detections and nothing else. POST /events/{id}/request is the owner's correction of an open request, owner-only and requested-only, taking the create form's fields whole on the same rules: the coordinate guess and the camera point stay optional and both-or-neither, the curated floor stays unenforced until the geolocate, the mirrors and their archived copies replace what the row held, and the source media moves on the remove_media_ids + files pair the published writes take, under the same one-source cap and the same source floor. No version is filed: a version supersedes a vouched claim and a request is a question, so the row is overwritten in place and keeps its id, its requested_at, its requester and its provenance columns. source_posted_at is optional on this path alone, since the bot opens requests whose source date it could not read and the owner must not have to invent one. The write takes the same row lock the geolocate, the version and the close take, so an edit racing a fulfilment or a withdrawal serializes with it and the loser sees the 409. The edit address now serves three shapes, a request being the third, and the request page and the event page carry the owner's pencil beside the close. Open requests joins the profile under the submissions, built from the same compact cards, reading the public requested view filtered to that analyst and expanding into search. The POST /events/requests reference rows that claimed the proof body was image-free and that event_time needed event_date are corrected to what the endpoint does.
Changed
The engine's warning and refusal sentences say what to do next (backend/app/services/tweet_ingest/resolve.py, backend/app/services/bot.py, frontend/src/app/import/page.tsx, docs/ingestion.md) (#342). One table words every code for the three surfaces that show it, the bot's reply, the archive's outcome email and the paste's response, so the rewording reaches all three at once: source_missing names the link that is absent rather than the search that failed, the two footage codes say which half came back empty, source_date_unknown and duplicate_media read as the row's state, and coords_invalid gives the bounds a coordinate has to sit in. The bot's request reply opens ✅ Geolocation request opened rather than restating what it did not find.
Fixed
A reply that only inherits the tag from its parent is not a mention (backend/app/services/tweet_ingest/extract.py, backend/app/services/bot.py, backend/app/services/x_api.py, backend/app/routers/webhooks.py, backend/app/models/bot_mention.py, frontend/src/app/import/page.tsx, docs/ingestion.md, docs/data-model.md) (#342). X opens a reply with the parent's author and the parent's mentions minus the replier, so an analyst answering a colleague whose post tagged the bot mentioned the bot without typing a character. That reached the bot on both deliveries, and since the thread under it is the replier's own and carries no coordinate, a linked analyst got a ❌ No coordinate in the post on somebody else's thread for a tag nobody made. A mention now counts as a tag only where its author typed it: on a post that is not a reply every @ViditBot is typed, and on a reply one sitting past the leading run of mentions is, the dot-mention .@ViditBot included, since the period is a character X never writes into a prefix. A reply whose only @ViditBot sits inside that run is settled by its parent, read once through syndication, the free path: a parent that mentions the bot, the bot's own post included, is where the tag came from, so nothing is acquired, nothing is answered and the mention is ledgered inherited. A parent that mentions the bot nowhere leaves the tag typed, so the bare @ViditBot under the analyst's own coordinate post and the climb it opens are untouched, and a parent syndication will not serve reads as inherited too, silence beating a refusal on a thread the tag may never have been meant for. The rule holds inside the analyst's own thread, where a follow-up under a post they already tagged inherits that tag and a re-tag they mean is typed after the prefix. The decision is one pure function on the text (extract.tags_bot), so both deliveries and the parent read share one answer, and the Mention both of them build now carries the parent post id, off referenced_tweets on the billed mentions read and in_reply_to_status_id_str on the webhook event. The loop guard on the failure reply stands: a courtesy answer to the bot that does type a tag still earns no reply. No migration, since bot_mentions.outcome carries no database domain.