fix: classify Telegram ticket replies by message id and record safe routing diagnostics
This commit is contained in:
parent
7622b49cb3
commit
5b643135e1
@ -35,6 +35,9 @@ class TelegramController extends Controller
|
||||
$data = $request->json()->all();
|
||||
|
||||
$this->formatMessage($data);
|
||||
if ($this->msg) {
|
||||
app(\App\Services\TelegramTickets\TicketBridge::class)->observeIncoming($this->msg->raw_message, (int) $this->msg->update_id);
|
||||
}
|
||||
$this->formatChatJoinRequest($data);
|
||||
$this->handle();
|
||||
}
|
||||
@ -97,9 +100,9 @@ class TelegramController extends Controller
|
||||
'is_private' => $message['chat']['type'] === 'private',
|
||||
];
|
||||
|
||||
if (isset($message['reply_to_message']['text'])) {
|
||||
if (isset($message['reply_to_message']['message_id'])) {
|
||||
$this->msg->message_type = 'reply_message';
|
||||
$this->msg->reply_text = $message['reply_to_message']['text'];
|
||||
$this->msg->reply_text = $message['reply_to_message']['text'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,16 @@ use Illuminate\Support\Facades\DB;
|
||||
class TicketBridge {
|
||||
public function settings():array {return array_replace(['notify_enabled'=>true,'reply_enabled'=>false,'receiver_user_ids'=>[]],(array)admin_setting('telegram_ticket',[]));}
|
||||
public function botKey():string {return hash('sha256',(string)admin_setting('telegram_bot_token',''));}
|
||||
public function observeIncoming(array $message,int $updateId):void {
|
||||
// Keep only short-lived routing facts for bound administrators, never message text or credentials.
|
||||
if(($message['chat']['type']??'')!=='private'||($message['from']['is_bot']??true)||($message['from']['id']??null)!=($message['chat']['id']??0))return;
|
||||
try {
|
||||
$admin=User::where('telegram_id',$message['from']['id'])->where('is_admin',1)->where('banned',0)->first();if(!$admin)return;
|
||||
$replyId=$message['reply_to_message']['message_id']??null;
|
||||
$matched=$replyId!==null&&TelegramTicketMessage::where('bot_key',$this->botKey())->where('chat_id',$message['chat']['id'])->where('telegram_message_id',$replyId)->exists();
|
||||
Cache::put('tg_ticket_inbound_'.$this->botKey().'_'.$admin->id,['received_at'=>now()->toIso8601String(),'update_id'=>$updateId,'is_reply'=>$replyId!==null,'matched_ticket'=>$matched],3600);
|
||||
}catch(\Throwable $e){\Illuminate\Support\Facades\Log::warning('Telegram ticket routing diagnostics unavailable');}
|
||||
}
|
||||
public function receivers() {
|
||||
$q=User::where('is_admin',1)->whereNotNull('telegram_id')->where('banned',0);
|
||||
$ids=$this->settings()['receiver_user_ids'];if($ids)$q->whereIn('id',$ids);return $q->get();
|
||||
|
||||
@ -65,3 +65,9 @@ Resend 发信、常规 SMTP、配额回退、投递事件回调、master 一次
|
||||
检查当前 app 与 plugins-core 发信入口:工单创建/用户追加内容推送 Telegram,管理员回复的邮件收件人为工单提交用户;收款与节点离线通知也通过 Telegram;用量日报由 master 发送 Telegram。未发现专门向管理员邮箱发送上述汇报的代码,因此没有需要移除的管理员邮件汇报实现。本次不新增全局邮箱封锁,不删除验证码、登录邮件、人工群发或正常用户通知;管理员汇报渠道约束已写入 BUSINESS_RULES.md。该核查不是新的真实工单验收。
|
||||
|
||||
用户随后明确关闭管理员邮箱汇报应通过开关保留实现,不能删除代码。已更新业务约束;当前尚未找到此类实际邮件入口,本轮没有删代码,也没有添加没有实际对象的开关。若后续增加该类入口,须提供独立默认关闭的开关。线上机器人现已启用,官方回调地址已注册,管理员绑定数仍为零;真实工单链路尚未验收。
|
||||
|
||||
### Telegram 真实回复排查
|
||||
|
||||
管理员完成绑定并开启回复后,已通过正式队列向管理员私聊发送一条隔离测试工单通知。用户确认收到并已使用回复功能发送文字,但现场核对仍未发现管理员回复、关联更新记录或 ticket_reply 邮件。绑定、通知映射、回复权限和官方回调 HTTP 200 均已核对,不能据此宣布真实闭环通过。
|
||||
|
||||
补充真实控制器 → 原插件 → 工单与邮件队列的隔离回归,回复按引用消息编号识别而不依赖引用原文;为已绑定管理员增加一小时自动过期、无正文的路由诊断缓存。工单与邮件测试合计 70 项、383 个断言通过,PHP 语法检查通过。此处记录为本地修正,发布及真实回复验收仍需现场核对。
|
||||
|
||||
@ -33,3 +33,7 @@
|
||||
迁移包含两张新表,应先备份数据库,再通过 Git 发布并执行迁移,刷新后台视图并重载队列。回滚代码时保留消息关联和去重记录,避免重复通知。
|
||||
|
||||
隔离测试与网页显示/保存不能替代真实验收。完整验收需管理员完成绑定,使用测试用户提交工单,管理员私聊回复,确认网站保存、邮件实际收到和重复更新不重复写入。记录结果到 STATUS.md;不得保存用户原始工单或令牌。
|
||||
|
||||
## 回复接收排查
|
||||
|
||||
回复按 reply_to_message.message_id 识别,不依赖引用原文是否包含 text。对已绑定且未禁用管理员的私聊文字消息,在缓存保留最后一次接收时间、更新编号、是否使用回复、是否命中工单关联,最多一小时;不保存消息正文、账号凭据或原始请求。该记录仅供服务器维护排错,不能以收到回调代替工单写回或邮件送达验收。
|
||||
|
||||
@ -94,6 +94,31 @@ class TicketBridgeTest extends TestCase {
|
||||
$this->assertStringContainsString('已绑定其他网站',$this->bridge->bind($this->message(['chat'=>['id'=>999],'from'=>['id'=>999]]),$code));
|
||||
$this->assertSame(1,TicketMessage::count());$this->assertSame(123,(int)$this->admin->fresh()->telegram_id);
|
||||
}
|
||||
public function test_real_webhook_routes_reply_by_message_id_without_original_text():void {
|
||||
$this->app->instance('hook.filters',[]);
|
||||
require_once base_path('plugins-core/Telegram/Plugin.php');
|
||||
Http::fake(['api.telegram.org/*'=>Http::response(['ok'=>true,'result'=>['message_id'=>987]],200)]);
|
||||
$plugin=new \Plugin\Telegram\Plugin('telegram');$plugin->boot();
|
||||
admin_setting(['telegram_webhook_secret'=>'testsecret']);
|
||||
$message=$this->message(['message_id'=>567]);
|
||||
$request=\Illuminate\Http\Request::create('/','POST',[],[],[],['CONTENT_TYPE'=>'application/json'],json_encode(['update_id'=>55,'message'=>$message]));
|
||||
$request->headers->set('X-Telegram-Bot-Api-Secret-Token','testsecret');
|
||||
app(\App\Http\Controllers\V1\Guest\TelegramController::class)->webhook($request);
|
||||
$this->assertSame(2,TicketMessage::count());
|
||||
Bus::assertDispatched(SendEmailJob::class,1);
|
||||
$diagnostic=Cache::get('tg_ticket_inbound_'.$this->bridge->botKey().'_'.$this->admin->id);
|
||||
$this->assertTrue($diagnostic['is_reply']);$this->assertTrue($diagnostic['matched_ticket']);
|
||||
$this->assertArrayNotHasKey('text',$diagnostic);
|
||||
}
|
||||
public function test_routing_diagnostics_distinguish_unmatched_reply_and_ignore_customers():void {
|
||||
$this->bridge->observeIncoming($this->message(['reply_to_message'=>['message_id'=>999]]),9);
|
||||
$key='tg_ticket_inbound_'.$this->bridge->botKey().'_'.$this->admin->id;
|
||||
$this->assertFalse(Cache::get($key)['matched_ticket']);
|
||||
$message=$this->message();unset($message['reply_to_message']);$this->bridge->observeIncoming($message,10);
|
||||
$this->assertFalse(Cache::get($key)['is_reply']);
|
||||
$this->bridge->observeIncoming($this->message(['from'=>['id'=>999]]),11);
|
||||
$this->assertSame(10,Cache::get($key)['update_id']);
|
||||
}
|
||||
public function test_webhook_checks_secret_header_and_refuses_another_bot_webhook():void {
|
||||
Http::fake(['api.telegram.org/*/getMe'=>Http::response(['ok'=>true,'result'=>['username'=>'testbot']],200),
|
||||
'api.telegram.org/*/getWebhookInfo'=>Http::response(['ok'=>true,'result'=>['url'=>'https://another.example/webhook']],200)]);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user