<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ALEKSANDR MICHAILOVICH</title>
    <description>The latest articles on DEV Community by ALEKSANDR MICHAILOVICH (@dev24pro).</description>
    <link>https://dev.to/dev24pro</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4182452%2F6b3d5eb1-9638-46f1-b96b-36a929c9e0e2.jpg</url>
      <title>DEV Community: ALEKSANDR MICHAILOVICH</title>
      <link>https://dev.to/dev24pro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dev24pro"/>
    <language>en</language>
    <item>
      <title>CRM CSV Exports: Quarantine Suspicious Fields Before Handoff [cross-post]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Fri, 30 Oct 2026 20:08:41 +0000</pubDate>
      <link>https://dev.to/dev24pro/crm-csv-exports-quarantine-suspicious-fields-before-handoff-cross-post-4l0g</link>
      <guid>https://dev.to/dev24pro/crm-csv-exports-quarantine-suspicious-fields-before-handoff-cross-post-4l0g</guid>
      <description>&lt;h2&gt;
  
  
  Why a CRM CSV export needs its own check
&lt;/h2&gt;

&lt;p&gt;Separate machine transfer from spreadsheet viewing before handing an export to a sales representative. User-entered fields can become formulas in the application that opens the file. Under an agreed policy, hold suspicious rows for review while retaining their original values in protected storage. Putting fields inside quotation marks does not establish that formulas are safe.&lt;/p&gt;

&lt;p&gt;OWASP describes CSV Injection as untrusted input entering a CSV and subsequently being interpreted by spreadsheet software. In particular, a cell beginning with &lt;code&gt;=&lt;/code&gt; may be treated as a formula. CWE-1236 documents the weakness and the limitations of mitigation techniques.[1][2]&lt;/p&gt;

&lt;p&gt;A company name, an enquiry comment, or a field copied from another service can contain untrusted text. Passing through a trusted CRM does not change its origin. Permission to download a report does not establish that every value is safe for the next application.&lt;/p&gt;

&lt;p&gt;This article proposes a policy for an export handler you control. It does not describe a verified built-in 24go.site feature, a universal defense for every spreadsheet, or permission to collect other people's contacts. The example uses synthetic records only. It checks handler decisions, does not launch Excel or Calc, and makes no CRM requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agree on who will open the file
&lt;/h2&gt;

&lt;p&gt;An export needs a named consumer. A machine-transfer file must preserve values according to an explicit data contract. A file intended for human viewing needs application-specific behavior checks. Button labels, format descriptions, and storage routes should make that distinction visible.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Agree on&lt;/th&gt;
&lt;th&gt;Treat as an error&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Machine transfer&lt;/td&gt;
&lt;td&gt;Schema, encoding, delimiter, empty-value handling&lt;/td&gt;
&lt;td&gt;Silently changing a phone number or comment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sales representative's review&lt;/td&gt;
&lt;td&gt;Application, version, import settings, exception policy&lt;/td&gt;
&lt;td&gt;Interpreting untrusted text as a formula&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exception investigation&lt;/td&gt;
&lt;td&gt;Owner, access, retention, viewing method&lt;/td&gt;
&lt;td&gt;Moving the original suspect values into an ordinary CSV&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not label a machine-transfer CSV safe to open by double-clicking. It has a different contract with its recipient. JSON with an explicit schema may be a suitable choice when exact text transfer matters, but JSON itself cannot control what the recipient does next. A later conversion into a spreadsheet creates another boundary that needs checking.&lt;/p&gt;

&lt;p&gt;Start a human-readable report with the smallest useful set of columns. If the recipient needs only an enquiry ID, status, and date, omit the full conversation. Reducing fields reduces the amount of untrusted text and disclosed information; it does not replace validation of the values that remain.&lt;/p&gt;

&lt;p&gt;The handoff sits alongside &lt;a href="https://www.24go.site/blog/integratsiya-telegram-lidov-v-crm?locale=en" rel="noopener noreferrer"&gt;Telegram-to-CRM integration&lt;/a&gt;, but export checking addresses a separate task after a record has been stored. The policy here does not verify the product settings or capabilities mentioned in historical blog articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep CSV serialization separate from formula policy
&lt;/h2&gt;

&lt;p&gt;A CSV library handles field and row boundaries. Your content policy decides which values may be handed to a particular consumer. Run these checks in sequence. Reading a file back successfully with the same library verifies the transfer format, not spreadsheet formula behavior.&lt;/p&gt;

&lt;p&gt;Python's documentation describes &lt;code&gt;csv.writer&lt;/code&gt;, delimiter and quoting options, and opening CSV files with &lt;code&gt;newline=''&lt;/code&gt;. By default, &lt;code&gt;csv.reader&lt;/code&gt; returns strings without automatic type conversion. That is Python parser behavior, not a promise about how a spreadsheet will open the same file.[3]&lt;/p&gt;

&lt;p&gt;Do not construct a row using &lt;code&gt;','.join(...)&lt;/code&gt;. Commas, quotation marks, and line breaks inside a comment must remain inside that field. The example uses &lt;code&gt;csv.writer&lt;/code&gt; to quote every field, then &lt;code&gt;csv.reader&lt;/code&gt; to check the round trip. It applies the suspicious-prefix policy before writing; the &lt;code&gt;QUOTE_ALL&lt;/code&gt; option does not replace that policy.&lt;/p&gt;

&lt;p&gt;OWASP lists risky beginnings including &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;@&lt;/code&gt;, tab, carriage return, line feed, and some full-width character variants. It also warns that delimiters and quotes can create a new cell boundary when handled incorrectly.[1] This gives you test cases, not evidence that a particular detector covers every interpretation.&lt;/p&gt;

&lt;p&gt;Do not remove the first character from the original CRM record. A plus can belong to a phone number, a minus can be meaningful text, and an apostrophe can belong to a name. If you choose a viewing transformation, retain the original separately and document what the recipient will see. Apply the change in the presentation layer and test it in the target application.&lt;/p&gt;

&lt;h2&gt;
  
  
  What whole-row quarantine means
&lt;/h2&gt;

&lt;p&gt;Under this proposed policy, a row containing a suspicious field is withheld from the human-view CSV. A separate exception log receives its ID, column name, and reason code. The original value stays in the system under restricted access. The handler does not automatically repair the value or delete the enquiry.&lt;/p&gt;

&lt;p&gt;This is deliberately conservative. A phone number beginning with a plus can hold up an export if you include it among the text columns checked by this rule. That is an expected false positive of the policy. Phone numbers, amounts, and other typed fields need their own agreed contract, not an undocumented exemption.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Example's decision&lt;/th&gt;
&lt;th&gt;Next action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ordinary text&lt;/td&gt;
&lt;td&gt;Include the row&lt;/td&gt;
&lt;td&gt;Check CSV structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Formula-like prefix after leading spaces&lt;/td&gt;
&lt;td&gt;Hold the entire row&lt;/td&gt;
&lt;td&gt;Review the field through a protected interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tab or line break anywhere in the field&lt;/td&gt;
&lt;td&gt;Hold the entire row&lt;/td&gt;
&lt;td&gt;Confirm whether multiline text is required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown status or invalid schema&lt;/td&gt;
&lt;td&gt;Stop processing with an error&lt;/td&gt;
&lt;td&gt;Repair the contract; do not issue a successful file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comma and quotes inside ordinary text&lt;/td&gt;
&lt;td&gt;Serialize with the CSV library&lt;/td&gt;
&lt;td&gt;Compare the value after reading it back&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In a real workflow, show both the included-row count and the held-row count. The recipient must know that the report is incomplete. Do not call it a complete export while records remain under review. Give the exception owner a separate, access-controlled work list.&lt;/p&gt;

&lt;p&gt;The exception log must not become another route for the same problem. Do not put the original suspect comment in a second CSV or paste the entire value into a notification. The example's JSON log contains only a synthetic ID, field name, and reason. Inspecting originals requires an interface that displays them as text without interpreting markup or formulas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the policy check without a spreadsheet
&lt;/h2&gt;

&lt;p&gt;Save the following code as &lt;code&gt;csv_policy_check.py&lt;/code&gt; and run &lt;code&gt;python3 csv_policy_check.py&lt;/code&gt;. It requires Python 3 and its standard library. It creates records in memory, checks included rows and the exception log, writes no export file, and opens no spreadsheet. The value &lt;code&gt;=1+1&lt;/code&gt; is a harmless string used to exercise the rule; no calculation is performed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="n"&gt;PREFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=+-@＝＋－＠&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;STATUSES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qualified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;leading_format_character&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PREFIXES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formula_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;export_for_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;types&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isascii&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;STATUSES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;issue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quoting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QUOTE_ALL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Call tomorrow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=1+1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qualified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Said &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, call later&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  +44123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;break&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＝1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\u200b&lt;/span&gt;&lt;span class="s"&gt;=1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;export_for_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＝1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＋1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;－1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＠name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formula_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  =1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formula_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;export_for_review&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid row accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;held&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect included IDs &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;3&lt;/code&gt;; the other synthetic records should appear in the held list with reason codes. The assertions also require rejection of an unknown status, a nonnumeric ID, and &lt;code&gt;None&lt;/code&gt;. A traceback or &lt;code&gt;AssertionError&lt;/code&gt; means the check failed; do not count that run as evidence that the policy works.&lt;/p&gt;

&lt;p&gt;The detector intentionally holds any line break in a note and a leading Unicode format character in category &lt;code&gt;Cf&lt;/code&gt; after whitespace. These are additional restrictions proposed by this example. They are not CSV requirements or a complete model of Unicode behavior in spreadsheets. If you add columns, extend validation to every new untrusted field.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Python check cannot establish
&lt;/h2&gt;

&lt;p&gt;The round trip establishes that the two permitted synthetic records survive this CSV dialect unchanged. It does not establish identical handling across every Excel version, Calc, or another application. OWASP explicitly warns that no sanitization strategy is universally safe for every spreadsheet and downstream consumer.[1]&lt;/p&gt;

&lt;p&gt;Before deployment, record the application, its version, regional settings, and import route. In an isolated environment using synthetic data, check the displayed value, cell type, column boundaries, and the absence of calculation for values intended to remain text. Do not use network formulas, commands, or real personal information for these checks.&lt;/p&gt;

&lt;p&gt;Then save and reopen the file through the same route the recipient will use. OWASP notes that Excel can remove quotes or escape characters during saving and reopening.[1] A first-open check is therefore insufficient for a policy that relies on cell transformations. If the required application is unavailable, that stage remains unverified rather than automatically passing.&lt;/p&gt;

&lt;p&gt;Quarantine also needs an operational check: the recipient sees the exceptions count, the owner can find the original enquiry, and a later export neither loses it nor silently removes its restriction. Changing an enquiry's business status must not automatically declare its comment safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduce the policy without losing enquiries
&lt;/h2&gt;

&lt;p&gt;Start with observation in a test environment. Agree on hold reasons with the process owner and establish that records remain in the original system. Decide whether to enable production blocking based on concrete tests and team requirements, not on the existence of one successful script.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the columns and permitted types, including separate rules for empty strings, missing values, and negative numbers.&lt;/li&gt;
&lt;li&gt;Assign a consumer to each export and prevent silent switching between human viewing and machine import.&lt;/li&gt;
&lt;li&gt;Retain inclusion and hold examples covering commas, quotes, spaces, control characters, and full-width characters.&lt;/li&gt;
&lt;li&gt;Check that a schema violation fails the whole job. Do not distribute a partly written file as a successful result.&lt;/li&gt;
&lt;li&gt;Agree on protected exception review and the route for exporting a record after a human decision.&lt;/li&gt;
&lt;li&gt;After changing a library, target application, or column set, rerun the affected checks and retain execution conditions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The in-memory approach in this example may not suit large exports. A production handler needs size limits, temporary-storage controls, access restrictions, and atomic delivery of the completed file. Those are implementation responsibilities, not properties of the sample function. Do not begin a download before checking the conditions under which you promise a complete result.&lt;/p&gt;

&lt;p&gt;Start with one existing report: list its untrusted fields, identify the actual recipient, and agree on what happens when the first row is held. If those answers are missing, the export contract is not ready. Consider &lt;a href="https://www.24go.site" rel="noopener noreferrer"&gt;24go.site&lt;/a&gt; for enquiry workflows separately from this check: this article does not establish that the service implements the export mechanism described here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;[1] OWASP, CSV Injection: &lt;a href="https://owasp.org/www-community/attacks/CSV_Injection" rel="noopener noreferrer"&gt;https://owasp.org/www-community/attacks/CSV_Injection&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[2] MITRE, CWE-1236: &lt;a href="https://cwe.mitre.org/data/definitions/1236.html" rel="noopener noreferrer"&gt;https://cwe.mitre.org/data/definitions/1236.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[3] Python, csv: &lt;a href="https://docs.python.org/3/library/csv.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/csv.html&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was also published on other platforms. Original: &lt;a href="https://www.24go.site/blog/crm-csv-export-formula-quarantine?locale=en" rel="noopener noreferrer"&gt;CRM CSV Exports: Quarantine Suspicious Fields Before Handoff&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>crm</category>
      <category>безопасность</category>
      <category>экспорт</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>CSV из CRM: как отправлять подозрительные поля на проверку [перекрёстная публикация]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Fri, 30 Oct 2026 20:08:39 +0000</pubDate>
      <link>https://dev.to/dev24pro/csv-iz-crm-kak-otpravliat-podozritielnyie-polia-na-provierku-pieriekriostnaia-publikatsiia-mcc</link>
      <guid>https://dev.to/dev24pro/csv-iz-crm-kak-otpravliat-podozritielnyie-polia-na-provierku-pieriekriostnaia-publikatsiia-mcc</guid>
      <description>&lt;h2&gt;
  
  
  Почему CSV из CRM требует отдельной проверки
&lt;/h2&gt;

&lt;p&gt;Перед передачей выгрузки менеджеру разделите машинный обмен и просмотр в электронной таблице. Поля, введённые пользователем, могут оказаться формулами для программы, которая откроет файл. Подозрительные строки лучше задержать для разбора по согласованной политике, сохранив исходные значения в защищённом хранилище. Заключение полей в кавычки само по себе не делает формулы безопасными.&lt;/p&gt;

&lt;p&gt;OWASP описывает CSV Injection как попадание недоверенных данных в CSV с последующей интерпретацией содержимого электронной таблицей. В частности, ячейка, начинающаяся с &lt;code&gt;=&lt;/code&gt;, может быть прочитана как формула. CWE-1236 отдельно описывает эту слабость и ограничения способов её устранения.[1][2]&lt;/p&gt;

&lt;p&gt;Недоверенный текст может оказаться в названии компании, комментарии к обращению или поле, перенесённом из другого сервиса. Данные проходят через доверенную CRM, но происхождение текста от этого не меняется. Право скачать файл тоже не означает, что все его значения безопасны для следующей программы.&lt;/p&gt;

&lt;p&gt;Ниже предложена политика для собственного обработчика экспорта. Это не описание проверенной встроенной функции 24go.site, не универсальная защита всех таблиц и не разрешение собирать чужие контакты. Пример работает только с искусственными записями. Он проверяет решения обработчика, не запускает Excel или Calc и не обращается к CRM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Сначала договоритесь, кто откроет файл
&lt;/h2&gt;

&lt;p&gt;У выгрузки должен быть конкретный потребитель. Файл для переноса между системами требует сохранения значений по контракту. Файл для просмотра человеком требует проверки поведения выбранной программы. Названия кнопок, описание формата и маршрут хранения должны отражать эту разницу.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Назначение&lt;/th&gt;
&lt;th&gt;Что согласовать&lt;/th&gt;
&lt;th&gt;Что считать ошибкой&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Машинный перенос&lt;/td&gt;
&lt;td&gt;Схему, кодировку, разделитель, обработку пустых значений&lt;/td&gt;
&lt;td&gt;Незаметное изменение телефона или комментария&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Просмотр менеджером&lt;/td&gt;
&lt;td&gt;Программу, версию, настройки импорта, правила исключений&lt;/td&gt;
&lt;td&gt;Открытие недоверенного текста как формулы&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Разбор исключений&lt;/td&gt;
&lt;td&gt;Ответственного, доступ, срок хранения, способ просмотра&lt;/td&gt;
&lt;td&gt;Перенос исходных подозрительных значений в обычный CSV&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Не называйте машинный CSV безопасным для открытия двойным щелчком. У него другой договор с получателем. Если требуется точная передача текста, можно выбрать JSON с явно описанной схемой, но сам выбор JSON не управляет тем, что получатель сделает с данными дальше. После преобразования в таблицу проверка понадобится на новой границе.&lt;/p&gt;

&lt;p&gt;Для человеческого отчёта начните с минимального набора колонок. Если менеджеру достаточно номера заявки, статуса и даты, не добавляйте полный текст переписки. Уменьшение состава полей сокращает объём недоверенного текста и раскрываемых сведений; оно не заменяет проверку оставшихся значений.&lt;/p&gt;

&lt;p&gt;Процесс передачи заявок связан с &lt;a href="https://www.24go.site/blog/integratsiya-telegram-lidov-v-crm?locale=ru" rel="noopener noreferrer"&gt;интеграцией Telegram и CRM&lt;/a&gt;, но проверка экспорта решает отдельную задачу после сохранения записи. Приведённая здесь политика не подтверждает настройки и функции продукта, перечисленные в исторических материалах блога.&lt;/p&gt;

&lt;h2&gt;
  
  
  Разделите сериализацию и политику формул
&lt;/h2&gt;

&lt;p&gt;CSV-библиотека отвечает за границы полей и строк. Проверка содержимого отвечает за то, какие данные вы разрешаете передавать конкретному потребителю. Эти проверки выполняются последовательно, и успешное чтение файла обратно библиотекой подтверждает только формат обмена.&lt;/p&gt;

&lt;p&gt;Документация Python описывает &lt;code&gt;csv.writer&lt;/code&gt;, параметры разделителя и кавычек, а также открытие файлов с &lt;code&gt;newline=''&lt;/code&gt;. По умолчанию &lt;code&gt;csv.reader&lt;/code&gt; возвращает строки без автоматического преобразования типов. Это поведение парсера Python, не обещание о том, как тот же файл откроет электронная таблица.[3]&lt;/p&gt;

&lt;p&gt;Не собирайте строку через &lt;code&gt;','.join(...)&lt;/code&gt;. Запятая, кавычка или перенос строки в комментарии должны остаться частью своего поля. В примере используется &lt;code&gt;csv.writer&lt;/code&gt; с кавычками для всех полей, затем &lt;code&gt;csv.reader&lt;/code&gt; проверяет обратное чтение. Политика подозрительных префиксов применяется до записи, а не подменяется параметром &lt;code&gt;QUOTE_ALL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;OWASP перечисляет среди рискованных начал &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;@&lt;/code&gt;, табуляцию, возврат каретки, перевод строки и некоторые полноширинные варианты символов. Там же указано, что разделители и кавычки могут изменить границу ячейки при неправильной обработке.[1] Список помогает составить тесты, но не доказывает полноту конкретного детектора.&lt;/p&gt;

&lt;p&gt;Не удаляйте первый знак из исходной CRM-записи. Плюс может быть частью номера телефона, минус — осмысленного текста, а апостроф — имени. Если выбран способ преобразования для просмотра, храните исходник отдельно и документируйте, какое значение увидит человек. Изменение должно происходить в представлении, с проверкой на целевом приложении.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что означает карантин всей строки
&lt;/h2&gt;

&lt;p&gt;В предложенной политике строка с подозрительным полем не попадает в человеческий CSV. Вместо неё в отдельный журнал исключений записываются идентификатор, название колонки и код причины. Исходное поле остаётся в системе с ограниченным доступом. Обработчик не пытается автоматически исправить значение и не удаляет заявку.&lt;/p&gt;

&lt;p&gt;Такой выбор консервативен. Телефон с плюсом тоже может задержать экспорт, если телефон включён в проверяемые текстовые колонки. Это ожидаемое ложное срабатывание данной политики. Для телефонов, сумм и других типов нужен отдельный согласованный контракт, а не молчаливое исключение из общего правила.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ситуация&lt;/th&gt;
&lt;th&gt;Решение примера&lt;/th&gt;
&lt;th&gt;Дальнейшее действие&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Обычный текст&lt;/td&gt;
&lt;td&gt;Разрешить строку&lt;/td&gt;
&lt;td&gt;Проверить структуру CSV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Формульный префикс после начальных пробелов&lt;/td&gt;
&lt;td&gt;Задержать всю строку&lt;/td&gt;
&lt;td&gt;Разобрать поле в защищённом интерфейсе&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Табуляция или перенос строки в любом месте&lt;/td&gt;
&lt;td&gt;Задержать всю строку&lt;/td&gt;
&lt;td&gt;Подтвердить необходимость многострочного текста&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Неизвестный статус или неверная схема&lt;/td&gt;
&lt;td&gt;Остановить обработку с ошибкой&lt;/td&gt;
&lt;td&gt;Исправить контракт, не выдавать успешный файл&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Запятая и кавычки внутри обычного текста&lt;/td&gt;
&lt;td&gt;Сохранить через CSV-библиотеку&lt;/td&gt;
&lt;td&gt;Сверить значение после обратного чтения&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;В реальном процессе полезно показывать два числа: сколько строк включено и сколько задержано. Получатель должен знать, что отчёт неполон. Не называйте файл полной выгрузкой, если часть строк осталась на проверке. Для ответственного за исключения нужен отдельный список работ с контролем доступа.&lt;/p&gt;

&lt;p&gt;Сам журнал не должен становиться вторым каналом проблемы. Не складывайте в его CSV исходный подозрительный комментарий и не отправляйте весь текст в уведомление. В примере JSON-журнал содержит только искусственный ID, имя поля и причину. Просмотр исходных данных потребует интерфейса, который выводит их как текст и не интерпретирует как разметку или формулы.&lt;/p&gt;

&lt;h2&gt;
  
  
  Воспроизводимый тест без электронной таблицы
&lt;/h2&gt;

&lt;p&gt;Сохраните код как &lt;code&gt;csv_policy_check.py&lt;/code&gt; и выполните &lt;code&gt;python3 csv_policy_check.py&lt;/code&gt;. Нужен Python 3 со стандартной библиотекой. Программа создаёт данные только в памяти, проверяет разрешённые строки и журнал исключений. Она не пишет выгрузку на диск и не открывает таблицы. Значение &lt;code&gt;=1+1&lt;/code&gt; служит безопасной строкой для теста правила, вычисления не происходит.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="n"&gt;PREFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=+-@＝＋－＠&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;STATUSES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qualified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;leading_format_character&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PREFIXES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formula_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;export_for_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;types&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isascii&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;STATUSES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;issue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quoting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QUOTE_ALL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Call tomorrow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=1+1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qualified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Said &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, call later&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  +44123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;break&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＝1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\u200b&lt;/span&gt;&lt;span class="s"&gt;=1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;export_for_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＝1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＋1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;－1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＠name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formula_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  =1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formula_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;export_for_review&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid row accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;held&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ожидаются разрешённые ID &lt;code&gt;1&lt;/code&gt; и &lt;code&gt;3&lt;/code&gt;; остальные тестовые записи попадут в журнал с причинами. Проверки также требуют отказа при неизвестном статусе, нецифровом ID и значении &lt;code&gt;None&lt;/code&gt;. Ошибка даёт traceback или &lt;code&gt;AssertionError&lt;/code&gt;; такой запуск нельзя считать подтверждением политики.&lt;/p&gt;

&lt;p&gt;Детектор сознательно задерживает любой перенос строки в комментарии и начальный форматирующий символ категории &lt;code&gt;Cf&lt;/code&gt; после пробелов. Это собственные дополнительные ограничения примера. Они не объявлены требованиями CSV или полным описанием поведения Unicode в таблицах. После изменения набора колонок проверку надо расширить на каждое новое недоверенное поле.&lt;/p&gt;

&lt;h2&gt;
  
  
  Какие проверки остаются за пределами Python
&lt;/h2&gt;

&lt;p&gt;Обратное чтение доказывает сохранение двух разрешённых тестовых записей в данном диалекте CSV. Оно не доказывает, что все версии Excel, Calc и другие программы одинаково обработают файл. OWASP прямо предупреждает об отсутствии универсальной стратегии для всех приложений и последующих потребителей.[1]&lt;/p&gt;

&lt;p&gt;Перед внедрением зафиксируйте программу, её версию, региональные настройки и способ импорта. В изолированной среде на искусственных данных проверьте видимое значение ячейки, её тип, границы колонок и отсутствие вычисления у текста, который должен остаться текстом. Не используйте сетевые формулы, команды или реальные персональные данные для такой проверки.&lt;/p&gt;

&lt;p&gt;Затем сохраните и повторно откройте файл тем же маршрутом, которым пойдёт менеджер. OWASP отмечает, что Excel может убирать кавычки или экранирующие символы при сохранении и повторном открытии.[1] Поэтому проверка первого открытия недостаточна для политики, которая полагается на преобразование ячеек. Если необходимого приложения нет, этот этап остаётся непроверенным, а не автоматически успешным.&lt;/p&gt;

&lt;p&gt;Карантин также требует эксплуатационной проверки: получатель видит число исключений, ответственный может найти исходную заявку, повторный экспорт не теряет её и не снимает ограничение самопроизвольно. Изменение статуса заявки не должно означать автоматическое признание комментария безопасным.&lt;/p&gt;

&lt;h2&gt;
  
  
  Как внедрять без потери заявок
&lt;/h2&gt;

&lt;p&gt;Сначала включите наблюдение в тестовом контуре, согласуйте причины задержки с владельцем процесса и убедитесь, что записи не исчезают из исходной системы. Решение о включении производственной блокировки принимается по конкретным тестам и требованиям команды, не по наличию одного успешного скрипта.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Опишите колонки и разрешённые типы; отдельно укажите правила для пустой строки, отсутствующего значения и отрицательных чисел.&lt;/li&gt;
&lt;li&gt;Назначьте каждой выгрузке потребителя и запретите незаметно менять режим просмотра на машинный импорт.&lt;/li&gt;
&lt;li&gt;Сохраните примеры разрешения и задержки, включая запятые, кавычки, пробелы, управляющие и полноширинные символы.&lt;/li&gt;
&lt;li&gt;Проверьте отказ всего задания при нарушении схемы. Не отправляйте частично записанный файл как успешный результат.&lt;/li&gt;
&lt;li&gt;Согласуйте защищённый разбор исключений и способ повторного экспорта после решения человека.&lt;/li&gt;
&lt;li&gt;После обновления библиотеки, приложения или колонок повторите затронутые проверки и сохраните условия запуска.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Для больших выгрузок запись в память из примера может не подходить. Производственный обработчик должен учитывать ограничения размера, временное хранилище, права доступа и атомарную выдачу готового файла. Это задачи реализации, не свойства приведённой функции. Не начинайте скачивание пользователю до проверки условий, при которых вы обещаете полный результат.&lt;/p&gt;

&lt;p&gt;Начните с одного своего отчёта: перечислите недоверенные поля, найдите его фактического получателя и согласуйте поведение при первой задержанной строке. Если это пока неизвестно, экспортный договор ещё не готов. Обсуждать использование &lt;a href="https://www.24go.site" rel="noopener noreferrer"&gt;24go.site&lt;/a&gt; для работы с обращениями стоит отдельно от этой проверки: статья не подтверждает наличие в сервисе описанного экспортного механизма.&lt;/p&gt;

&lt;h2&gt;
  
  
  Источники
&lt;/h2&gt;

&lt;p&gt;[1] OWASP, CSV Injection: &lt;a href="https://owasp.org/www-community/attacks/CSV_Injection" rel="noopener noreferrer"&gt;https://owasp.org/www-community/attacks/CSV_Injection&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[2] MITRE, CWE-1236: &lt;a href="https://cwe.mitre.org/data/definitions/1236.html" rel="noopener noreferrer"&gt;https://cwe.mitre.org/data/definitions/1236.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[3] Python, csv: &lt;a href="https://docs.python.org/3/library/csv.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/csv.html&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Эта статья также опубликована на других платформах. Оригинал: &lt;a href="https://www.24go.site/blog/crm-csv-export-formula-quarantine?locale=ru" rel="noopener noreferrer"&gt;CSV из CRM: как отправлять подозрительные поля на проверку&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>crm</category>
      <category>безопасность</category>
      <category>экспорт</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Telegram Group Migration: Keep CRM Routing When the Chat ID Changes [cross-post]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Wed, 28 Oct 2026 02:36:09 +0000</pubDate>
      <link>https://dev.to/dev24pro/telegram-group-migration-keep-crm-routing-when-the-chat-id-changes-cross-post-16kb</link>
      <guid>https://dev.to/dev24pro/telegram-group-migration-keep-crm-routing-when-the-chat-id-changes-cross-post-16kb</guid>
      <description>&lt;h1&gt;
  
  
  Telegram Group Migration: Keep CRM Routing When the Chat ID Changes
&lt;/h1&gt;

&lt;p&gt;When a Telegram group becomes a supergroup, do not treat its new chat ID as a new customer conversation. Record the relationship between the old and new IDs, keep your internal conversation identifier unchanged, and resolve the current destination before sending an authorized notification. If the two IDs already belong to different conversations, stop automatic processing and investigate the conflict rather than silently merging records.&lt;/p&gt;

&lt;p&gt;This guide is for developers maintaining their own Telegram Bot API integration and CRM routing. It assumes a bot legitimately present in a working group, authenticated incoming updates, durable storage and a staging environment. The design is not a verified built-in feature of 24go.site. It does not require collecting members or sending unsolicited messages. A group is not an individual contact, and continuity of a group does not grant new permission to contact its members.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the migration fields in the right direction
&lt;/h2&gt;

&lt;p&gt;The official &lt;a href="https://core.telegram.org/bots/api#message" rel="noopener noreferrer"&gt;Message reference&lt;/a&gt; defines two optional fields. They describe the same relationship from different sides:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Received evidence&lt;/th&gt;
&lt;th&gt;Old group ID&lt;/th&gt;
&lt;th&gt;New supergroup ID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A message with &lt;code&gt;migrate_to_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;That message's &lt;code&gt;chat.id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Its &lt;code&gt;migrate_to_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A message with &lt;code&gt;migrate_from_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Its &lt;code&gt;migrate_from_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;That message's &lt;code&gt;chat.id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed request with &lt;code&gt;parameters.migrate_to_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;chat_id&lt;/code&gt; used in that exact request&lt;/td&gt;
&lt;td&gt;The parameter's value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third row comes from &lt;a href="https://core.telegram.org/bots/api#responseparameters" rel="noopener noreferrer"&gt;ResponseParameters&lt;/a&gt;, not from a service-message body. Keep the request context: without the original destination, the error response alone does not identify both ends. Do not look for an old identifier in a translated error description. Branch on the documented structured field, not on a particular English sentence or on every HTTP 400 response.&lt;/p&gt;

&lt;p&gt;The fields are optional. Your handler must work when it observes only one direction, receives equivalent evidence again, or encounters migration evidence after normal traffic to the new group. Do not require two service messages to arrive together or in an assumed order. This is a defensive processing rule, not a claim that every installation will see all these cases.&lt;/p&gt;

&lt;p&gt;Store the identifiers without narrowing them to 32-bit integers. Telegram notes that migration IDs can have more than 32 significant bits and at most 52 significant bits. A signed 64-bit database integer is a straightforward choice. If a CRM custom field cannot preserve that integer exactly, use a validated decimal string at that boundary and test round trips. Do not manufacture a new ID by adding a prefix to the old one; use the value supplied by Telegram.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the conversation from its transport address
&lt;/h2&gt;

&lt;p&gt;A fragile schema uses &lt;code&gt;chat_id&lt;/code&gt; simultaneously as the CRM conversation key, the destination for notifications and the historical identity of every event. A simple replacement then either loses history or makes the new chat look like a new lead.&lt;/p&gt;

&lt;p&gt;Use separate concepts in your own data model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;conversation_id&lt;/code&gt;: your stable internal identity for the working conversation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chat_alias&lt;/code&gt;: a Telegram identifier known to refer to that conversation, scoped to your tenant and bot.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;active_chat_id&lt;/code&gt;: the current destination proposed for new operations, subject to current permissions and application policy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;received_chat_id&lt;/code&gt;: the actual ID recorded on a historical event; keep this value unchanged.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scope is your integration's isolation rule, not Telegram's identifier format. Include tenant and bot context in lookups so one installation cannot accidentally attach evidence to another tenant's conversation. A unique constraint on scope plus alias should prevent the same alias from owning two internal conversations. With concurrent workers, you still need transactional checks; an in-memory lookup is not enough.&lt;/p&gt;

&lt;p&gt;An alias preserves continuity of routing. It does not mean that two CRM people are the same person, that all deals should be merged, or that a message identifier from the old chat is valid in the new one. Preserve message references as their original chat-and-message pair. Test historical link or reply behavior separately instead of rewriting old message IDs on the strength of a chat migration alone.&lt;/p&gt;

&lt;p&gt;For the wider integration problem, see &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=en" rel="noopener noreferrer"&gt;handling repeated Telegram webhooks&lt;/a&gt;. Event deduplication remains necessary, but it solves a different problem: a migration can arrive as a new update and still concern the existing conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make migration one atomic local decision
&lt;/h2&gt;

&lt;p&gt;Process the migration branch before a generic “unknown chat means create a CRM conversation” branch. Otherwise, a service message from the supergroup may create an unnecessary second record before your handler examines its migration fields.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate the update at your normal trusted intake boundary. Persist the incoming evidence and its source event key. A user's text containing an ID is not equivalent to a Telegram migration field.&lt;/li&gt;
&lt;li&gt;Extract the old and new IDs using the table above. Reject malformed values and self-mappings. Retain which field supplied the relationship.&lt;/li&gt;
&lt;li&gt;In a database transaction, lock or otherwise serialize the scoped alias records involved. Read their current conversation ownership again inside that transaction.&lt;/li&gt;
&lt;li&gt;If the old alias has one owner and the new alias is unclaimed, attach the new alias to the existing conversation. Record the migration relationship and update its active destination.&lt;/li&gt;
&lt;li&gt;If both aliases already belong to the same conversation, treat the operation as a repeat. Preserve the evidence, but do not create another CRM object or notification job.&lt;/li&gt;
&lt;li&gt;If the aliases belong to different conversations, quarantine the operation for reconciliation. Do not choose the newer row, overwrite a foreign key or delete either conversation automatically.&lt;/li&gt;
&lt;li&gt;Commit the mapping and any local follow-up job together. Only then acknowledge or finish the corresponding processing step according to your intake contract.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If neither alias is known, store unresolved evidence or use an explicit onboarding rule approved for that group. Do not guess which existing CRM record should own it based on the group title. If only the new alias is known, link the old alias only after the same scope and evidence checks; a reverse lookup is not permission to overwrite a conflict.&lt;/p&gt;

&lt;p&gt;A database transaction does not include a remote CRM write. If the CRM also stores the active destination, update it through a separately tracked operation and retain its result. After an ambiguous timeout, reconcile the actual result instead of assuming the update failed. The local alias table should remain the routing authority while the external projection catches up under a documented policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check direction, repeats and conflicts locally
&lt;/h2&gt;

&lt;p&gt;Save this example as &lt;code&gt;migration_check.py&lt;/code&gt; and run &lt;code&gt;python3 migration_check.py&lt;/code&gt;. It uses only the Python standard library, artificial IDs and process memory. It never calls Telegram or a CRM and does not create a database. The &lt;code&gt;scope&lt;/code&gt; tuple represents one tenant and one bot. The two message dictionaries are deliberately simplified input fixtures, not recordings from a real group.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_to_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_to_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_from_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_from_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no migration field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid pair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old alias needs reconciliation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;


&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1009876543210&lt;/span&gt;
&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_to_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_from_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;aliases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001111111111&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001111111111&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conflict accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001111111111&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other-tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old alias needs reconciliation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope crossed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;direction=ok repeat=ok conflict=blocked scope=isolated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected final line is &lt;code&gt;direction=ok repeat=ok conflict=blocked scope=isolated&lt;/code&gt;. A failed assertion means the checked policy has not held; do not remove the assertion just to get the line. The example intentionally stops on an unknown old alias. It does not implement the optional reverse-linking or onboarding paths described above.&lt;/p&gt;

&lt;p&gt;This check establishes field direction and a small sequential policy only. It does not validate a full Telegram update schema, integer bounds, authenticated intake, concurrent transactions, durable recovery, active-destination changes or the remote CRM projection. Add those checks at the layers that actually implement them. Keeping the example small makes its result inspectable instead of presenting a toy dictionary as a production integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolve queued work without erasing its history
&lt;/h2&gt;

&lt;p&gt;Old jobs may still contain the old chat ID. Deleting them loses work; changing every historical payload obscures what originally happened. Prefer storing a stable conversation reference on the job and resolving its current destination at execution time. Record both the originally requested destination and the resolved destination in the operation receipt.&lt;/p&gt;

&lt;p&gt;Before sending, verify that the bot still has the required access and that the operation remains appropriate for the conversation. A migration alias alone does not establish current membership or posting permission. Do not automatically replay an operation whose earlier delivery result is unknown: resolving a destination does not determine whether a message was already sent.&lt;/p&gt;

&lt;p&gt;If your system follows alias chains, impose a hop limit and detect cycles. A repeated mapping from old to new should not create a reverse edge just because &lt;code&gt;migrate_from_chat_id&lt;/code&gt; was observed. Both service-message forms express a direction from old to new. If a chain conflicts with current ownership or loops, stop routing that job and preserve the evidence for an operator. A fixed hop limit is your safety policy, not a Telegram migration limit.&lt;/p&gt;

&lt;p&gt;Cached destinations need a clear invalidation strategy. Otherwise, the database can be correct while a worker continues using an obsolete address. In staging, make one worker hold an old cached value and check how the migration version or cache expiry causes it to reload. Do not claim this is solved because a single fresh process finds the new row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnose the failure before editing CRM records
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;th&gt;Check next&lt;/th&gt;
&lt;th&gt;Avoid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Structured migration target in a failed response&lt;/td&gt;
&lt;td&gt;Match it to the stored request and scoped conversation&lt;/td&gt;
&lt;td&gt;Parsing any error text as a new ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New-group traffic creates a second conversation&lt;/td&gt;
&lt;td&gt;Handler ordering and alias lookup&lt;/td&gt;
&lt;td&gt;Deleting the new record before checking ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database has the new ID, worker sends to the old one&lt;/td&gt;
&lt;td&gt;Queue resolution and cache invalidation&lt;/td&gt;
&lt;td&gt;Replaying every queued operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New alias belongs to another conversation&lt;/td&gt;
&lt;td&gt;Tenant/bot scope and previous onboarding&lt;/td&gt;
&lt;td&gt;Automatic conversation merge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New destination still rejects an operation&lt;/td&gt;
&lt;td&gt;Current permissions and the actual error&lt;/td&gt;
&lt;td&gt;Treating migration as a blanket access repair&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An operator should be able to see the source evidence, both IDs, conversation ownership and the decision without opening entire message histories. Log only the metadata required for diagnosis under your retention policy. Keep bot tokens and unnecessary personal data out of operational receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the real integration before resuming work
&lt;/h2&gt;

&lt;p&gt;Back up the affected mapping records and test in an authorized staging group. Deliver the same migration evidence sequentially and concurrently: there should be one ownership decision and no duplicate local job. Stop the worker after the transaction commits, restart it, and confirm that unfinished projection work can resume. Introduce an explicit ownership conflict and check that neither conversation is silently reassigned.&lt;/p&gt;

&lt;p&gt;Then inspect an old queued job. Its historical source ID should remain intact, its resolved destination should follow the accepted mapping, and its prior delivery state should still determine whether it may run. Check the CRM projection separately from the alias table. Finally, verify an ordinary authorized interaction in the current group and record what actually happened; a local test line is not evidence of delivery.&lt;/p&gt;

&lt;p&gt;The rollout is ready only when continuity, isolation and failure handling have all been observed in your own stack. If trustworthy evidence does not establish the old-to-new relationship, leave the mapping unresolved. Pausing one route with a clear reason is safer than attaching a working group to the wrong customer record.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was also published on other platforms. Original: &lt;a href="https://www.24go.site/blog/telegram-group-migration-crm-chat-id?locale=en" rel="noopener noreferrer"&gt;Telegram Group Migration: Keep CRM Routing When the Chat ID Changes&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Миграция группы Telegram: как сохранить привязку к CRM при смене chat_id [перекрёстная публикация]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Wed, 28 Oct 2026 02:36:08 +0000</pubDate>
      <link>https://dev.to/dev24pro/mighratsiia-ghruppy-telegram-kak-sokhranit-priviazku-k-crm-pri-smienie-chatid-pieriekriostnaia-publikatsiia-41g6</link>
      <guid>https://dev.to/dev24pro/mighratsiia-ghruppy-telegram-kak-sokhranit-priviazku-k-crm-pri-smienie-chatid-pieriekriostnaia-publikatsiia-41g6</guid>
      <description>&lt;h1&gt;
  
  
  Миграция группы Telegram: как сохранить привязку к CRM при смене chat_id
&lt;/h1&gt;

&lt;p&gt;После преобразования группы Telegram в супергруппу не считайте новый chat_id новой беседой с клиентом. Зафиксируйте связь старого и нового идентификаторов, сохраните внутренний идентификатор беседы, а перед разрешённой отправкой определяйте её текущий адрес. Если два chat_id уже принадлежат разным беседам, остановите автоматическую обработку и разберите конфликт вместо того, чтобы незаметно объединять карточки.&lt;/p&gt;

&lt;p&gt;Это руководство предназначено разработчику собственной интеграции Telegram Bot API с CRM. Предполагаются бот, законно добавленный в рабочую группу, проверка входящих запросов, постоянное хранилище и тестовое окружение. Описанный подход не является подтверждённой встроенной функцией 24go.site. Для него не нужны сбор участников или нежелательные рассылки. Группа не равна отдельному контакту, а сохранение её истории не даёт нового согласия на обращения к участникам.&lt;/p&gt;

&lt;h2&gt;
  
  
  В какую сторону читать поля миграции
&lt;/h2&gt;

&lt;p&gt;В официальном описании &lt;a href="https://core.telegram.org/bots/api#message" rel="noopener noreferrer"&gt;Message&lt;/a&gt; указаны два необязательных поля. Они показывают одну связь с разных сторон:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Полученное свидетельство&lt;/th&gt;
&lt;th&gt;Старый ID группы&lt;/th&gt;
&lt;th&gt;Новый ID супергруппы&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Сообщение с &lt;code&gt;migrate_to_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chat.id&lt;/code&gt; этого сообщения&lt;/td&gt;
&lt;td&gt;Значение &lt;code&gt;migrate_to_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сообщение с &lt;code&gt;migrate_from_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Значение &lt;code&gt;migrate_from_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chat.id&lt;/code&gt; этого сообщения&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Неуспешный запрос с &lt;code&gt;parameters.migrate_to_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chat_id&lt;/code&gt;, использованный именно в этом запросе&lt;/td&gt;
&lt;td&gt;Значение параметра&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Третья строка относится к &lt;a href="https://core.telegram.org/bots/api#responseparameters" rel="noopener noreferrer"&gt;ResponseParameters&lt;/a&gt;, а не к телу служебного сообщения. Сохраняйте контекст запроса: без исходного адреса ответ с ошибкой не определяет оба конца связи. Не извлекайте старый идентификатор из переведённого описания ошибки. Ветвитесь по документированному структурному полю, а не по конкретной английской фразе или любому HTTP 400.&lt;/p&gt;

&lt;p&gt;Поля необязательны. Обработчик должен выдерживать ситуацию, когда обнаружено только одно направление, эквивалентные сведения пришли повторно или сведения о миграции получены уже после обычного события из новой группы. Не требуйте, чтобы два служебных сообщения пришли вместе или в предполагаемом порядке. Это защитное правило обработки, а не утверждение, что каждая установка обязательно встретит все перечисленные ситуации.&lt;/p&gt;

&lt;p&gt;Храните идентификаторы без сужения до 32-битного целого. Telegram указывает, что у migration ID может быть больше 32 значащих бит, но не больше 52. Знаковое 64-битное целое в БД - прямой вариант хранения. Если пользовательское поле CRM не сохраняет такое целое точно, используйте на этой границе проверенную десятичную строку и проверьте обратное преобразование. Не вычисляйте новый ID, добавляя префикс к старому: используйте значение, переданное Telegram.&lt;/p&gt;

&lt;h2&gt;
  
  
  Отделите беседу от транспортного адреса
&lt;/h2&gt;

&lt;p&gt;Хрупкая схема использует chat_id одновременно как ключ беседы в CRM, адрес уведомлений и историческую принадлежность каждого события. Простая замена тогда либо стирает происхождение старых данных, либо превращает новую группу в новый лид.&lt;/p&gt;

&lt;p&gt;В своей модели разделите четыре понятия:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;conversation_id&lt;/code&gt; - стабильный внутренний идентификатор рабочей беседы.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chat_alias&lt;/code&gt; - идентификатор Telegram, который относится к этой беседе в пределах вашего клиента системы и бота.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;active_chat_id&lt;/code&gt; - текущий адрес для новых операций; его применимость всё равно зависит от актуальных прав и правил приложения.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;received_chat_id&lt;/code&gt; - фактический ID, записанный в историческом событии; его не переписывают.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Область изоляции задаёт ваша интеграция, а не формат Telegram ID. Включайте tenant и bot в ключи поиска, чтобы одна установка не могла случайно привязать свидетельство к беседе другого клиента системы. Уникальное ограничение по области и alias должно запрещать принадлежность одного alias двум внутренним беседам. При нескольких обработчиках нужны транзакционные проверки: словарь в памяти их не заменяет.&lt;/p&gt;

&lt;p&gt;Alias сохраняет непрерывность маршрутизации. Он не доказывает, что два человека в CRM - один человек, что все сделки нужно объединить или что идентификатор сообщения старого чата действителен в новом. Сохраняйте ссылки на сообщения как исходную пару «чат-сообщение». Поведение старых ссылок и ответов проверяйте отдельно, а не переписывайте message_id только на основании миграции чата.&lt;/p&gt;

&lt;p&gt;Общую задачу повторной доставки разбирает материал &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=ru" rel="noopener noreferrer"&gt;о повторных вебхуках Telegram&lt;/a&gt;. Дедупликация событий по-прежнему нужна, но решает другой вопрос: миграция может прийти как новый update и при этом относиться к существующей беседе.&lt;/p&gt;

&lt;h2&gt;
  
  
  Принимайте решение о миграции атомарно
&lt;/h2&gt;

&lt;p&gt;Ветку миграции обрабатывайте раньше универсального правила «неизвестный чат - создать беседу в CRM». Иначе служебное сообщение из супергруппы может создать ненужную карточку до того, как обработчик проверит поля миграции.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Проверьте обновление на обычной доверенной границе приёма. Сохраните входящее свидетельство и ключ исходного события. Текст пользователя с каким-либо ID не равен структурному полю миграции Telegram.&lt;/li&gt;
&lt;li&gt;Извлеките старый и новый ID по таблице. Отклоните некорректные значения и случай, когда старый и новый ID одинаковы. Сохраните, какое поле подтвердило связь.&lt;/li&gt;
&lt;li&gt;В транзакции БД заблокируйте затронутые scoped alias или иным способом сериализуйте работу с ними. Заново прочитайте принадлежность обоих alias внутри транзакции.&lt;/li&gt;
&lt;li&gt;Если у старого alias одна беседа, а новый свободен, привяжите новый alias к существующей беседе. Запишите связь миграции и обновите активный адрес беседы.&lt;/li&gt;
&lt;li&gt;Если оба alias уже принадлежат той же беседе, считайте операцию повтором. Сохраните свидетельство, но не создавайте новый объект CRM или новое задание уведомления.&lt;/li&gt;
&lt;li&gt;Если alias принадлежат разным беседам, отправьте операцию на сверку. Не выбирайте более свежую строку, не перезаписывайте внешний ключ и не удаляйте одну беседу автоматически.&lt;/li&gt;
&lt;li&gt;Зафиксируйте привязку и локальное последующее задание вместе. Только после этого подтверждайте соответствующий шаг обработки согласно контракту вашего приёмника.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Если неизвестны оба alias, сохраните неразрешённое свидетельство либо примените отдельное правило подключения, согласованное для этой группы. Не угадывайте владельца среди существующих карточек по названию группы. Если известен только новый alias, старый можно привязать после тех же проверок области и свидетельства; обратный поиск не разрешает перезаписывать конфликт.&lt;/p&gt;

&lt;p&gt;Транзакция вашей БД не охватывает удалённую запись в CRM. Если CRM тоже хранит текущий адрес, обновляйте его отдельной отслеживаемой операцией и сохраняйте её результат. После неоднозначного таймаута сначала установите фактический результат, а не считайте запись неудачной. Таблица alias должна оставаться источником маршрутизации, пока внешняя копия догоняет её по заранее определённому правилу.&lt;/p&gt;

&lt;h2&gt;
  
  
  Локально проверьте направление, повтор и конфликт
&lt;/h2&gt;

&lt;p&gt;Сохраните пример в &lt;code&gt;migration_check.py&lt;/code&gt; и выполните &lt;code&gt;python3 migration_check.py&lt;/code&gt;. Нужна только стандартная библиотека Python; используются искусственные ID и память процесса. Пример не обращается к Telegram или CRM и не создаёт базу данных. Кортеж scope обозначает одного клиента системы и одного бота. Два словаря сообщений - намеренно упрощённые входные данные, а не записи событий реальной группы.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_to_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_to_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_from_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_from_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no migration field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid pair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old alias needs reconciliation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;


&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1009876543210&lt;/span&gt;
&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_to_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_from_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;aliases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001111111111&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001111111111&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conflict accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001111111111&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other-tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old alias needs reconciliation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope crossed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;direction=ok repeat=ok conflict=blocked scope=isolated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ожидаемая последняя строка: &lt;code&gt;direction=ok repeat=ok conflict=blocked scope=isolated&lt;/code&gt;. Ошибка assertion означает, что проверяемое правило не выдержано; не удаляйте проверку ради нужной строки. Пример намеренно останавливается при неизвестном старом alias. Он не реализует необязательные ветки обратной привязки и первичного подключения, описанные выше.&lt;/p&gt;

&lt;p&gt;Такая проверка устанавливает направление полей и небольшое последовательное правило. Она не проверяет полную схему Telegram update, допустимый диапазон целых чисел, доверенный приём, конкурентные транзакции, восстановление после сбоя, смену активного адреса или внешнюю копию в CRM. Эти проверки добавляют на тех уровнях, где соответствующее поведение действительно реализовано. Небольшой пример позволяет проверить результат, не выдавая учебный словарь за промышленную интеграцию.&lt;/p&gt;

&lt;h2&gt;
  
  
  Обновляйте маршрут задания, а не его прошлое
&lt;/h2&gt;

&lt;p&gt;В очереди могут остаться задания со старым chat_id. Удаление таких заданий теряет работу, а массовая замена исторических payload скрывает исходные обстоятельства. Предпочтительно хранить в задании стабильную ссылку на беседу и определять текущий адрес при исполнении. В квитанции операции записывайте и первоначально запрошенный адрес, и фактически выбранный.&lt;/p&gt;

&lt;p&gt;До отправки проверьте, что у бота остался нужный доступ, а сама операция по-прежнему уместна в этой беседе. Alias миграции не устанавливает актуальное членство или разрешение на публикацию. Не повторяйте автоматически операцию с неизвестным предыдущим результатом доставки: определение адреса не отвечает на вопрос, было ли сообщение уже отправлено.&lt;/p&gt;

&lt;p&gt;Если система проходит цепочки alias, ограничьте число переходов и обнаруживайте циклы. Повтор связи старого ID с новым не должен создавать обратное ребро только потому, что встретилось &lt;code&gt;migrate_from_chat_id&lt;/code&gt;. Оба вида служебного сообщения задают направление от старого к новому. Если цепочка зацикливается или противоречит принадлежности беседы, остановите задание и сохраните свидетельства для оператора. Ограничение числа переходов - ваше защитное правило, а не заявленный лимит миграций Telegram.&lt;/p&gt;

&lt;p&gt;Для закешированных адресов нужен ясный способ сброса. Иначе данные в БД могут быть верными, а обработчик продолжит обращаться по старому адресу. В тестовом окружении оставьте одному обработчику старое значение в кеше и проверьте, как версия миграции или истечение срока кеша заставляет перечитать запись. Успешный поиск в новом процессе не доказывает, что обновился старый кеш.&lt;/p&gt;

&lt;h2&gt;
  
  
  Прежде чем править CRM, определите причину
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Наблюдение&lt;/th&gt;
&lt;th&gt;Следующая проверка&lt;/th&gt;
&lt;th&gt;Чего избегать&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;В ошибке есть структурный migration target&lt;/td&gt;
&lt;td&gt;Связать его с сохранённым запросом и scoped беседой&lt;/td&gt;
&lt;td&gt;Получать новый ID из любого текста ошибки&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Трафик новой группы создаёт вторую беседу&lt;/td&gt;
&lt;td&gt;Порядок веток обработчика и поиск alias&lt;/td&gt;
&lt;td&gt;Удалять новую карточку до проверки принадлежности&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;В БД новый ID, обработчик отправляет на старый&lt;/td&gt;
&lt;td&gt;Выбор адреса в очереди и сброс кеша&lt;/td&gt;
&lt;td&gt;Повторять все задания подряд&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Новый alias занят другой беседой&lt;/td&gt;
&lt;td&gt;Область tenant/bot и предыдущее подключение&lt;/td&gt;
&lt;td&gt;Автоматически объединять беседы&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Новый адрес всё равно отклоняет действие&lt;/td&gt;
&lt;td&gt;Актуальные права и фактическая ошибка&lt;/td&gt;
&lt;td&gt;Считать миграцию универсальным восстановлением доступа&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Оператору нужны исходное свидетельство, оба ID, принадлежность бесед и принятое решение, а не доступ ко всей переписке. Записывайте только необходимые для диагностики метаданные согласно своей политике хранения. Не включайте токены бота и лишние персональные данные в технические квитанции.&lt;/p&gt;

&lt;h2&gt;
  
  
  Проверьте настоящую интеграцию до возобновления работы
&lt;/h2&gt;

&lt;p&gt;Сохраните резервную копию затронутых записей маршрутизации и тестируйте в разрешённой группе. Повторите одно и то же свидетельство миграции последовательно и одновременно: решение о принадлежности должно быть одно, без дублирующего локального задания. Остановите обработчик после фиксации транзакции, перезапустите и убедитесь, что незавершённая синхронизация может продолжиться. Создайте явный конфликт владельцев и проверьте, что ни одна беседа не переназначена молча.&lt;/p&gt;

&lt;p&gt;Затем проверьте старое задание из очереди. Его исходный chat_id должен остаться неизменным в истории, текущий адрес должен определяться по принятой связи, а предыдущее состояние доставки должно по-прежнему влиять на решение о запуске. Отдельно проверьте внешнюю копию адреса в CRM, а не только таблицу alias. Наконец, выполните обычное разрешённое взаимодействие в текущей группе и запишите фактический результат: строка локального теста не подтверждает доставку.&lt;/p&gt;

&lt;p&gt;Внедрение готово только после того, как непрерывность, изоляция и обработка ошибок проверены именно в вашем стеке. Если надёжное свидетельство не связывает старый ID с новым, оставьте привязку неразрешённой. Приостановить один маршрут с понятной причиной безопаснее, чем привязать рабочую группу к чужой карточке клиента.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Эта статья также опубликована на других платформах. Оригинал: &lt;a href="https://www.24go.site/blog/telegram-group-migration-crm-chat-id?locale=ru" rel="noopener noreferrer"&gt;Миграция группы Telegram: как сохранить привязку к CRM при смене chat_id&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Telegram Bot Webhooks: Validate secret_token Before Queuing CRM Work [cross-post]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Tue, 27 Oct 2026 04:12:41 +0000</pubDate>
      <link>https://dev.to/dev24pro/telegram-bot-webhooks-validate-secrettoken-before-queuing-crm-work-cross-post-3n78</link>
      <guid>https://dev.to/dev24pro/telegram-bot-webhooks-validate-secrettoken-before-queuing-crm-work-cross-post-3n78</guid>
      <description>&lt;h1&gt;
  
  
  Telegram Bot Webhooks: Validate secret_token Before Queuing CRM Work
&lt;/h1&gt;

&lt;p&gt;Check &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt; before interpreting a business event or writing a job to a queue. Your system chooses the expected value and supplies it to Telegram as &lt;code&gt;secret_token&lt;/code&gt; when registering the webhook. A request without an accepted value must not start qualification, create a lead, or change a deal. A matching secret, however, neither establishes the user's permissions nor prevents repeated delivery.&lt;/p&gt;

&lt;p&gt;This guide covers the ingress boundary of your own Telegram Bot API → receiver → queue → CRM integration. You need access to the receiver's code, configuration, and a test environment. It does not describe a verified built-in feature of 24go.site. The example uses invented public test values, runs locally, and neither contacts Telegram nor reads real secrets. It tests a receiver decision, not the security of an entire deployed service.&lt;/p&gt;

&lt;h2&gt;
  
  
  What secret_token actually checks
&lt;/h2&gt;

&lt;p&gt;Telegram's &lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;setWebhook documentation&lt;/a&gt; defines an optional &lt;code&gt;secret_token&lt;/code&gt;: a string of 1 to 256 characters using &lt;code&gt;A-Z&lt;/code&gt;, &lt;code&gt;a-z&lt;/code&gt;, &lt;code&gt;0-9&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt;, and &lt;code&gt;-&lt;/code&gt;. When configured, Telegram includes that value in the &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt; header of every webhook request. Supplying the parameter during registration is only half the setup. Your receiver must enforce the header check.&lt;/p&gt;

&lt;p&gt;This is a shared secret, not a signature over the message body. The documented mechanism does not include a signed timestamp or a single-use nonce. Someone who obtains the value can present it again; the header alone does not establish the freshness of the JSON or replace secret protection. Protect the connections and keep the value out of logs, traces, screenshots, and error messages.&lt;/p&gt;

&lt;p&gt;Keep the webhook secret distinct from the bot token. The bot token is used to access the Bot API; this guide concerns a separate value for incoming requests. Separating them lets you replace the ingress secret without deliberately exposing the bot token. A leaked bot token remains a separate incident: replacing only the webhook secret does not address every consequence of compromised API access.&lt;/p&gt;

&lt;p&gt;A successful check answers a narrow question: did this request present a secret accepted by this endpoint? It does not establish whether a particular Telegram user may change a particular deal. After accepting the event, you still need to check the actor, their access to the object, and whether the requested transition is allowed. A CRM record identifier in incoming JSON is not, by itself, permission to modify that record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reject before creating a side effect
&lt;/h2&gt;

&lt;p&gt;Give the receiver an explicit sequence so a refusal cannot accidentally create work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Limit request size and read time at the outer layer. You can check a header before application-level JSON parsing, but the HTTP server might already buffer the body. Server and proxy limits are still necessary.&lt;/li&gt;
&lt;li&gt;Check that the method and route match your webhook endpoint. Do not redirect a request containing the secret to an arbitrary location or provide an alternative unprotected route.&lt;/li&gt;
&lt;li&gt;Extract one secret header using the HTTP framework's normal facilities. Header names are case-insensitive; compare the value exactly, without changing its case or trimming whitespace.&lt;/li&gt;
&lt;li&gt;Validate the receiver configuration. An empty expected value is a configuration error, not permission to accept requests without credentials. Prefer detecting this at startup and withholding readiness.&lt;/li&gt;
&lt;li&gt;On mismatch, stop without queuing a job or calling the CRM. Do not return the expected value or comparison details to the caller.&lt;/li&gt;
&lt;li&gt;Only then parse JSON, validate the event structure, and pass the event to durable storage. The successful HTTP acknowledgement must match the storage guarantee your system actually provides.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The status codes below are our example policy, not mandatory Telegram choices: &lt;code&gt;403&lt;/code&gt; for a failed secret check, &lt;code&gt;503&lt;/code&gt; for missing configuration, and &lt;code&gt;202&lt;/code&gt; for the decision “allow the next step.” A real handler must not send that final response before successfully preserving the event. If storage fails, authorization may have succeeded, but the request as a whole has not yet been accepted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;Telegram documents retries after responses outside the 2XY range&lt;/a&gt;. Rejecting legitimate events because of a configuration mismatch can therefore trigger repeated delivery. Do not hide the problem with an unconditional &lt;code&gt;200&lt;/code&gt;: that can conceal event loss. Restore matching configuration and receiver readiness, then verify that the event flow has recovered. This guide does not assume a precise retry schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  A receiver decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Decision before the queue&lt;/th&gt;
&lt;th&gt;What the operator checks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Expected secret is absent&lt;/td&gt;
&lt;td&gt;Do not accept; instance is not ready&lt;/td&gt;
&lt;td&gt;Configuration delivery to every instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Header is missing or empty&lt;/td&gt;
&lt;td&gt;Reject without business actions&lt;/td&gt;
&lt;td&gt;Webhook registration and proxy forwarding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value differs by one character&lt;/td&gt;
&lt;td&gt;Reject; do not try to repair it&lt;/td&gt;
&lt;td&gt;Registered and expected configuration versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value has extra whitespace&lt;/td&gt;
&lt;td&gt;Reject under this exact-match policy&lt;/td&gt;
&lt;td&gt;Configuration serialization; do not add trimming as a workaround&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret is correct but JSON is invalid&lt;/td&gt;
&lt;td&gt;Authentication passed; event not accepted&lt;/td&gt;
&lt;td&gt;Separate body limits and schema checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret is correct and event was already stored&lt;/td&gt;
&lt;td&gt;Apply deduplication&lt;/td&gt;
&lt;td&gt;Stable event key and existing processing record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret is correct but actor lacks permission&lt;/td&gt;
&lt;td&gt;Do not execute the forbidden action&lt;/td&gt;
&lt;td&gt;Business authorization, not the transport secret&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Define a separate policy for duplicate secret headers. Reject ambiguous input at an agreed layer and test the actual HTTP stack. Servers can expose repeated fields as a list, a combined string, or a selected value. A function that takes one string cannot establish how duplicate headers behave across your network path.&lt;/p&gt;

&lt;h2&gt;
  
  
  A local test without a real secret
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;ingress_check.py&lt;/code&gt; and run &lt;code&gt;python3 ingress_check.py&lt;/code&gt;. It uses only the Python 3 standard library: no packages, bot, network, or CRM. Every value shown is public test data and must not be copied into production configuration. The function receives the already-extracted header value, not raw HTTP bytes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.python.org/3/library/hmac.html#hmac.compare_digest" rel="noopener noreferrer"&gt;Python's documentation&lt;/a&gt; describes &lt;code&gt;hmac.compare_digest&lt;/code&gt; as avoiding content-based short-circuit comparison. String arguments must be ASCII, so the example first checks the type and permitted alphabet. This is not a claim that the entire network service is immune to timing analysis. The documentation separately notes information that differences in argument types and lengths could reveal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[A-Za-z0-9_-]{1,256}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_checks&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo_receiver_token_123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo_receiver_token_124&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;space&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unicode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_я&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;too_long&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;257&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bad_config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repeat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;queued&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configured&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configured&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;queued&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;queued&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repeat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted_for_next_step:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queued&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run_checks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local execution checked nine cases. &lt;code&gt;match&lt;/code&gt; and &lt;code&gt;repeat&lt;/code&gt; returned &lt;code&gt;202&lt;/code&gt;; six invalid incoming values returned &lt;code&gt;403&lt;/code&gt;; empty configuration returned &lt;code&gt;503&lt;/code&gt;. The final line was &lt;code&gt;accepted_for_next_step: 2&lt;/code&gt;. Here, &lt;code&gt;queued&lt;/code&gt; is only a list counting permission to continue, not a real queue. The repeated value is deliberately accepted again: secret comparison stores no event history. This result shows the boundary of the check, not a problem that rotation solves.&lt;/p&gt;

&lt;p&gt;Use a separate event key and durable storage to handle repeated deliveries. The &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=en" rel="noopener noreferrer"&gt;Telegram-to-CRM idempotency guide&lt;/a&gt; covers that stage. Do not treat the secret and &lt;code&gt;update_id&lt;/code&gt; as interchangeable controls: one limits entry, while the other helps distinguish deliveries of an already accepted event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the reverse-proxy boundary
&lt;/h2&gt;

&lt;p&gt;A reverse proxy receives the external HTTP request and forwards it to the application. Draw the actual route: public address → TLS termination → proxy or load balancer → specific handler. For each segment, identify who can connect and where the secret is checked. HTTPS on the public address alone says nothing about whether an internal application port is reachable by an unintended caller.&lt;/p&gt;

&lt;p&gt;Check that clients cannot bypass the protective layer and reach the application directly. If only the proxy validates the secret, direct backend access without the same check bypasses that boundary. If the application validates it, the proxy must preserve the incoming value. Do not configure the proxy to insert the correct secret unconditionally into every external request: that would give an arbitrary client a trusted header inside your infrastructure.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.aiogram.dev/en/latest/dispatcher/webhook.html" rel="noopener noreferrer"&gt;official aiogram webhook example&lt;/a&gt; supplies &lt;code&gt;secret_token&lt;/code&gt; both when registering the webhook and when constructing &lt;code&gt;SimpleRequestHandler&lt;/code&gt;. It also shows a web server bound to localhost behind a proxy. These are useful inspection points, not a universal deployment recipe. Container networking or a separate load balancer may require a different bind address and appropriate network restrictions. Verify the behavior of the framework version you actually run.&lt;/p&gt;

&lt;p&gt;In a test deployment, send a request with a deliberately incorrect test value through the public address and confirm that no job appears in storage. Repeat through any accessible internal route within the authorized scope of your infrastructure. With the correct test value, check a different outcome: whether the request reaches the handler that validates and persists the event. These network checks were not performed for this guide. A local function cannot detect a dropped proxy header, an exposed backend port, or a wrongly routed endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotate without leaving a permanent spare entrance
&lt;/h2&gt;

&lt;p&gt;Rotation means coordinating a replacement of the accepted value and the webhook registration parameter. Treat it as a distributed configuration change: instances can update at different times, and previously sent requests may still be in flight. The cited Telegram contract does not promise an atomic changeover of every delivery to a new configuration.&lt;/p&gt;

&lt;p&gt;For a planned replacement, prepare the new secret in your approved secret store and make the receiver configuration available to every instance. If you choose to accept two values temporarily, specify a bounded overlap window, an owner, and the removal point for the old value. Register the new value through the existing secure bot-management tool and verify fresh test events. After changeover, remove the old value from every receiver and confirm rejection using the old test value. This is an operational proposal, not a built-in automatic Telegram rotation feature.&lt;/p&gt;

&lt;p&gt;If the old secret may have leaked, overlap extends the period in which someone can use it. Work with the incident owner to decide whether to revoke it immediately, accepting a temporary interruption if necessary. Do not switch to “accept anything while we investigate.” Also, do not use &lt;code&gt;drop_pending_updates&lt;/code&gt; merely to make an error disappear. Telegram documents that option as dropping pending updates, not repairing authentication.&lt;/p&gt;

&lt;p&gt;Keep working values out of article commands, shell history, and tickets. Record the configuration version, changeover time, and test outcome, not the secret itself. Separately inspect how your management tool retrieves and passes the value. A general guide cannot establish that it never appears in process arguments or debug logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnose refusals and establish readiness
&lt;/h2&gt;

&lt;p&gt;When refusals increase after a change, first establish which layer generated the response: an external protection service, the proxy, the HTTP framework, or your check. A &lt;code&gt;403&lt;/code&gt; by itself does not establish a bad secret_token. Correlate request time and trace identifier without copying sensitive headers. Check the selected endpoint, configuration version on every instance, and header forwarding. Broader CRM permissions will not repair an ingress failure.&lt;/p&gt;

&lt;p&gt;The Bot API's &lt;a href="https://core.telegram.org/bots/api#getwebhookinfo" rel="noopener noreferrer"&gt;getWebhookInfo method&lt;/a&gt; reports webhook state, including the pending-update count and last-error details when present. It is a diagnostic source, not evidence of the secret expected by your application. Review the output before sharing it: webhook addresses and infrastructure details can also be sensitive.&lt;/p&gt;

&lt;p&gt;Start operational monitoring with ingress-refusal counts, configuration failures, and the age of unprocessed events. A failed check must not automatically dump the entire conversation into logs. For an authorized request, separately track durable acceptance and work completion: transport authentication does not prove CRM success. Inspect the logging and metrics pipeline too, since it may capture sensitive headers independently of your handler.&lt;/p&gt;

&lt;p&gt;Readiness for the next stage means an incorrect test secret creates no job through any reachable route, a correct one reaches format validation and storage, missing configuration prevents readiness, and a repeated valid request is handled by separate deduplication. Test user permissions and the actual CRM effect independently. These are observable checks of your own system, not a security guarantee or permission to collect contacts or send unsolicited messages.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was also published on other platforms. Original: &lt;a href="https://www.24go.site/blog/telegram-webhook-secret-token-validation?locale=en" rel="noopener noreferrer"&gt;Telegram Bot Webhooks: Validate secret_token Before Queuing CRM Work&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Вебхук Telegram-бота: проверка secret_token до очереди и CRM [перекрёстная публикация]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Tue, 27 Oct 2026 04:12:39 +0000</pubDate>
      <link>https://dev.to/dev24pro/viebkhuk-telegram-bota-provierka-secrettoken-do-ochieriedi-i-crm-pieriekriostnaia-publikatsiia-2pp8</link>
      <guid>https://dev.to/dev24pro/viebkhuk-telegram-bota-provierka-secrettoken-do-ochieriedi-i-crm-pieriekriostnaia-publikatsiia-2pp8</guid>
      <description>&lt;h1&gt;
  
  
  Вебхук Telegram-бота: проверка secret_token до очереди и CRM
&lt;/h1&gt;

&lt;p&gt;Проверяйте &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt; до разбора бизнес-события и записи задания в очередь. Ожидаемое значение задаётся на вашей стороне и передаётся Telegram при регистрации вебхука через &lt;code&gt;secret_token&lt;/code&gt;. Запрос без подходящего значения не должен запускать квалификацию, создавать лид или менять сделку. Однако совпадение секрета не подтверждает права пользователя и не защищает от повторной доставки.&lt;/p&gt;

&lt;p&gt;Ниже разобрана входная граница собственной интеграции Telegram Bot API → обработчик → очередь → CRM. Понадобятся доступ к коду приёмника, его настройкам и тестовому окружению. Это не инструкция по настройке проверенной встроенной функции 24go.site. Пример работает локально с вымышленными значениями, не обращается к Telegram и не читает настоящие секреты. Он проверяет решение приёмника, а не безопасность развёрнутого сервиса целиком.&lt;/p&gt;

&lt;h2&gt;
  
  
  Какую проверку даёт secret_token
&lt;/h2&gt;

&lt;p&gt;В &lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;описании setWebhook&lt;/a&gt; Telegram документирует необязательный &lt;code&gt;secret_token&lt;/code&gt;: строку длиной от 1 до 256 символов из &lt;code&gt;A-Z&lt;/code&gt;, &lt;code&gt;a-z&lt;/code&gt;, &lt;code&gt;0-9&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt; и &lt;code&gt;-&lt;/code&gt;. Если параметр задан, Telegram включает его в заголовок &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt; каждого webhook-запроса. Передать параметр при регистрации недостаточно: ваш приёмник должен действительно проверять заголовок.&lt;/p&gt;

&lt;p&gt;Это общий секрет между отправителем и приёмником, а не подпись содержимого сообщения. В указанном контракте нет подписанного времени или одноразового nonce. Следовательно, тот, кто получил значение, может предъявить его повторно; сам заголовок не доказывает свежесть JSON и не заменяет защиту ключей. Защищайте соединения и не записывайте значение в журналы, трассировки, скриншоты или сообщения об ошибках.&lt;/p&gt;

&lt;p&gt;Не путайте webhook secret с токеном бота. Токен бота используется для доступа к Bot API; здесь обсуждается отдельное значение для входящих запросов. Разделение позволяет менять входной секрет, не раскрывая намеренно токен бота. Но утечку токена бота нужно разбирать отдельно: замена только webhook secret не устраняет все последствия компрометации доступа к API.&lt;/p&gt;

&lt;p&gt;Успешная проверка отвечает на узкий вопрос: предъявлен ли секрет, который принимает этот endpoint? Она не отвечает на вопрос, имеет ли конкретный Telegram-пользователь право менять данную сделку. После приёма события всё ещё нужно проверить отправителя, его доступ к объекту и допустимость действия. Нельзя считать идентификатор CRM из входящего JSON достаточным разрешением на изменение записи.&lt;/p&gt;

&lt;h2&gt;
  
  
  Поставьте отказ перед побочным эффектом
&lt;/h2&gt;

&lt;p&gt;Задайте последовательность обработки так, чтобы отказ не успевал создать работу:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;На внешнем слое ограничьте размер запроса и время чтения. Заголовок можно проверить до прикладного разбора JSON, но HTTP-сервер всё равно может буферизовать тело. Ограничения на сервере и прокси остаются необходимыми.&lt;/li&gt;
&lt;li&gt;Убедитесь, что метод и маршрут соответствуют вашему webhook endpoint. Не перенаправляйте запрос с секретом на произвольный адрес и не создавайте запасной маршрут без той же проверки.&lt;/li&gt;
&lt;li&gt;Извлеките один заголовок секрета штатными средствами HTTP-фреймворка. Имена HTTP-заголовков регистронезависимы; значение секрета сравнивайте точно, не меняя регистр и не удаляя пробелы.&lt;/li&gt;
&lt;li&gt;Проверьте конфигурацию приёмника. Пустое ожидаемое значение - ошибка настройки, а не разрешение принимать запросы без секрета. Лучше обнаружить её при запуске и не объявлять экземпляр готовым.&lt;/li&gt;
&lt;li&gt;При несовпадении завершите обработку без постановки задания и без вызова CRM. Не возвращайте клиенту ожидаемое значение или подробности сравнения.&lt;/li&gt;
&lt;li&gt;Только после этого разбирайте JSON, проверяйте его структуру и передавайте событие в надёжное хранилище. Успешное HTTP-подтверждение должно соответствовать фактической гарантии сохранения в вашей системе.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Коды ответа ниже - наша примерная политика, а не обязательный выбор Telegram: &lt;code&gt;403&lt;/code&gt; для проверки секрета, завершившейся отказом, &lt;code&gt;503&lt;/code&gt; при отсутствии настройки и &lt;code&gt;202&lt;/code&gt; как решение «разрешить следующий шаг». В реальном обработчике последний код нельзя отправлять до успешного сохранения события. Если хранилище недоступно, авторизация могла пройти, но запрос ещё не принят целиком.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;Telegram описывает повтор запросов при ответе не из диапазона 2XY&lt;/a&gt;. Поэтому отказ на настоящем событии из-за ошибки конфигурации может вызвать повторные доставки. Не «чините» это безусловным &lt;code&gt;200&lt;/code&gt;: так можно скрыть потерю события. Восстановите согласованность секрета и готовность приёмника, затем проверьте, что поток событий возобновился. Точные сроки повторов здесь не предполагаются.&lt;/p&gt;

&lt;h2&gt;
  
  
  Таблица решений для приёмника
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Условие&lt;/th&gt;
&lt;th&gt;Решение до очереди&lt;/th&gt;
&lt;th&gt;Что проверять оператору&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ожидаемый секрет отсутствует&lt;/td&gt;
&lt;td&gt;Не принимать; экземпляр не готов&lt;/td&gt;
&lt;td&gt;Доставку настройки всем экземплярам&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Заголовка нет или значение пустое&lt;/td&gt;
&lt;td&gt;Отклонить без бизнес-действий&lt;/td&gt;
&lt;td&gt;Регистрацию вебхука и передачу заголовка прокси&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Значение отличается на один символ&lt;/td&gt;
&lt;td&gt;Отклонить, не исправлять автоматически&lt;/td&gt;
&lt;td&gt;Согласованность зарегистрированной и ожидаемой версий&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;В значении есть лишний пробел&lt;/td&gt;
&lt;td&gt;Отклонить по выбранной политике точного сравнения&lt;/td&gt;
&lt;td&gt;Сериализацию настройки; не добавлять &lt;code&gt;strip()&lt;/code&gt; как обход&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Секрет верен, JSON некорректен&lt;/td&gt;
&lt;td&gt;Авторизация прошла, событие не принято&lt;/td&gt;
&lt;td&gt;Отдельные проверки формата и лимитов тела&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Секрет верен, событие уже сохранено&lt;/td&gt;
&lt;td&gt;Применить дедупликацию&lt;/td&gt;
&lt;td&gt;Ключ события и существующую запись обработки&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Секрет верен, пользователь не имеет прав&lt;/td&gt;
&lt;td&gt;Не выполнять запрещённое действие&lt;/td&gt;
&lt;td&gt;Бизнес-авторизацию, а не транспортный секрет&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Политику для дублирующихся заголовков задайте отдельно: отклоняйте неоднозначный ввод на согласованном слое и тестируйте именно свой стек. Разные серверы могут представлять повторные поля списком, объединённой строкой или одним выбранным значением. Нельзя утверждать, что пример с одним строковым аргументом проверил поведение дублирующихся заголовков на сетевом пути.&lt;/p&gt;

&lt;h2&gt;
  
  
  Локальная проверка без настоящего секрета
&lt;/h2&gt;

&lt;p&gt;Сохраните код в &lt;code&gt;ingress_check.py&lt;/code&gt; и выполните &lt;code&gt;python3 ingress_check.py&lt;/code&gt;. Нужна только стандартная библиотека Python 3; пакеты, бот, сеть и CRM не используются. Все строки ниже - публичные тестовые данные, их нельзя переносить в рабочую конфигурацию. Функция получает уже извлечённое значение заголовка, а не сырые HTTP-байты.&lt;/p&gt;

&lt;p&gt;В &lt;a href="https://docs.python.org/3/library/hmac.html#hmac.compare_digest" rel="noopener noreferrer"&gt;документации Python&lt;/a&gt; указано, что &lt;code&gt;hmac.compare_digest&lt;/code&gt; позволяет избежать короткого замыкания сравнения по содержимому. Строковые аргументы должны состоять из ASCII-символов, поэтому пример сначала проверяет тип и допустимый алфавит. Это не обещание полной защиты сетевого сервиса от анализа времени: документация отдельно оговаривает возможные утечки при различии типов и длин аргументов.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[A-Za-z0-9_-]{1,256}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_checks&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo_receiver_token_123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo_receiver_token_124&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;space&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unicode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_я&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;too_long&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;257&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bad_config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repeat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;queued&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configured&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configured&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;queued&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;queued&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repeat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted_for_next_step:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queued&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run_checks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;В локальном запуске примера проверены девять случаев: &lt;code&gt;match&lt;/code&gt; и &lt;code&gt;repeat&lt;/code&gt; дают &lt;code&gt;202&lt;/code&gt;, шесть некорректных входных значений дают &lt;code&gt;403&lt;/code&gt;, пустая конфигурация - &lt;code&gt;503&lt;/code&gt;. Итоговая строка: &lt;code&gt;accepted_for_next_step: 2&lt;/code&gt;. Список &lt;code&gt;queued&lt;/code&gt; здесь лишь считает разрешения на следующий шаг и не является настоящей очередью. Повтор намеренно принимается ещё раз: сравнение секрета не хранит историю событий. Это показывает границу проверки, а не недостаток, который нужно исправлять ротацией.&lt;/p&gt;

&lt;p&gt;Для обработки повторов используйте отдельный ключ события и постоянное хранилище. Этот этап подробнее разобран в &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=ru" rel="noopener noreferrer"&gt;руководстве по идемпотентности Telegram → CRM&lt;/a&gt;. Не объединяйте секрет и &lt;code&gt;update_id&lt;/code&gt; в один критерий: первый ограничивает вход, второй помогает различать доставки уже принятого события.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что меняется за обратным прокси
&lt;/h2&gt;

&lt;p&gt;Обратный прокси принимает внешний HTTP-запрос и пересылает его приложению. Составьте схему реального пути: публичный адрес → TLS termination → прокси или балансировщик → конкретный обработчик. Для каждого участка определите, кто может подключиться и где проверяется заголовок. Наличие HTTPS на публичном адресе само по себе ничего не говорит о доступности внутреннего порта.&lt;/p&gt;

&lt;p&gt;Проверьте, что клиенты не могут обойти защитный слой и обратиться к приложению напрямую. Если секрет проверяет только прокси, прямой доступ к backend в обход этой проверки нарушает границу защиты. Если секрет проверяет приложение, прокси должен сохранить входное значение. Не настраивайте прокси так, чтобы он безусловно вставлял правильный секрет в каждый внешний запрос: тогда произвольный клиент получит доверенный заголовок уже внутри вашей инфраструктуры.&lt;/p&gt;

&lt;p&gt;В &lt;a href="https://docs.aiogram.dev/en/latest/dispatcher/webhook.html" rel="noopener noreferrer"&gt;официальном примере aiogram&lt;/a&gt; &lt;code&gt;secret_token&lt;/code&gt; передаётся и при регистрации webhook, и при создании &lt;code&gt;SimpleRequestHandler&lt;/code&gt;. Там же показан web-сервер, привязанный к localhost за прокси. Это полезные точки для проверки, а не универсальная схема развёртывания. Контейнерная сеть или отдельный балансировщик могут потребовать другой адрес привязки и соответствующие сетевые ограничения. Проверьте поведение именно той версии фреймворка, которую используете.&lt;/p&gt;

&lt;p&gt;В тестовом развёртывании отправьте через публичный адрес запрос с заведомо неверным тестовым значением и убедитесь, что в хранилище не появилось задания. Повторите проверку через любой доступный внутренний маршрут, не выходя за разрешённые пределы своей инфраструктуры. Затем проверьте другой результат с правильным тестовым значением: доходит ли запрос до обработчика, который проверяет и сохраняет событие. Эти сетевые проверки здесь не выполнялись. Локальная функция не обнаружит потерю заголовка на прокси, открытый внутренний порт или неверно настроенный маршрут.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ротация без постоянного запасного входа
&lt;/h2&gt;

&lt;p&gt;Ротация означает согласованную замену принимаемого значения и параметра регистрации webhook. Планируйте её как изменение распределённой системы: экземпляры могут обновиться в разное время, а ранее отправленные запросы ещё могут находиться в пути. Приведённый контракт Telegram не обещает атомарного переключения всех доставок на новую конфигурацию.&lt;/p&gt;

&lt;p&gt;Для плановой замены подготовьте новый секрет в разрешённом хранилище и доставьте конфигурацию всем экземплярам приёмника. Если временно принимаются два значения, явно определите ограниченное окно, ответственного за изменение и момент удаления старого значения. Затем зарегистрируйте новое значение через существующий безопасный инструмент управления ботом и проверьте поступление новых тестовых событий. После переключения удалите старое значение на всех экземплярах и подтвердите отказ при запросе со старым тестовым значением. Это эксплуатационная рекомендация, а не встроенная функция автоматической ротации Telegram.&lt;/p&gt;

&lt;p&gt;Если старый секрет мог утечь, окно совместимости продлевает возможность его использования. Вместе с ответственным за инцидент решите, нужно ли немедленно отозвать старое значение, даже если это приведёт к временному перерыву. Не переходите к режиму «принимать всё, пока разбираемся». Не используйте и &lt;code&gt;drop_pending_updates&lt;/code&gt; только для того, чтобы убрать ошибку: в документации эта настройка описана как удаление ожидающих обновлений, а не как исправление авторизации.&lt;/p&gt;

&lt;p&gt;Не вставляйте рабочие значения в команды из статьи, историю shell или тикеты. Записывайте версию конфигурации, время переключения и результат проверки, но не сам секрет. Отдельно проверьте, как инструмент управления получает и передаёт значение. Общее руководство не подтверждает, что секрет не появляется в аргументах процесса или отладочных журналах.&lt;/p&gt;

&lt;h2&gt;
  
  
  Как разбирать отказы и определять готовность
&lt;/h2&gt;

&lt;p&gt;Если после изменения стало больше отказов, сначала выясните, на каком уровне сформирован ответ: внешним защитным сервисом, прокси, HTTP-фреймворком или вашим проверяющим кодом. Один &lt;code&gt;403&lt;/code&gt; сам по себе не доказывает, что неверен secret_token. Сопоставьте время запроса и идентификатор трассировки, не копируя чувствительные заголовки. Проверьте выбранный endpoint, версию настройки на каждом экземпляре и передачу заголовка. Расширение прав CRM не исправит ошибку на входе.&lt;/p&gt;

&lt;p&gt;Метод &lt;code&gt;getWebhookInfo&lt;/code&gt; в &lt;a href="https://core.telegram.org/bots/api#getwebhookinfo" rel="noopener noreferrer"&gt;Bot API&lt;/a&gt; позволяет смотреть состояние вебхука, включая число ожидающих обновлений и сведения о последней ошибке, если они есть. Это источник диагностических данных, а не подтверждение значения секрета, которое ожидает приложение. Прежде чем делиться полным ответом, проверьте его: адрес вебхука и сведения об инфраструктуре тоже могут быть чувствительными.&lt;/p&gt;

&lt;p&gt;Начните мониторинг со счётчиков отказов на входе, ошибок конфигурации и возраста необработанных событий. Отказ проверки не должен приводить к записи всей переписки в журналы. Для разрешённого запроса отдельно отслеживайте надёжное сохранение и выполнение работы: транспортная авторизация не доказывает успешную работу CRM. Проверьте также конвейер журналирования и метрик: он может записывать чувствительные заголовки независимо от обработчика.&lt;/p&gt;

&lt;p&gt;Приёмник готов к следующему этапу, если неверный тестовый секрет не создаёт задание ни через один доступный маршрут, правильный проходит к проверке формата и сохранению, отсутствие настройки блокирует готовность, а повторный правильный запрос обрабатывается отдельной дедупликацией. Права пользователя и фактический результат в CRM проверяются отдельно. Это наблюдаемые проверки собственной системы, а не гарантия безопасности и не разрешение собирать контакты или отправлять нежелательные сообщения.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Эта статья также опубликована на других платформах. Оригинал: &lt;a href="https://www.24go.site/blog/telegram-webhook-secret-token-validation?locale=ru" rel="noopener noreferrer"&gt;Вебхук Telegram-бота: проверка secret_token до очереди и CRM&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Telegram Bot Inline Buttons: Check Permissions and Reject Stale Actions [cross-post]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Tue, 27 Oct 2026 03:11:40 +0000</pubDate>
      <link>https://dev.to/dev24pro/telegram-bot-inline-buttons-check-permissions-and-reject-stale-actions-cross-post-1274</link>
      <guid>https://dev.to/dev24pro/telegram-bot-inline-buttons-check-permissions-and-reject-stale-actions-cross-post-1274</guid>
      <description>&lt;h1&gt;
  
  
  Telegram Bot Inline Buttons: Check Permissions and Reject Stale Actions
&lt;/h1&gt;

&lt;p&gt;Do not update a CRM enquiry just because &lt;code&gt;callback_data&lt;/code&gt; contains a command. Use the value to look up a server-side action record. Before making a change, check the sender, their current permissions, the button's expiry, the enquiry version, and whether the action has already run. A button sent yesterday is not permanent authority to change today's record.&lt;/p&gt;

&lt;p&gt;This design is for developers building their own Telegram Bot API bot to handle inbound enquiries. It is not a 24go.site feature setup. The scope is ordinary callback buttons on messages in private chats. Inline mode, games, and Web Apps need separate handlers. At this point, the server must already authenticate update delivery and validate the JSON structure. Checks inside a forged request do not authenticate its user.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a button press actually supplies
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://core.telegram.org/bots/api#callbackquery" rel="noopener noreferrer"&gt;CallbackQuery&lt;/a&gt;, &lt;code&gt;from&lt;/code&gt; identifies the sender. A message sent by the bot supplies &lt;code&gt;message&lt;/code&gt;; a message sent through inline mode supplies &lt;code&gt;inline_message_id&lt;/code&gt;. Do not assume every callback contains an ordinary, accessible message.&lt;/p&gt;

&lt;p&gt;The documentation explicitly warns that the originating message may contain no buttons with the received &lt;code&gt;data&lt;/code&gt;. Looking for a matching string in the current keyboard therefore does not replace a server-side check. A label such as “Confirm demo” is not authorization either.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://core.telegram.org/bots/api#inlinekeyboardbutton" rel="noopener noreferrer"&gt;InlineKeyboardButton&lt;/a&gt; limits &lt;code&gt;callback_data&lt;/code&gt; to 1-64 bytes. In a production design, use a short, unpredictable identifier rather than a phone number, staff role, or full enquiry JSON. Check the UTF-8 byte length, not the character count. The identifier does not replace permission checks: even a known, valid key must work only in an authorized context.&lt;/p&gt;

&lt;p&gt;It helps to distinguish three things when handling a callback: the callback data, the message context, and the server-side action record. The first helps locate an action, the second supports context checks, and the third defines exactly which operation may run. Neither the visible button label nor the presence of a similar button in the interface proves that the operation is still authorized.&lt;/p&gt;

&lt;p&gt;Do not use &lt;code&gt;callback_data&lt;/code&gt; to carry the entire business command. Apart from the length limit, doing so makes revoking permission harder and can expose data in logs or during mishandling. The server-side record should remain the source of truth. Even if an identifier is guessed or copied, all the other checks still apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the action's meaning on the server
&lt;/h2&gt;

&lt;p&gt;When issuing a button, store a record containing its key, internal enquiry ID, allowed operation, intended Telegram user ID, enquiry version, expiry, and execution state. Bind the record to the appropriate bot and, where needed, to the chat and message. The application chooses the lifetime; Telegram does not define the business expiry used here.&lt;/p&gt;

&lt;p&gt;Resolve permissions from current server-side records, not from a role embedded in a button. If an employee loses access or the enquiry moves to another team, an old button must not restore that access. For a customer-facing bot, first establish a link between the Telegram user and the specific enquiry. A matching username or knowledge of an enquiry number does not establish that link.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;On failure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Is the message context supported?&lt;/td&gt;
&lt;td&gt;Do not act; ask the user to open the current conversation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the server recognize the key?&lt;/td&gt;
&lt;td&gt;Do not reconstruct commands from an arbitrary string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is the sender the action's intended owner?&lt;/td&gt;
&lt;td&gt;Leave the enquiry unchanged and disclose no enquiry details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the user still have access?&lt;/td&gt;
&lt;td&gt;Refuse, even if the button is still visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has the action expired?&lt;/td&gt;
&lt;td&gt;Offer a way to request a new button&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has the action already completed?&lt;/td&gt;
&lt;td&gt;Do not run it again; show the known result only if access is authorized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Are the enquiry version and state still suitable?&lt;/td&gt;
&lt;td&gt;Reload the record and request fresh confirmation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For plan or price selection, keep the offer and its version on the server. If the terms have changed, show the new terms and ask for confirmation rather than silently applying a different price. This design is not a complete payment protocol.&lt;/p&gt;

&lt;p&gt;The order of checks matters too. First reject unsupported contexts and unknown keys, then check the sender and their current permissions. Do not reveal a detailed rejection reason to someone who is not allowed to access the enquiry: messages such as “that enquiry does not exist” or “someone else owns it” may disclose information. Keep a structured reason in internal logs, but use a neutral response for the user.&lt;/p&gt;

&lt;p&gt;If the user is authorized but the action has expired or the enquiry version has changed, do not automatically apply the old command to the new version. Load the current state, create a new action, and show the user what they are confirming. This is particularly important for changes to ownership, status, amount, meeting consent, or offer terms.&lt;/p&gt;

&lt;p&gt;Expiry and version checks solve different problems. Expiry limits how long a particular button can be used. The version check detects whether the object has changed since the action was created. A longer expiry does not solve stale data, and a new version does not make an old key safe to use indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgement is not a successful write
&lt;/h2&gt;

&lt;p&gt;Telegram clients display a progress indicator until &lt;a href="https://core.telegram.org/bots/api#answercallbackquery" rel="noopener noreferrer"&gt;answerCallbackQuery&lt;/a&gt; is called. Answer rejected callbacks too. For a longer operation, a neutral “Checking your request” is appropriate. Say “Enquiry updated” only after the result is confirmed. A successful Telegram method response does not confirm a CRM write.&lt;/p&gt;

&lt;p&gt;Perform the version check, action claim, and local change atomically in a transaction. Otherwise, two handlers can both observe &lt;code&gt;used: false&lt;/code&gt;. The function below does not protect against that race.&lt;/p&gt;

&lt;p&gt;A local transaction cannot cover a request to a remote CRM. Persist a durable operation and use that CRM's documented idempotency mechanism, or reconcile an ambiguous timeout. Do not mark an action as finally completed before its effect is confirmed. The separate guide to &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=en" rel="noopener noreferrer"&gt;repeated webhooks and duplicate CRM leads&lt;/a&gt; covers this boundary. A second click and redelivery of one event are different causes that may need to resolve to the same business operation.&lt;/p&gt;

&lt;p&gt;Define in advance what each server-side action state means: awaiting checks, claimed by a handler, sent to an external system, confirmed, or requiring reconciliation. The names and storage model depend on the application; the important point is that a timeout must not become a confirmed success automatically. If the external request has an ambiguous outcome, retry only with a safe idempotency key or after checking the result with the CRM.&lt;/p&gt;

&lt;p&gt;Avoid holding a database lock during a long network request unless your design requires it. One possible approach is to check permissions and version, atomically claim the action, and store a durable task in a transaction; then process the task separately and record the confirmed result. This is a general outline, not a ready-made design for every database and CRM. Choose transaction and recovery boundaries for the storage system you use.&lt;/p&gt;

&lt;p&gt;A callback repeated after a successful operation must not start a new change. If access is still authorized, you can return the known result. If access has been revoked, do not disclose that result merely because the same user clicked the same button before. Check access when returning a result, too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the refusal rules locally
&lt;/h2&gt;

&lt;p&gt;Save the code as &lt;code&gt;callback_policy_test.py&lt;/code&gt; and run &lt;code&gt;python callback_policy_test.py&lt;/code&gt; without &lt;code&gt;-O&lt;/code&gt;, which disables &lt;code&gt;assert&lt;/code&gt;. It requires Python 3 and no additional libraries. This is a sequential decision test with artificial inputs, not a bot, storage implementation, or concurrency test. &lt;code&gt;DEMO_ONLY&lt;/code&gt; is a public test string, not an example of a production secret key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Preconditions: authenticated delivery and validated object shapes.
&lt;/span&gt;    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsupported_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown_action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed_users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permission_denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;already_used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lead_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stale_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid_transition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eligible&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEMO_ONLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;
&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEMO_ONLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lead_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;lead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed_users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deepcopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2002&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed_users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]},&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;closed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eligible&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permission_denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;already_used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stale_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown_action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsupported_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid_transition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actual output from the local run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eligible"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wrong_user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"permission_denied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"already_used"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stale_version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unknown_action"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unsupported_context"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invalid_transition"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;eligible&lt;/code&gt; means only that the displayed checks passed. The function does not write a record, consume a key, or perform a transaction. &lt;code&gt;now&lt;/code&gt; and &lt;code&gt;expires_at&lt;/code&gt; are illustrative numbers on one time scale. In production, use server time and a consistent expiry format. Validate input object shapes and expected types before calling the function.&lt;/p&gt;

&lt;p&gt;If an &lt;code&gt;AssertionError&lt;/code&gt; occurs, investigate which branch changed. Do not delete the assertion just to make the test pass. If the policy changes, update the expected results deliberately.&lt;/p&gt;

&lt;p&gt;The test demonstrates individual rejection cases, but it does not cover every combination of conditions and is not a substitute for application tests. For example, it does not check a binding to a particular message, chat ID, or bot, because those details depend on the chosen action model. Add such checks to your tests if they are part of that model. Also test malformed types and missing fields at the handler boundary: the function explicitly assumes that input shapes have already been validated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before connecting the CRM
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In a private test chat, press a valid button as its intended user. Only the allowed operation on the correct enquiry should be created.&lt;/li&gt;
&lt;li&gt;Revoke the user's access, then press the old button. The enquiry must remain unchanged, and restricted details must not be disclosed.&lt;/li&gt;
&lt;li&gt;Change the enquiry version or close it before the click. An old confirmation must not overwrite the newer state.&lt;/li&gt;
&lt;li&gt;Test an expired action, an unknown key, and an unsupported context. Expect a refusal and an appropriate callback answer, not an unhandled exception.&lt;/li&gt;
&lt;li&gt;Send two concurrent requests and repeat a completed action. Verify the database's atomic action claim and confirm there is no second business effect.&lt;/li&gt;
&lt;li&gt;Simulate loss of the CRM response. Do not declare success or failure based on the timeout alone; recover the result through the durable operation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Also test that two actions created for different versions of one enquiry cannot overwrite a newer state in sequence. Check that a repeated click on a used button returns an appropriate response without creating a second external request. For a concurrency test, use genuinely parallel handlers and the storage system that will run in the application: a sequential test of &lt;code&gt;decide&lt;/code&gt; does not cover this.&lt;/p&gt;

&lt;p&gt;Test the interface failure path as well. If the message containing the button is inaccessible, do not treat that as proof that the action succeeded or was cancelled. The server holds the business state; removing or updating a keyboard does not replace checking it. If the user can request a new confirmation, that confirmation must reflect the current terms and enquiry version.&lt;/p&gt;

&lt;p&gt;These integration tests were not performed here. Decision reasons, internal operation IDs, and checked versions are usually sufficient for technical logs. Do not copy complete callbacks, personal data, or keys without a need. Removing the keyboard improves the interface but does not replace server-side rejection of stale actions. The system is ready when another user's or a stale click cannot change the enquiry, and a confirmed action cannot create a second effect.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was also published on other platforms. Original: &lt;a href="https://www.24go.site/blog/telegram-bot-callback-authorization?locale=en" rel="noopener noreferrer"&gt;Telegram Bot Inline Buttons: Check Permissions and Reject Stale Actions&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Inline-кнопки Telegram-бота: проверка прав и устаревших действий [перекрёстная публикация]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Tue, 27 Oct 2026 03:11:38 +0000</pubDate>
      <link>https://dev.to/dev24pro/inline-knopki-telegram-bota-provierka-prav-i-ustarievshikh-dieistvii-pieriekriostnaia-publikatsiia-e12</link>
      <guid>https://dev.to/dev24pro/inline-knopki-telegram-bota-provierka-prav-i-ustarievshikh-dieistvii-pieriekriostnaia-publikatsiia-e12</guid>
      <description>&lt;h1&gt;
  
  
  Inline-кнопки Telegram-бота: проверка прав и устаревших действий
&lt;/h1&gt;

&lt;p&gt;Не меняйте заявку в CRM только потому, что в &lt;code&gt;callback_data&lt;/code&gt; пришла команда. Используйте это значение как ключ серверной записи действия. Перед изменением проверьте отправителя, его текущие права, срок действия кнопки, версию заявки и то, не было ли действие выполнено ранее. Кнопка, отправленная вчера, не даёт бессрочного разрешения менять сегодняшнюю заявку.&lt;/p&gt;

&lt;p&gt;Это схема для разработчика собственного Telegram Bot API бота, который обрабатывает входящие обращения. Это не инструкция по настройке функции 24go.site. Рассматриваются обычные callback-кнопки в сообщениях личного чата. Для inline mode, игр и Web Apps нужны отдельные обработчики. К этому этапу сервер уже должен проверять подлинность доставки обновления и валидировать структуру JSON. Проверки внутри поддельного запроса не подтверждают личность пользователя.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что действительно приходит при нажатии
&lt;/h2&gt;

&lt;p&gt;В объекте &lt;a href="https://core.telegram.org/bots/api#callbackquery" rel="noopener noreferrer"&gt;CallbackQuery&lt;/a&gt; поле &lt;code&gt;from&lt;/code&gt; обозначает отправителя нажатия. Для сообщения, отправленного ботом, предусмотрено поле &lt;code&gt;message&lt;/code&gt;; для сообщения через inline mode используется &lt;code&gt;inline_message_id&lt;/code&gt;. Нельзя рассчитывать, что любой callback содержит обычное доступное сообщение.&lt;/p&gt;

&lt;p&gt;В документации отдельно сказано, что сообщение, из которого пришёл запрос, может не содержать кнопок с полученным &lt;code&gt;data&lt;/code&gt;. Поэтому поиск этой строки в текущей клавиатуре не заменяет серверную проверку. Подпись «Подтвердить демо» тоже не даёт разрешения на запись.&lt;/p&gt;

&lt;p&gt;У &lt;a href="https://core.telegram.org/bots/api#inlinekeyboardbutton" rel="noopener noreferrer"&gt;InlineKeyboardButton&lt;/a&gt; значение &lt;code&gt;callback_data&lt;/code&gt; ограничено 1-64 байтами. В рабочей реализации удобно использовать короткий непредсказуемый идентификатор, а не телефон, роль менеджера или весь JSON заявки. Длину проверяют в байтах UTF-8, а не по числу символов. Сам идентификатор не заменяет проверку прав: даже известный действительный ключ должен работать только в разрешённом контексте.&lt;/p&gt;

&lt;p&gt;При обработке полезно различать три вещи: данные callback, контекст сообщения и серверную запись действия. Первое позволяет найти действие, второе помогает проверить, откуда пришёл запрос, а третье задаёт, какую именно операцию разрешено выполнить. Ни видимая надпись на кнопке, ни наличие похожей кнопки в интерфейсе не доказывают, что операция всё ещё разрешена.&lt;/p&gt;

&lt;p&gt;Не используйте &lt;code&gt;callback_data&lt;/code&gt; как место для хранения всей бизнес-команды. Помимо ограничения длины, это усложняет отзыв разрешения и создаёт риск раскрытия данных в логах или при ошибочной обработке. Серверная запись должна оставаться источником истины. Даже если идентификатор случайно угадан или скопирован, остальные проверки всё равно обязательны.&lt;/p&gt;

&lt;h2&gt;
  
  
  Храните смысл действия на сервере
&lt;/h2&gt;

&lt;p&gt;При создании кнопки сохраните запись с ключом, внутренним ID заявки, разрешённым видом операции, Telegram ID предполагаемого пользователя, версией заявки, сроком действия и состоянием выполнения. Привяжите запись к нужному боту и, если необходимо, к чату и сообщению. Срок выбирает приложение: Telegram не задаёт описанный здесь бизнес-срок.&lt;/p&gt;

&lt;p&gt;Проверяйте права по актуальным серверным данным, а не по роли, записанной в кнопке. Если сотрудник потерял доступ или заявка перешла в другую команду, старая кнопка не должна восстанавливать этот доступ. Для клиентского бота сначала установите связь между пользователем Telegram и конкретной заявкой. Совпадение username или знание номера заявки такую связь не подтверждает.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Проверка&lt;/th&gt;
&lt;th&gt;Решение при отказе&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Поддерживается ли контекст сообщения?&lt;/td&gt;
&lt;td&gt;Не выполнять действие; предложить открыть актуальный диалог&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Найден ли серверный ключ?&lt;/td&gt;
&lt;td&gt;Не восстанавливать команду из произвольной строки&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Совпадает ли отправитель с владельцем действия?&lt;/td&gt;
&lt;td&gt;Не менять заявку и не раскрывать её данные&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сохранилось ли право доступа к заявке?&lt;/td&gt;
&lt;td&gt;Отказать, даже если кнопка ещё видна&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Истёк ли срок действия?&lt;/td&gt;
&lt;td&gt;Предложить запросить новую кнопку&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Действие уже выполнено?&lt;/td&gt;
&lt;td&gt;Не выполнять повторно; показать известный результат, только если доступ разрешён&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Подходят ли текущие версия и состояние заявки?&lt;/td&gt;
&lt;td&gt;Загрузить запись заново и запросить подтверждение ещё раз&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Для выбора тарифа или суммы храните предложение и его версию на сервере. Если условия изменились, покажите новые условия и запросите подтверждение, а не применяйте другую цену без уведомления. Эта схема не является готовым платёжным протоколом.&lt;/p&gt;

&lt;p&gt;Порядок проверок тоже имеет значение. Сначала отсекайте неподдерживаемый контекст и неизвестный ключ, затем проверяйте отправителя и его актуальные права. Не возвращайте подробную причину отказа человеку, который не имеет доступа к заявке: формулировки вроде «такой заявки нет» или «владелец другой» могут раскрыть лишнюю информацию. Для внутреннего журнала сохраните структурированную причину, но пользовательский ответ делайте нейтральным.&lt;/p&gt;

&lt;p&gt;Если пользователь уполномочен, но запись действия истекла или версия заявки изменилась, не пытайтесь автоматически применить старую команду к новой версии. Загрузите актуальное состояние, сформируйте новое действие и покажите пользователю, что именно он подтверждает. Это особенно важно для операций, которые меняют ответственного, статус, сумму, согласие на встречу или условия предложения.&lt;/p&gt;

&lt;p&gt;Срок действия и версия решают разные задачи. Срок ограничивает время, в течение которого можно использовать конкретную кнопку. Версия показывает, не изменился ли объект с момента формирования этого действия. Увеличение срока не устраняет проблему устаревшей версии, а новая версия не делает старый ключ безопасным для бесконечного использования.&lt;/p&gt;

&lt;h2&gt;
  
  
  Отделяйте ответ Telegram от результата записи
&lt;/h2&gt;

&lt;p&gt;После нажатия клиент показывает индикатор загрузки, пока бот не вызовет &lt;a href="https://core.telegram.org/bots/api#answercallbackquery" rel="noopener noreferrer"&gt;answerCallbackQuery&lt;/a&gt;. Отвечайте и на отклонённые callback-запросы. Если операция занимает время, уместно нейтральное сообщение «Проверяем действие». Писать «Заявка изменена» следует только после подтверждения результата. Успешный ответ метода Telegram не подтверждает запись в CRM.&lt;/p&gt;

&lt;p&gt;Проверку версии, захват действия и локальное изменение выполняйте атомарно в одной транзакции. Иначе два обработчика могут одновременно увидеть &lt;code&gt;used: false&lt;/code&gt;. Приведённая ниже функция не защищает от такой гонки.&lt;/p&gt;

&lt;p&gt;Локальная транзакция не охватывает запрос к удалённой CRM. Сохраните устойчивую операцию и используйте документированный механизм идемпотентности этой CRM либо сверьте результат после неоднозначного таймаута. Не помечайте действие окончательно выполненным, пока эффект не подтверждён. Граница между повторной доставкой и повторной обработкой подробнее рассмотрена в статье &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=ru" rel="noopener noreferrer"&gt;о повторных вебхуках и дублях CRM&lt;/a&gt;. Повторное нажатие и повторная доставка одного события имеют разные причины, но могут относиться к одной бизнес-операции.&lt;/p&gt;

&lt;p&gt;Заранее определите, что означает каждый статус серверной записи: ожидает проверки, захвачена обработчиком, отправлена во внешнюю систему, подтверждена или требует сверки. Названия и модель хранения зависят от приложения; важно, чтобы таймаут не превращался автоматически в подтверждённый успех. Если внешний запрос завершился неясно, повторять операцию можно только с безопасным ключом идемпотентности либо после сверки результата с CRM.&lt;/p&gt;

&lt;p&gt;Не держите блокировку базы во время долгого сетевого запроса без необходимости. Один из возможных подходов: в транзакции проверить актуальные права и версию, атомарно захватить действие и записать устойчивую задачу; затем обработать задачу отдельно и сохранить подтверждённый результат. Это общий план, а не готовая схема для любой базы и CRM. Границы транзакции и восстановления следует согласовать с используемым хранилищем.&lt;/p&gt;

&lt;p&gt;Повторный callback после успешной операции не должен запускать новое изменение. Если доступ всё ещё подтверждён, можно вернуть уже известный результат. Если право доступа отозвано, не раскрывайте результат только потому, что тот же пользователь нажал ту же кнопку ранее. Проверка доступа нужна и для повторного чтения результата.&lt;/p&gt;

&lt;h2&gt;
  
  
  Локально проверьте таблицу отказов
&lt;/h2&gt;

&lt;p&gt;Сохраните код как &lt;code&gt;callback_policy_test.py&lt;/code&gt; и выполните &lt;code&gt;python callback_policy_test.py&lt;/code&gt; без &lt;code&gt;-O&lt;/code&gt;, который отключает &lt;code&gt;assert&lt;/code&gt;. Нужен Python 3, дополнительные библиотеки не требуются. Это последовательная проверка решения на искусственных данных, а не бот, реализация хранилища или тест конкурентной обработки. &lt;code&gt;DEMO_ONLY&lt;/code&gt; - публичная тестовая строка, а не пример рабочего секретного ключа.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Preconditions: authenticated delivery and validated object shapes.
&lt;/span&gt;    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsupported_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown_action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed_users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permission_denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;already_used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lead_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stale_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid_transition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eligible&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEMO_ONLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;
&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEMO_ONLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expires_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lead_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;lead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed_users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deepcopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2002&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed_users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]},&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;closed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eligible&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permission_denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;already_used&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stale_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown_action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsupported_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid_transition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Фактический вывод локального запуска:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eligible"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wrong_user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"permission_denied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"expired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"already_used"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stale_version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unknown_action"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unsupported_context"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invalid_transition"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;eligible&lt;/code&gt; означает лишь, что пройдены показанные проверки. Функция ничего не записывает, не помечает ключ использованным и не выполняет транзакцию. &lt;code&gt;now&lt;/code&gt; и &lt;code&gt;expires_at&lt;/code&gt; в примере - условные числа на одной шкале времени. В рабочем коде используйте серверное время и согласованный формат срока. До вызова функции проверьте структуру входных объектов и ожидаемые типы.&lt;/p&gt;

&lt;p&gt;При &lt;code&gt;AssertionError&lt;/code&gt; выясните, какая ветка изменилась. Не удаляйте проверку, чтобы тест прошёл. Если политика изменилась, осознанно обновите ожидаемые результаты.&lt;/p&gt;

&lt;p&gt;Тест демонстрирует отдельные отказы, но не проверяет все сочетания условий и не заменяет тесты приложения. Например, здесь нет проверки привязки к конкретному сообщению, чат-идентификатору или боту, потому что это зависит от выбранной схемы. Добавляйте такие условия в тесты, если они входят в вашу модель действия. Также отдельно проверяйте некорректные типы и отсутствующие поля на границе обработчика: функция явно предполагает, что структура входных объектов уже проверена.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что проверить перед подключением CRM
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;В тестовом личном чате нажмите действительную кнопку от имени предполагаемого пользователя. Должна создаваться только разрешённая операция по нужной заявке.&lt;/li&gt;
&lt;li&gt;Отзовите у пользователя доступ, затем нажмите старую кнопку. Заявка должна остаться без изменений; закрытые данные не должны раскрываться.&lt;/li&gt;
&lt;li&gt;Измените версию заявки или закройте её до нажатия. Старое подтверждение не должно перезаписать новое состояние.&lt;/li&gt;
&lt;li&gt;Проверьте истёкший срок, неизвестный ключ и неподдерживаемый контекст. Ожидайте отказ и корректный ответ на callback, а не необработанное исключение.&lt;/li&gt;
&lt;li&gt;Отправьте два запроса одновременно и повторите уже завершённый. Проверьте атомарный захват действия в базе и отсутствие второго бизнес-эффекта.&lt;/li&gt;
&lt;li&gt;Смоделируйте потерю ответа CRM. Не считайте операцию успешной или неуспешной только на основании таймаута; восстановите результат через устойчивую операцию.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Дополнительно проверьте, что два действия, созданные для разных версий одной заявки, не могут по очереди перезаписать более новое состояние. Убедитесь, что повторное нажатие на уже использованную кнопку возвращает подходящий ответ и не создаёт второй внешний запрос. Для конкурентного теста используйте действительно параллельные обработчики и то же хранилище, которое будет работать в приложении: последовательный тест функции &lt;code&gt;decide&lt;/code&gt; этого не проверяет.&lt;/p&gt;

&lt;p&gt;Проверьте также путь отказа интерфейса. Если сообщение с кнопкой недоступно, не считайте это доказательством того, что действие было выполнено или отменено. Необходимое бизнес-состояние хранится на сервере; удаление или обновление клавиатуры не заменяет его проверку. Если пользователь может запросить новое подтверждение, оно должно отражать текущие условия и актуальную версию заявки.&lt;/p&gt;

&lt;p&gt;Эти интеграционные проверки здесь не выполнялись. Для технических журналов обычно достаточно причины решения, внутренних ID операции и проверенной версии. Не копируйте полные callback-запросы, личные данные или ключи без необходимости. Удаление клавиатуры улучшает интерфейс, но не заменяет серверный отказ для устаревших действий. Готовность означает, что чужое или устаревшее нажатие не меняет заявку, а подтверждённое действие не создаёт второго эффекта.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Эта статья также опубликована на других платформах. Оригинал: &lt;a href="https://www.24go.site/blog/telegram-bot-callback-authorization?locale=ru" rel="noopener noreferrer"&gt;Inline-кнопки Telegram-бота: проверка прав и устаревших действий&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Telegram Bot Contacts: Avoid Saving Someone Else's Phone in Your CRM</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Fri, 23 Oct 2026 03:08:39 +0000</pubDate>
      <link>https://dev.to/dev24pro/kontakt-v-telegram-botie-kak-nie-zapisat-chuzhoi-tieliefon-v-crm-cross-post-18og</link>
      <guid>https://dev.to/dev24pro/kontakt-v-telegram-botie-kak-nie-zapisat-chuzhoi-tieliefon-v-crm-cross-post-18og</guid>
      <description>&lt;h1&gt;
  
  
  Telegram Bot Contacts: Avoid Saving Someone Else's Phone in Your CRM
&lt;/h1&gt;

&lt;p&gt;Do not save every incoming &lt;code&gt;contact.phone_number&lt;/code&gt; as the sender's phone number. To link a contact automatically in a private chat, compare &lt;code&gt;message.contact.user_id&lt;/code&gt; with &lt;code&gt;message.from.id&lt;/code&gt;. If the contact ID is missing or different, leave the number unlinked and ask the user to share their own contact using a button.&lt;/p&gt;

&lt;p&gt;This is a policy for your own Telegram Bot API handler, not instructions for configuring a 24go.site feature. It assumes that your server has already authenticated delivery and validated the update's structure. Matching IDs links the contact to the Telegram user in that event. It does not verify a phone number by SMS, establish identity, or grant permission to send marketing messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask for the right contact first
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://core.telegram.org/bots/api#keyboardbutton" rel="noopener noreferrer"&gt;KeyboardButton&lt;/a&gt;, &lt;code&gt;request_contact: true&lt;/code&gt; means that pressing the button sends the user's phone number as a contact. This is available only in private chats. Use a regular reply keyboard, not an inline button.&lt;/p&gt;

&lt;p&gt;For example, pass this object as &lt;code&gt;reply_markup&lt;/code&gt; in your bot's existing &lt;code&gt;sendMessage&lt;/code&gt; call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keyboard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Share my phone number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"request_contact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resize_keyboard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"one_time_keyboard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explain the purpose in the message: “Share your phone number if you would like a sales representative to call about this enquiry. You can also continue the conversation here.” The button's presence does not mean the number has arrived. Process the incoming message, not the fact that you sent a keyboard.&lt;/p&gt;

&lt;p&gt;A user can also send a contact manually. The &lt;a href="https://core.telegram.org/bots/api#contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; object's &lt;code&gt;user_id&lt;/code&gt; is optional. Your handler therefore needs a missing-ID branch, even if every successful test with your button has returned one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide before writing to the CRM
&lt;/h2&gt;

&lt;p&gt;Compare the user ID in &lt;code&gt;message.from.id&lt;/code&gt;, not a name, username, or phone string. &lt;a href="https://core.telegram.org/bots/api#user" rel="noopener noreferrer"&gt;User.id&lt;/a&gt; identifies a user or bot. It is not a phone number.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Handler decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chat is not private&lt;/td&gt;
&lt;td&gt;Do not run this linking procedure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message has no contact&lt;/td&gt;
&lt;td&gt;Continue the normal conversation; leave the phone unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sender ID or &lt;code&gt;contact.user_id&lt;/code&gt; is missing&lt;/td&gt;
&lt;td&gt;Leave the contact unlinked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both IDs exist but differ&lt;/td&gt;
&lt;td&gt;Do not save the contact as the sender's phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDs match and a number is present&lt;/td&gt;
&lt;td&gt;Allow the next step under your storage policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A missing ID is not evidence of deception. A mismatch is not a reason to accuse the user either: they may have shared a colleague's contact. Reply neutrally: “We could not link this contact to your Telegram profile. If you would like us to call you, please use the button to share your own contact.” Enquiries on someone else's behalf need a separate process, not a weaker check.&lt;/p&gt;

&lt;p&gt;A match should not silently overwrite a previously confirmed CRM number. Retain the source, receipt time, sender ID, and check result. Define a separate rule for updating an existing phone number. Do not merge records for different people just because their phone strings match.&lt;/p&gt;

&lt;h2&gt;
  
  
  A local test without a bot or CRM
&lt;/h2&gt;

&lt;p&gt;The code below checks only which branch is selected for artificial inputs. It does not receive HTTP requests, verify a webhook secret, normalize numbers, or store them. It needs Python 3 and no additional libraries. Save it as &lt;code&gt;contact_policy_test.py&lt;/code&gt; and run &lt;code&gt;python contact_policy_test.py&lt;/code&gt; without &lt;code&gt;-O&lt;/code&gt;, which disables &lt;code&gt;assert&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;contact_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outside_private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;sender_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;contact_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sender_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unlinked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sender_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;different_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unlinked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;same_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TEST_ONLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deepcopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2002&lt;/span&gt;
&lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;contact_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;same_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;different_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unlinked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outside_private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unlinked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unlinked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"same_user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"different_user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unlinked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"no_contact"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"outside_private"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unlinked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unlinked"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TEST_ONLY&lt;/code&gt; is deliberately not a real phone number. &lt;code&gt;same_user&lt;/code&gt; means only that the input passed the checks shown; it does not mean the string can be called. The final case rejects a string ID: the example expects numeric IDs from Bot API JSON. If your library converts types, define that boundary explicitly and test the conversion.&lt;/p&gt;

&lt;p&gt;If you get an &lt;code&gt;AssertionError&lt;/code&gt;, investigate which rule changed rather than removing the check. Validate the structure before calling this function: for example, &lt;code&gt;chat&lt;/code&gt; and &lt;code&gt;from&lt;/code&gt; must be objects of the expected shape. This is not a general parser for arbitrary JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the real bot separately
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Send the reply button in a private test chat, press it, and inspect the resulting &lt;code&gt;from.id&lt;/code&gt; and &lt;code&gt;contact.user_id&lt;/code&gt;. Do not publish the real phone number in the test log.&lt;/li&gt;
&lt;li&gt;In a controlled test, send another test participant's contact with their permission. Expect automatic linking to be refused if the IDs differ; if the ID is missing, expect &lt;code&gt;unlinked&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Send ordinary text after the contact. It should not erase the stored phone number. Declining to share a number should not interrupt the available Telegram conversation.&lt;/li&gt;
&lt;li&gt;Check the inbound HTTP boundary. &lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;setWebhook&lt;/a&gt; supports &lt;code&gt;secret_token&lt;/code&gt; and the &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt; header. Your handler must validate its configured secret before processing the event. Comparing IDs inside a forged request proves nothing by itself.&lt;/li&gt;
&lt;li&gt;Redeliver the same event in your test environment. It should not create a second write operation. That is a separate &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=en" rel="noopener noreferrer"&gt;webhook idempotency problem&lt;/a&gt;, not a property of contact checking.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The local example does not establish the results of these integration tests. Record the client, time, selected branch, and test CRM change separately. Technical logs generally need the result and internal record references, not full contacts or secrets copied into them unnecessarily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this check stops
&lt;/h2&gt;

&lt;p&gt;Request a phone number only for a purpose the user understands. Restrict access, set a retention period, and provide a way to correct or delete it. Linking a contact to a Telegram ID does not establish permission for advertising calls, verify legal identity, or guarantee that the number is currently reachable. Assess data-processing and messaging requirements separately for your use case.&lt;/p&gt;

&lt;p&gt;Readiness means that a contact with an unknown or different user's ID does not become the sender's phone number, declining does not interrupt the conversation, and redelivery does not add an extra record. Connect the procedure to your production CRM only after these checks.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was also published on other platforms. Original on 24go.site: &lt;a href="https://www.24go.site/blog/telegram-bot-contact-owner-check?locale=en" rel="noopener noreferrer"&gt;Telegram Bot Contacts: Avoid Saving Someone Else's Phone in Your CRM&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Контакт в Telegram-боте: как не записать чужой телефон в CRM [перекрёстная публикация]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Fri, 23 Oct 2026 03:08:38 +0000</pubDate>
      <link>https://dev.to/dev24pro/kontakt-v-telegram-botie-kak-nie-zapisat-chuzhoi-tieliefon-v-crm-pieriekriostnaia-publikatsiia-ofa</link>
      <guid>https://dev.to/dev24pro/kontakt-v-telegram-botie-kak-nie-zapisat-chuzhoi-tieliefon-v-crm-pieriekriostnaia-publikatsiia-ofa</guid>
      <description>&lt;h1&gt;
  
  
  Контакт в Telegram-боте: как не записать чужой телефон в CRM
&lt;/h1&gt;

&lt;p&gt;Не записывайте любой полученный &lt;code&gt;contact.phone_number&lt;/code&gt; как телефон отправителя. Для автоматической привязки в личном чате сопоставьте &lt;code&gt;message.contact.user_id&lt;/code&gt; и &lt;code&gt;message.from.id&lt;/code&gt;. Если идентификатор контакта отсутствует или отличается, оставьте телефон непривязанным и попросите пользователя поделиться своим контактом через кнопку.&lt;/p&gt;

&lt;p&gt;Это правило для собственного обработчика Telegram Bot API, а не инструкция по настройке функции 24go.site. Предполагается, что сервер уже проверил подлинность доставки и структуру обновления. Совпадение идентификаторов связывает контакт с Telegram-пользователем в полученном событии. Это не замена SMS-проверке, установлению личности или разрешению на маркетинговые сообщения.&lt;/p&gt;

&lt;h2&gt;
  
  
  Сначала запросите нужный контакт
&lt;/h2&gt;

&lt;p&gt;В &lt;a href="https://core.telegram.org/bots/api#keyboardbutton" rel="noopener noreferrer"&gt;KeyboardButton&lt;/a&gt; поле &lt;code&gt;request_contact: true&lt;/code&gt; означает, что при нажатии кнопки телефон пользователя отправляется как контакт. Эта возможность доступна только в личных чатах. Используйте обычную reply-клавиатуру, а не inline-кнопку.&lt;/p&gt;

&lt;p&gt;Например, передайте такой объект как &lt;code&gt;reply_markup&lt;/code&gt; в существующем вызове &lt;code&gt;sendMessage&lt;/code&gt; вашего бота:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keyboard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Поделиться своим телефоном"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"request_contact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resize_keyboard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"one_time_keyboard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Объясните цель в тексте сообщения: «Поделитесь своим телефоном, если хотите, чтобы менеджер связался с вами по этой заявке. Можно продолжить разговор здесь». Само наличие кнопки не означает, что телефон уже получен. Обрабатывайте пришедшее сообщение, а не факт отправки клавиатуры.&lt;/p&gt;

&lt;p&gt;Пользователь также может прислать контакт вручную. В объекте &lt;a href="https://core.telegram.org/bots/api#contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; поле &lt;code&gt;user_id&lt;/code&gt; необязательно. Поэтому обработчику нужна ветка для отсутствующего ID, даже если успешный тест с вашей кнопкой всегда возвращал ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Решение перед записью в CRM
&lt;/h2&gt;

&lt;p&gt;Сравнивайте ID пользователя из &lt;code&gt;message.from.id&lt;/code&gt;, а не имя, username или строку телефона. &lt;a href="https://core.telegram.org/bots/api#user" rel="noopener noreferrer"&gt;User.id&lt;/a&gt; обозначает идентификатор пользователя или бота. Это не номер телефона.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Вход&lt;/th&gt;
&lt;th&gt;Решение обработчика&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Чат не личный&lt;/td&gt;
&lt;td&gt;Не выполнять эту процедуру привязки&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;В сообщении нет контакта&lt;/td&gt;
&lt;td&gt;Продолжить обычный диалог, не менять телефон&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Нет ID отправителя или &lt;code&gt;contact.user_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Оставить контакт непривязанным&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Оба ID есть, но различаются&lt;/td&gt;
&lt;td&gt;Не записывать контакт как телефон отправителя&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ID совпадают, номер присутствует&lt;/td&gt;
&lt;td&gt;Разрешить следующий шаг согласно вашей политике хранения&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Отсутствующий ID не доказывает обман. Несовпадение тоже не повод обвинять человека: он мог отправить контакт коллеги. Ответьте нейтрально: «Не удалось связать этот контакт с вашим Telegram-профилем. Если хотите, чтобы мы позвонили вам, воспользуйтесь кнопкой, чтобы отправить свой контакт». Для обращения от имени другого человека нужен отдельный процесс, а не ослабление проверки.&lt;/p&gt;

&lt;p&gt;Совпадение не должно молча перезаписывать ранее подтверждённый номер в CRM. Сохраняйте источник, время получения, ID отправителя и результат проверки. Правило обновления существующего телефона определите отдельно. Не объединяйте записи разных людей только потому, что совпали строки телефонов.&lt;/p&gt;

&lt;h2&gt;
  
  
  Локальный тест без бота и CRM
&lt;/h2&gt;

&lt;p&gt;Код ниже проверяет только выбор ветки на искусственных данных. Он не принимает HTTP-запросы, не проверяет секрет вебхука, не нормализует номера и не сохраняет их. Нужен Python 3, дополнительные библиотеки не требуются. Сохраните код в &lt;code&gt;contact_policy_test.py&lt;/code&gt; и выполните &lt;code&gt;python contact_policy_test.py&lt;/code&gt; без флага &lt;code&gt;-O&lt;/code&gt;, который отключает &lt;code&gt;assert&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;contact_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outside_private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;sender_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;contact_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sender_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unlinked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sender_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;different_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unlinked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;same_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TEST_ONLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deepcopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2002&lt;/span&gt;
&lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;contact_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;same_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;different_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unlinked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outside_private&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unlinked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unlinked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ожидаемый вывод:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"same_user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"different_user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unlinked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"no_contact"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"outside_private"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unlinked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unlinked"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TEST_ONLY&lt;/code&gt; намеренно не является настоящим номером. Результат &lt;code&gt;same_user&lt;/code&gt; означает только, что вход прошёл показанные проверки. Он не подтверждает, что по этой строке можно позвонить. В последнем случае отклоняется строковый ID: пример ожидает числовые ID из JSON Bot API. Если ваша библиотека преобразует типы, явно задайте правила такого преобразования на границе и протестируйте их.&lt;/p&gt;

&lt;p&gt;При &lt;code&gt;AssertionError&lt;/code&gt; выясните, какое правило изменилось, а не удаляйте проверку. Перед вызовом этой функции проверьте структуру: например, &lt;code&gt;chat&lt;/code&gt; и &lt;code&gt;from&lt;/code&gt; должны быть объектами ожидаемой формы. Это не универсальный парсер произвольного JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что отдельно проверить на настоящем боте
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;В тестовом личном чате отправьте reply-кнопку, нажмите её и проверьте полученные &lt;code&gt;from.id&lt;/code&gt; и &lt;code&gt;contact.user_id&lt;/code&gt;. Не публикуйте реальный телефон в журнале теста.&lt;/li&gt;
&lt;li&gt;В контролируемой проверке отправьте контакт другого тестового участника с его разрешения. При несовпадении ID автоматическая привязка должна быть отклонена; при отсутствии ID ожидайте &lt;code&gt;unlinked&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Отправьте обычный текст после контакта. Он не должен стирать сохранённый телефон. Отказ поделиться номером не должен прерывать доступный диалог в Telegram.&lt;/li&gt;
&lt;li&gt;Проверьте входной HTTP-контур. &lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;setWebhook&lt;/a&gt; поддерживает &lt;code&gt;secret_token&lt;/code&gt; и заголовок &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt;. Обработчик должен проверить настроенный секрет до обработки события. Сравнение ID внутри поддельного запроса само по себе ничего не удостоверяет.&lt;/li&gt;
&lt;li&gt;Повторно доставьте то же событие в тестовой среде. Оно не должно создавать вторую операцию записи. Это отдельная задача &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=ru" rel="noopener noreferrer"&gt;идемпотентности вебхуков&lt;/a&gt;, а не свойство проверки контакта.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Локальный пример не подтверждает результаты этих интеграционных проверок. Отдельно зафиксируйте клиент, время, выбранную ветку и изменение в тестовой CRM. В технических логах обычно достаточно результата и внутренних ссылок на запись. Не переносите туда полные контакты и секреты без необходимости.&lt;/p&gt;

&lt;h2&gt;
  
  
  Где заканчивается эта проверка
&lt;/h2&gt;

&lt;p&gt;Запрашивайте телефон только для цели, понятной пользователю. Ограничьте доступ к номеру, установите срок хранения и предусмотрите способ его исправления или удаления. Связь контакта с Telegram ID не доказывает право звонить с рекламой, не подтверждает личность и не гарантирует, что номер сейчас доступен. Требования к обработке данных и сообщениям отдельно оцените для своего сценария.&lt;/p&gt;

&lt;p&gt;Критерий готовности: контакт с неизвестным или отличающимся ID не становится телефоном отправителя, отказ не прерывает диалог, а повторная доставка не добавляет лишнюю запись. Подключайте эту процедуру к рабочей CRM только после таких проверок.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Эта статья также опубликована на других платформах. Оригинал на dev.to: &lt;a href="https://www.24go.site/blog/telegram-bot-contact-owner-check?locale=ru" rel="noopener noreferrer"&gt;Контакт в Telegram-боте: как не записать чужой телефон в CRM&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Repeated Telegram Webhooks: Avoiding a Second CRM Lead</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Thu, 22 Oct 2026 08:50:35 +0000</pubDate>
      <link>https://dev.to/dev24pro/povtornyi-viebkhuk-telegram-kak-nie-sozdat-vtoroi-lid-v-crm-cross-post-40i5</link>
      <guid>https://dev.to/dev24pro/povtornyi-viebkhuk-telegram-kak-nie-sozdat-vtoroi-lid-v-crm-cross-post-40i5</guid>
      <description>&lt;h1&gt;
  
  
  Repeated Telegram Webhooks: Avoiding a Second CRM Lead
&lt;/h1&gt;

&lt;p&gt;If the same Telegram event arrives twice, your integration should not create a second CRM lead. A stable event identifier and a processing record help prevent that. But checking whether an ID has been seen is not enough: if the CRM creates a lead and its response is lost, your handler cannot tell whether it is safe to send another create request.&lt;/p&gt;

&lt;p&gt;This is a design guide for developers connecting Telegram Bot API → their own handler → a CRM. It does not describe a verified built-in feature of 24go.site. It assumes your bot already receives the relevant enquiries legitimately, and that you have a database and a test CRM. The design does not grant permission to collect other people's contact details or send unsolicited messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give events, contacts, and deals different identities
&lt;/h2&gt;

&lt;p&gt;These objects need different keys and rules for handling repeats.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;What its key protects&lt;/th&gt;
&lt;th&gt;Example key in your integration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Incoming event&lt;/td&gt;
&lt;td&gt;Processing the same delivery twice&lt;/td&gt;
&lt;td&gt;source + your tenant + bot + &lt;code&gt;update_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contact&lt;/td&gt;
&lt;td&gt;Linking multiple enquiries to one person&lt;/td&gt;
&lt;td&gt;tenant + source + user identifier, when available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deal&lt;/td&gt;
&lt;td&gt;Separating a new enquiry from an existing conversation&lt;/td&gt;
&lt;td&gt;internal enquiry ID under an agreed business rule&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the &lt;a href="https://core.telegram.org/bots/api#update" rel="noopener noreferrer"&gt;Telegram Bot API&lt;/a&gt;, &lt;code&gt;update_id&lt;/code&gt; uniquely identifies an update. Telegram explicitly documents its use for ignoring repeated updates. The additional components above are a proposed isolation scheme for your system, not Telegram's key format.&lt;/p&gt;

&lt;p&gt;Two different messages from one person are not duplicate deliveries of the same event. They might extend one deal or start different deals; agree on the rule with the process owner. Do not turn “one delivery, one processing action” into “one person, one deal forever.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Receiver: persist first, acknowledge second
&lt;/h2&gt;

&lt;p&gt;Idempotency means that repeating an operation does not produce an additional effect. Store incoming updates durably rather than relying only on process memory.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authenticate the request and validate its expected structure. Telegram's &lt;code&gt;setWebhook&lt;/code&gt; supports &lt;code&gt;secret_token&lt;/code&gt;; when configured, Telegram sends it in &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt;. Check it before queuing work. Do not log the token's value.&lt;/li&gt;
&lt;li&gt;Build the event key. Do not use request arrival time, which changes on redelivery.&lt;/li&gt;
&lt;li&gt;Store the event and set its state to &lt;code&gt;pending&lt;/code&gt; in one transaction. Put a unique constraint on the composite key. If jobs live in a separate table, write both records in the same transaction to avoid losing work between writes.&lt;/li&gt;
&lt;li&gt;Return a successful HTTP response after the commit succeeds. If the key already exists, acknowledge delivery without creating a new job. An existing &lt;code&gt;pending&lt;/code&gt; record must remain available to the worker.&lt;/li&gt;
&lt;li&gt;If durable storage fails, do not acknowledge success. Your handler should not claim to have accepted an event it has not stored.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://hookdeck.com/webhooks/guides/implement-webhook-idempotency" rel="noopener noreferrer"&gt;Hookdeck's idempotency guide&lt;/a&gt; describes unique constraints and processing history as idempotency strategies. The database constraint matters: separate “check whether it exists” and “insert” operations do not provide the same atomic protection against simultaneous requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worker: a timeout does not mean creation failed
&lt;/h2&gt;

&lt;p&gt;Before attempting creation, persist an &lt;code&gt;operation_id&lt;/code&gt; that identifies the business operation. Keep it unchanged across worker retries. Prevent two workers from executing the same operation at once with atomic job claiming, an ownership deadline, and a recovery procedure for abandoned jobs.&lt;/p&gt;

&lt;p&gt;Choose the next step based on capabilities verified in your CRM's own documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent creation is supported.&lt;/strong&gt; Reuse the same operation key on each retry within its documented validity period. Do not assume every CRM accepts an &lt;code&gt;Idempotency-Key&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A unique external ID and atomic upsert are supported.&lt;/strong&gt; Map the operation to that ID. Here, upsert means creating or updating by key; an ordinary search followed by creation does not provide equivalent protection against a race.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neither mechanism is available.&lt;/strong&gt; After an ambiguous timeout, move the job to &lt;code&gt;needs_reconciliation&lt;/code&gt;. Check the CRM using an external operation reference, request logs, or manual inspection. If you cannot establish the result, do not blindly submit another create request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After confirmed success, store &lt;code&gt;crm_id&lt;/code&gt; and mark the job &lt;code&gt;done&lt;/code&gt;. If the process dies after creating the lead but before saving the result, recovery must use the same idempotency mechanism or reconciliation. A local event table alone does not guarantee exactly-once effects in a remote CRM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what repeated and new messages should do
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same event key, &lt;code&gt;pending&lt;/code&gt; record&lt;/td&gt;
&lt;td&gt;Acknowledge delivery; continue the original job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same event key, &lt;code&gt;done&lt;/code&gt; record&lt;/td&gt;
&lt;td&gt;Do not repeat the CRM action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different &lt;code&gt;update_id&lt;/code&gt;, same contact&lt;/td&gt;
&lt;td&gt;Apply the rule for continuing or opening a deal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout after sending a create request&lt;/td&gt;
&lt;td&gt;Retry with a supported stable key or reconcile; do not generate a new random key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Late stage-change event&lt;/td&gt;
&lt;td&gt;Check its version or whether the transition is valid rather than overwriting unconditionally&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not transfer one provider's guarantees to another. For example, &lt;a href="https://docs.stripe.com/webhooks#event-delivery-behaviors" rel="noopener noreferrer"&gt;Stripe documents event retries and the absence of guaranteed delivery order&lt;/a&gt;. This illustrates why delivery contracts need checking; it is not the specification for your CRM or Telegram integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test failure paths before enabling the integration
&lt;/h2&gt;

&lt;p&gt;Use anonymized test enquiries and check more than the happy path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deliver the same key twice, then concurrently in two requests. Storage should contain one job, and the CRM effect should not be duplicated.&lt;/li&gt;
&lt;li&gt;Stop the worker after &lt;code&gt;pending&lt;/code&gt; is persisted. The event should still be available for processing after restart.&lt;/li&gt;
&lt;li&gt;Simulate a CRM record being created while its response is lost. Recovery should use the original key or move the job to &lt;code&gt;needs_reconciliation&lt;/code&gt;, not blindly create again.&lt;/li&gt;
&lt;li&gt;Send a different message from the same test contact. Confirm that event deduplication does not discard it.&lt;/li&gt;
&lt;li&gt;Test authentication failure and database failure. A forged request should not enter the queue, and an unsaved event should not receive a successful acknowledgement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the local SQLite check accompanying this article, inserting the same key twice sequentially left one row; inserting a different key left two. This checks a storage constraint using artificial data. It is not a CRM test, a concurrent HTTP test, or a test of the entire recovery design.&lt;/p&gt;

&lt;p&gt;For operation, monitor the age of &lt;code&gt;pending&lt;/code&gt; jobs, errors, and the number of jobs requiring &lt;code&gt;needs_reconciliation&lt;/code&gt;. Retain the mapping &lt;code&gt;event_key → operation_id → crm_id&lt;/code&gt;. Avoid placing full conversations and tokens in technical logs unnecessarily. Align key retention with actual redelivery periods and data requirements: after a key is deleted, an old event can look new again.&lt;/p&gt;

&lt;p&gt;Readiness is not just seeing a test lead appear once. Confirm that retries do not add another effect, unfinished work can recover, and an ambiguous response does not turn into a duplicate deal.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was also published on other platforms. Original on 24go.site: &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=en" rel="noopener noreferrer"&gt;Repeated Telegram Webhooks: Avoiding a Second CRM Lead&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Повторный вебхук Telegram: как не создать второй лид в CRM [перекрёстная публикация]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Thu, 22 Oct 2026 08:50:34 +0000</pubDate>
      <link>https://dev.to/dev24pro/povtornyi-viebkhuk-telegram-kak-nie-sozdat-vtoroi-lid-v-crm-pieriekriostnaia-publikatsiia-be8</link>
      <guid>https://dev.to/dev24pro/povtornyi-viebkhuk-telegram-kak-nie-sozdat-vtoroi-lid-v-crm-pieriekriostnaia-publikatsiia-be8</guid>
      <description>&lt;h1&gt;
  
  
  Повторный вебхук Telegram: как не создать второй лид в CRM
&lt;/h1&gt;

&lt;p&gt;Если одно событие Telegram доставлено дважды, CRM не должна создавать второй лид. Устойчивый идентификатор события и запись о его обработке помогают этого избежать. Но одной проверки «мы уже видели этот ID» недостаточно: если CRM создала лид, а ответ потерялся, обработчик не знает, безопасно ли повторять запрос.&lt;/p&gt;

&lt;p&gt;Это руководство по проектированию интеграции Telegram Bot API → собственного обработчика → CRM. Оно не описывает проверенную встроенную функцию 24go.site. Предполагается, что бот уже законно получает нужные обращения, а у вас есть база данных и тестовая CRM. Эта схема не даёт права собирать чужие контактные данные или отправлять сообщения без соответствующих оснований.&lt;/p&gt;

&lt;h2&gt;
  
  
  Разделите идентификаторы событий, контактов и сделок
&lt;/h2&gt;

&lt;p&gt;Для этих объектов нужны разные ключи и правила обработки повторов.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Объект&lt;/th&gt;
&lt;th&gt;Что защищает его ключ&lt;/th&gt;
&lt;th&gt;Пример ключа в вашей интеграции&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Входящее событие&lt;/td&gt;
&lt;td&gt;Повторную обработку одной доставки&lt;/td&gt;
&lt;td&gt;источник + ваш tenant + бот + &lt;code&gt;update_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Контакт&lt;/td&gt;
&lt;td&gt;Связь нескольких обращений с одним человеком&lt;/td&gt;
&lt;td&gt;tenant + источник + идентификатор пользователя, если он доступен&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сделка&lt;/td&gt;
&lt;td&gt;Разделение нового обращения и продолжения существующего диалога&lt;/td&gt;
&lt;td&gt;внутренний ID обращения согласно согласованному бизнес-правилу&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;В &lt;a href="https://core.telegram.org/bots/api#update" rel="noopener noreferrer"&gt;Telegram Bot API&lt;/a&gt; &lt;code&gt;update_id&lt;/code&gt; служит уникальным идентификатором обновления. Telegram прямо указывает, что он позволяет игнорировать повторные обновления. Дополнительные компоненты составного ключа выше предлагают изолировать записи в вашей системе; это не формат ключа Telegram.&lt;/p&gt;

&lt;p&gt;Два разных сообщения одного человека не являются повторной доставкой одного события. Они могут относиться к одной сделке или начинать разные сделки. Это следует согласовать с владельцем процесса. Не превращайте правило «одна доставка, одна обработка» в правило «один человек, одна сделка навсегда».&lt;/p&gt;

&lt;h2&gt;
  
  
  Приёмник: сначала сохранить, затем подтвердить
&lt;/h2&gt;

&lt;p&gt;Идемпотентность означает, что повтор операции не создаёт дополнительного эффекта. Входящие обновления нужно сохранять в постоянном хранилище, а не только в памяти процесса.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Проверьте подлинность запроса и ожидаемую структуру. В &lt;code&gt;setWebhook&lt;/code&gt; предусмотрен параметр &lt;code&gt;secret_token&lt;/code&gt;. Если он настроен, Telegram отправляет его в заголовке &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt;. Проверьте его до постановки задания в очередь. Не записывайте значение токена в журнал.&lt;/li&gt;
&lt;li&gt;Сформируйте ключ события. Не используйте время получения запроса: при повторной доставке оно изменится.&lt;/li&gt;
&lt;li&gt;В одной транзакции сохраните событие и установите ему состояние &lt;code&gt;pending&lt;/code&gt;. Наложите уникальное ограничение на составной ключ. Если задания хранятся в отдельной таблице, записывайте событие и задание в той же транзакции, чтобы между записями не потерять работу.&lt;/li&gt;
&lt;li&gt;После успешного commit верните успешный HTTP-ответ. Если ключ уже существует, подтвердите доставку, но не создавайте новое задание. Существующая запись &lt;code&gt;pending&lt;/code&gt; должна оставаться доступной воркеру.&lt;/li&gt;
&lt;li&gt;Если сохранить данные в постоянном хранилище не удалось, не отвечайте успехом. Обработчик не должен сообщать о приёме события, которого он не сохранил.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;В &lt;a href="https://hookdeck.com/webhooks/guides/implement-webhook-idempotency" rel="noopener noreferrer"&gt;руководстве Hookdeck по идемпотентности&lt;/a&gt; уникальные ограничения и история обработки описаны как стратегии защиты от повторов. Важна именно гарантия ограничения базы данных: отдельные операции «проверить наличие» и «вставить» не обеспечивают такую же атомарную защиту от одновременных запросов.&lt;/p&gt;

&lt;h2&gt;
  
  
  Воркер: таймаут не означает, что создание не удалось
&lt;/h2&gt;

&lt;p&gt;До попытки создания сохраните постоянный &lt;code&gt;operation_id&lt;/code&gt;, идентификатор этой бизнес-операции. Он должен оставаться неизменным при повторных запусках воркера. Не допускайте, чтобы два воркера одновременно выполняли одну операцию: используйте атомарный захват задания, срок владения и процедуру восстановления зависших заданий.&lt;/p&gt;

&lt;p&gt;Выберите следующий шаг, исходя из возможностей конкретной CRM, подтверждённых её документацией:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Поддерживается идемпотентное создание.&lt;/strong&gt; Повторно используйте тот же ключ операции в течение установленного срока его действия. Не предполагайте, что любая CRM принимает заголовок &lt;code&gt;Idempotency-Key&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Поддерживаются уникальный внешний ID и атомарный upsert.&lt;/strong&gt; Сопоставьте операцию с этим ID. Здесь upsert означает создание или обновление по ключу. Обычный поиск с последующим созданием не обеспечивает аналогичную защиту от гонки.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Нет ни одного из этих механизмов.&lt;/strong&gt; После неоднозначного таймаута переведите задание в &lt;code&gt;needs_reconciliation&lt;/code&gt;, то есть на сверку. Проверьте CRM по внешней ссылке на операцию, журналам запросов или вручную. Если результат установить не удалось, не отправляйте запрос на создание повторно вслепую.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;После подтверждённого успеха сохраните &lt;code&gt;crm_id&lt;/code&gt; и отметьте задание как &lt;code&gt;done&lt;/code&gt;. Если процесс завершится после создания лида, но до сохранения результата, при восстановлении нужно использовать тот же механизм идемпотентности или выполнить сверку. Одна лишь локальная таблица событий не гарантирует выполнение операции ровно один раз в удалённой CRM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Определите, что делать с повторными и новыми сообщениями
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ситуация&lt;/th&gt;
&lt;th&gt;Действие&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Тот же ключ события, запись &lt;code&gt;pending&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Подтвердить доставку и продолжить исходное задание&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Тот же ключ события, запись &lt;code&gt;done&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Не повторять действие в CRM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Другой &lt;code&gt;update_id&lt;/code&gt;, тот же контакт&lt;/td&gt;
&lt;td&gt;Применить правило продолжения или открытия сделки&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Таймаут после отправки запроса на создание&lt;/td&gt;
&lt;td&gt;Повторить запрос с поддерживаемым постоянным ключом или выполнить сверку; не генерировать новый случайный ключ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Запоздавшее событие об изменении стадии&lt;/td&gt;
&lt;td&gt;Проверить версию или допустимость перехода, а не перезаписывать стадию безусловно&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Не переносите гарантии одного поставщика на другого. Например, &lt;a href="https://docs.stripe.com/webhooks#event-delivery-behaviors" rel="noopener noreferrer"&gt;Stripe описывает повторную доставку событий и отсутствие гарантированного порядка доставки&lt;/a&gt;. Это пример того, почему нужно проверять условия доставки, а не спецификация вашей CRM или Telegram-интеграции.&lt;/p&gt;

&lt;h2&gt;
  
  
  Проверьте сценарии отказа до включения интеграции
&lt;/h2&gt;

&lt;p&gt;На обезличенных тестовых обращениях проверьте не только штатный сценарий:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Доставьте один и тот же ключ дважды, затем одновременно в двух запросах. В хранилище должно остаться одно задание, а эффект в CRM не должен дублироваться.&lt;/li&gt;
&lt;li&gt;Остановите воркер после сохранения &lt;code&gt;pending&lt;/code&gt;. После перезапуска событие должно оставаться доступным для обработки.&lt;/li&gt;
&lt;li&gt;Смоделируйте создание записи в CRM с потерей ответа. При восстановлении нужно использовать исходный ключ или перевести задание в &lt;code&gt;needs_reconciliation&lt;/code&gt;, а не повторять создание вслепую.&lt;/li&gt;
&lt;li&gt;Отправьте другое сообщение от того же тестового контакта. Убедитесь, что дедупликация событий не отбрасывает его.&lt;/li&gt;
&lt;li&gt;Проверьте отказ в авторизации и сбой базы данных. Поддельный запрос не должен попасть в очередь, а несохранённое событие не должно получить успешное подтверждение.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;В локальной проверке SQLite к этой статье один ключ последовательно вставили дважды: в таблице осталась одна строка. Вставка другого ключа увеличила число строк до двух. Это проверка ограничения хранилища на искусственных данных, а не тест CRM, конкурентных HTTP-запросов или всей схемы восстановления.&lt;/p&gt;

&lt;p&gt;Для эксплуатации отслеживайте возраст заданий &lt;code&gt;pending&lt;/code&gt;, ошибки и количество заданий &lt;code&gt;needs_reconciliation&lt;/code&gt;. Храните связь &lt;code&gt;event_key → operation_id → crm_id&lt;/code&gt;. Не записывайте полные переписки и токены в технические журналы без необходимости. Срок хранения ключей согласуйте с фактическим периодом повторной доставки и требованиями к данным: после удаления ключа старое событие может снова выглядеть новым.&lt;/p&gt;

&lt;p&gt;Готовность определяется не тем, что тестовый лид появился один раз. Нужно убедиться, что повторная доставка не создаёт дополнительный эффект, незавершённая работа восстанавливается, а неоднозначный ответ не приводит к дублирующей сделке.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Эта статья также опубликована на других платформах. Оригинал на dev.to: &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=ru" rel="noopener noreferrer"&gt;Повторный вебхук Telegram: как не создать второй лид в CRM&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
  </channel>
</rss>
