37 lines
20 KiB
Plaintext
37 lines
20 KiB
Plaintext
CHAT_OUTPUT_BEGIN
|
|
COMMAND_ID=SUPPORT-260918-GRAM-SUPPORT-STAFF-EMAIL-SOURCE-READONLY-516R1R4
|
|
STATUS=OK
|
|
RC=0
|
|
HOST=pve01
|
|
MODE=read-only
|
|
COMPONENT=gram-support-staff-email-source-readonly
|
|
REFERENCE_REGISTER_CHECK=OK
|
|
REFERENCE_SHA256=5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66
|
|
ERROR_REGISTER_CHECK=OK
|
|
ERROR_REGISTER_SHA256=3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0
|
|
COMMAND_SHA256=08ff6acd78dea5c4a5621dcca2fedad234c67817f25ff1f3b4ee9b9b2d14c5f0
|
|
DUPLICATE_FAILED_COMMAND_BLOCKED=false
|
|
EXECUTION_STARTED=true
|
|
CHANGE_DECLARED=false
|
|
RESULT_CONTRACT_VALID=true
|
|
RESULT_CONTRACT_STATUS=NOT_APPLICABLE
|
|
RESULT_CONTRACT_ERROR=NONE
|
|
COMMAND_RC=0
|
|
CHANGES_MADE=false
|
|
ROLLBACK_STARTED=false
|
|
ROLLBACK_RESTORED=null
|
|
MUTATION_OUTCOME=NO_MUTATION
|
|
SANITIZED=yes
|
|
SECRETS_INCLUDED=no
|
|
PRIVATE_ADDRESSES_INCLUDED=no
|
|
RAW_EVIDENCE_SHA256=3375e95bfa30e7b6adf9c22d4a03b7407be9113f1d2ed450ec96bed61add1e9a
|
|
SANITIZED_OUTPUT_SHA256=3375e95bfa30e7b6adf9c22d4a03b7407be9113f1d2ed450ec96bed61add1e9a
|
|
OUTPUT_BEGIN
|
|
GRAM516R1R4_SAFE_REPORT={"classification":{"all_sites_eligible_staff_match_target":true,"changes_made":false,"eligible_staff_target_match_total":7,"eligible_staff_total":7,"email_consumer_present":true,"event_dispatcher_present":true,"runtime_probe_7of7":true,"send_attempts":0,"source_probe_ok":true},"schema_version":1,"scope":"READ_ONLY_GRAM_SUPPORT_STAFF_EMAIL_SOURCE","secret_policy":"NO_NON_TARGET_EMAIL_ADDRESSES_EXPORTED","sites":[{"all_eligible_match_target":true,"eligible_staff_count":1,"eligible_target_match_count":1,"slug":"codevipe","staff":[{"email_matches_target":true,"email_nonempty":true,"gram_manage":true,"gram_viewAll":true,"is_admin":true,"is_moderator":true,"primary_group_id":2,"secondary_group_ids":[3,4],"staff_eligible":true,"user_id_sha256":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"}]},{"all_eligible_match_target":true,"eligible_staff_count":1,"eligible_target_match_count":1,"slug":"dsmods","staff":[{"email_matches_target":true,"email_nonempty":true,"gram_manage":true,"gram_viewAll":true,"is_admin":true,"is_moderator":true,"primary_group_id":2,"secondary_group_ids":[3,4],"staff_eligible":true,"user_id_sha256":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"}]},{"all_eligible_match_target":true,"eligible_staff_count":1,"eligible_target_match_count":1,"slug":"gamevipe","staff":[{"email_matches_target":true,"email_nonempty":true,"gram_manage":true,"gram_viewAll":true,"is_admin":true,"is_moderator":true,"primary_group_id":2,"secondary_group_ids":[3,4],"staff_eligible":true,"user_id_sha256":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"}]},{"all_eligible_match_target":true,"eligible_staff_count":1,"eligible_target_match_count":1,"slug":"hkmods","staff":[{"email_matches_target":true,"email_nonempty":true,"gram_manage":true,"gram_viewAll":true,"is_admin":true,"is_moderator":true,"primary_group_id":2,"secondary_group_ids":[3,4],"staff_eligible":true,"user_id_sha256":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"}]},{"all_eligible_match_target":true,"eligible_staff_count":1,"eligible_target_match_count":1,"slug":"kingofwolk","staff":[{"email_matches_target":true,"email_nonempty":true,"gram_manage":true,"gram_viewAll":true,"is_admin":true,"is_moderator":true,"primary_group_id":2,"secondary_group_ids":[3,4],"staff_eligible":true,"user_id_sha256":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"}]},{"all_eligible_match_target":true,"eligible_staff_count":1,"eligible_target_match_count":1,"slug":"zakrutim","staff":[{"email_matches_target":true,"email_nonempty":true,"gram_manage":true,"gram_viewAll":true,"is_admin":true,"is_moderator":true,"primary_group_id":2,"secondary_group_ids":[3,4],"staff_eligible":true,"user_id_sha256":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"}]},{"all_eligible_match_target":true,"eligible_staff_count":1,"eligible_target_match_count":1,"slug":"newfi","staff":[{"email_matches_target":true,"email_nonempty":true,"gram_manage":true,"gram_viewAll":true,"is_admin":true,"is_moderator":true,"primary_group_id":2,"secondary_group_ids":[3,4],"staff_eligible":true,"user_id_sha256":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"}]}],"source":{"files":{"Notification/DeliveryGate.php":{"line_count":16,"present":true,"text":"<?php\n\nnamespace Gram\\Support\\Notification;\n\ninterface DeliveryGate\n{\n /**\n * Return true only when this exact event/consumer/recipient delivery\n * has not already been completed and this caller owns the retry lease.\n */\n public function acquire(int $eventId, string $consumer, string $recipientKey): bool;\n\n public function markSent(int $eventId, string $consumer, string $recipientKey): void;\n\n public function markFailed(int $eventId, string $consumer, string $recipientKey, string $failureClass): void;\n}\n"},"Notification/Dispatcher.php":{"line_count":67,"present":true,"text":"<?php\n\nnamespace Gram\\Support\\Notification;\n\nuse Gram\\Support\\Entity\\SupportEvent;\nuse Gram\\Support\\Entity\\Ticket;\nuse XF\\Entity\\User;\n\nfinal class Dispatcher\n{\n private AlertConsumer $alerts;\n private EmailConsumer $email;\n\n public function __construct(AlertConsumer $alerts, EmailConsumer $email)\n {\n $this->alerts = $alerts;\n $this->email = $email;\n }\n\n /**\n * Owner-facing delivery. Staff alert fan-out is intentionally provided by\n * dispatchStaff() so staff recipients must still pass Gram Support ACL.\n */\n public function dispatchOwner(SupportEvent $event, Ticket $ticket, ?User $owner): void\n {\n if (!Policy::mayEmitExternal($event))\n {\n return;\n }\n\n if ($owner && $owner->user_id)\n {\n $this->alerts->consumeForUser($event, $ticket, $owner);\n $this->email->consumeForRegistered($event, $ticket, $owner);\n return;\n }\n\n $this->email->consumeForGuest($event, $ticket);\n }\n\n /**\n * @param User[] $staffUsers\n */\n public function dispatchStaff(SupportEvent $event, Ticket $ticket, array $staffUsers): void\n {\n if (!Policy::mayEmitExternal($event))\n {\n return;\n }\n\n foreach ($staffUsers as $staff)\n {\n if (!$staff instanceof User || !$staff->user_id)\n {\n continue;\n }\n\n if (!$staff->hasPermission('gramSupport', 'viewAll')\n && !$staff->hasPermission('gramSupport', 'manage'))\n {\n continue;\n }\n\n $this->alerts->consumeForUser($event, $ticket, $staff);\n }\n }\n}\n"},"Notification/EmailConsumer.php":{"line_count":107,"present":true,"text":"<?php\n\nnamespace Gram\\Support\\Notification;\n\nuse Gram\\Support\\Entity\\SupportEvent;\nuse Gram\\Support\\Entity\\Ticket;\nuse XF\\Entity\\User;\n\nfinal class EmailConsumer\n{\n private DeliveryGate $gate;\n\n public function __construct(DeliveryGate $gate)\n {\n $this->gate = $gate;\n }\n\n public function consumeForRegistered(SupportEvent $event, Ticket $ticket, User $recipient): bool\n {\n if (!Policy::mayEmitExternal($event) || !$recipient->user_id)\n {\n return false;\n }\n\n if (!$ticket->user_id || (int)$ticket->user_id !== (int)$recipient->user_id)\n {\n return false;\n }\n\n $recipientKey = RecipientKey::user((int)$recipient->user_id);\n return $this->send(\n $event,\n $recipientKey,\n function () use ($event, $ticket, $recipient)\n {\n \\XF::app()->mailer()->newMail()\n ->setToUser($recipient)\n ->setTemplate('gram_support_ticket_event', [\n 'ticket' => $ticket,\n 'event' => $event,\n 'accessUrl' => null\n ])\n ->queue();\n }\n );\n }\n\n public function consumeForGuest(SupportEvent $event, Ticket $ticket): bool\n {\n if (!Policy::mayEmitExternal($event))\n {\n return false;\n }\n\n $email = trim((string)$ticket->guest_email);\n if ($email === '')\n {\n return false;\n }\n\n $recipientKey = RecipientKey::email($email);\n\n return $this->send(\n $event,\n $recipientKey,\n function () use ($event, $ticket, $email)\n {\n // Raw guest access token is intentionally unavailable here:\n // only its hash is persisted. Email links to the manual access\n // page; the guest uses the one-time/saved access code.\n \\XF::app()->mailer()->newMail()\n ->setTo($email)\n ->setTemplate('gram_support_ticket_event_guest', [\n 'ticket' => $ticket,\n 'event' => $event,\n 'accessUrl' => \\XF::app()->router('public')->buildLink('canonical:support/access')\n ])\n ->queue();\n }\n );\n }\n\n private function send(SupportEvent $event, string $recipientKey, callable $sender): bool\n {\n if (!$this->gate->acquire((int)$event->event_id, 'email', $recipientKey))\n {\n return false;\n }\n\n try\n {\n $sender();\n $this->gate->markSent((int)$event->event_id, 'email', $recipientKey);\n return true;\n }\n catch (\\Throwable $e)\n {\n $this->gate->markFailed(\n (int)$event->event_id,\n 'email',\n $recipientKey,\n get_class($e)\n );\n throw $e;\n }\n }\n}\n"},"Notification/EntityDeliveryGate.php":{"line_count":118,"present":true,"text":"<?php\n\nnamespace Gram\\Support\\Notification;\n\nfinal class EntityDeliveryGate implements DeliveryGate\n{\n private const LEASE_SECONDS = 120;\n\n public function acquire(int $eventId, string $consumer, string $recipientKey): bool\n {\n if ($eventId < 1 || $consumer === '' || $recipientKey === '')\n {\n return false;\n }\n\n $deliveryKey = $this->deliveryKey($eventId, $consumer, $recipientKey);\n $lockName = 'gram_support_delivery_' . substr($deliveryKey, 0, 32);\n $db = \\XF::db();\n\n if ((int)$db->fetchOne('SELECT GET_LOCK(?, 2)', [$lockName]) !== 1)\n {\n return false;\n }\n\n try\n {\n $now = \\XF::$time;\n $delivery = \\XF::em()->find('Gram\\Support:Delivery', $deliveryKey);\n\n if (!$delivery)\n {\n $delivery = \\XF::em()->create('Gram\\Support:Delivery');\n $delivery->delivery_key = $deliveryKey;\n $delivery->event_id = $eventId;\n $delivery->consumer = $consumer;\n $delivery->recipient_key = $recipientKey;\n $delivery->state = 'pending';\n $delivery->lease_expires = $now + self::LEASE_SECONDS;\n $delivery->attempt_count = 1;\n $delivery->created_date = $now;\n $delivery->updated_date = $now;\n $delivery->save();\n return true;\n }\n\n if ((string)$delivery->state === 'sent')\n {\n return false;\n }\n\n if ((string)$delivery->state === 'pending' && (int)$delivery->lease_expires > $now)\n {\n return false;\n }\n\n $delivery->state = 'pending';\n $delivery->lease_expires = $now + self::LEASE_SECONDS;\n $delivery->attempt_count = (int)$delivery->attempt_count + 1;\n $delivery->updated_date = $now;\n $delivery->save();\n\n return true;\n }\n finally\n {\n try\n {\n $db->fetchOne('SELECT RELEASE_LOCK(?)', [$lockName]);\n }\n catch (\\Throwable $ignored)\n {\n }\n }\n }\n\n public function markSent(int $eventId, string $consumer, string $recipientKey): void\n {\n $delivery = \\XF::em()->find(\n 'Gram\\Support:Delivery',\n $this->deliveryKey($eventId, $consumer, $recipientKey)\n );\n\n if (!$delivery)\n {\n return;\n }\n\n $delivery->state = 'sent';\n $delivery->lease_expires = 0;\n $delivery->updated_date = \\XF::$time;\n $delivery->sent_date = \\XF::$time;\n $delivery->save();\n }\n\n public function markFailed(int $eventId, string $consumer, string $recipientKey, string $failureClass): void\n {\n $delivery = \\XF::em()->find(\n 'Gram\\Support:Delivery',\n $this->deliveryKey($eventId, $consumer, $recipientKey)\n );\n\n if (!$delivery)\n {\n return;\n }\n\n $delivery->state = 'failed';\n $delivery->lease_expires = 0;\n $delivery->last_failure_class = substr($failureClass, 0, 191);\n $delivery->updated_date = \\XF::$time;\n $delivery->save();\n }\n\n private function deliveryKey(int $eventId, string $consumer, string $recipientKey): string\n {\n return hash('sha256', $eventId . \"\\0\" . $consumer . \"\\0\" . $recipientKey);\n }\n}\n"},"Notification/EventDispatcher.php":{"line_count":42,"present":true,"text":"<?php\n\nnamespace Gram\\Support\\Notification;\n\nuse Gram\\Support\\Entity\\SupportEvent;\nuse Gram\\Support\\Entity\\Ticket;\n\nfinal class EventDispatcher\n{\n public function dispatchSafely(SupportEvent $event, Ticket $ticket): void\n {\n try\n {\n $gate = new EntityDeliveryGate();\n $alerts = new AlertConsumer($gate);\n $email = new EmailConsumer($gate);\n $dispatcher = new Dispatcher($alerts, $email);\n\n $owner = null;\n if ((int)$ticket->user_id > 0)\n {\n $owner = \\XF::em()->find('XF:User', (int)$ticket->user_id);\n }\n\n $dispatcher->dispatchOwner($event, $ticket, $owner);\n\n if ((int)$ticket->assigned_user_id > 0)\n {\n $staff = \\XF::em()->find('XF:User', (int)$ticket->assigned_user_id);\n if ($staff)\n {\n $dispatcher->dispatchStaff($event, $ticket, [$staff]);\n }\n }\n }\n catch (\\Throwable $e)\n {\n // Event/Ticket/Message are already persisted truth.\n \\XF::logException($e, false, 'Gram Support notification consumer: ');\n }\n }\n}\n"},"Notification/Policy.php":{"line_count":61,"present":true,"text":"<?php\n\nnamespace Gram\\Support\\Notification;\n\nuse Gram\\Support\\Entity\\SupportEvent;\n\nfinal class Policy\n{\n private const INTERNAL_EVENT_TYPES = [\n 'internal_note_created',\n 'internal_note_updated',\n 'internal_note_deleted'\n ];\n\n public static function mayEmitExternal(SupportEvent $event): bool\n {\n if (in_array((string)$event->event_type, self::INTERNAL_EVENT_TYPES, true))\n {\n return false;\n }\n\n $payload = self::safePayload($event);\n\n if (!empty($payload['is_internal']))\n {\n return false;\n }\n\n return true;\n }\n\n public static function safePayload(SupportEvent $event): array\n {\n $payload = $event->safe_payload_json;\n\n if (is_array($payload))\n {\n return $payload;\n }\n\n if (!is_string($payload) || $payload === '')\n {\n return [];\n }\n\n $decoded = json_decode($payload, true);\n return is_array($decoded) ? $decoded : [];\n }\n\n public static function alertExtraData(SupportEvent $event): array\n {\n $payload = self::safePayload($event);\n\n return [\n 'support_event_id' => (int)$event->event_id,\n 'ticket_id' => (int)$event->ticket_id,\n 'event_type' => (string)$event->event_type,\n 'public_ref' => isset($payload['public_ref']) ? (string)$payload['public_ref'] : ''\n ];\n }\n}\n"},"Notification/RecipientKey.php":{"line_count":28,"present":true,"text":"<?php\n\nnamespace Gram\\Support\\Notification;\n\nfinal class RecipientKey\n{\n public static function user(int $userId): string\n {\n if ($userId < 1)\n {\n throw new \\InvalidArgumentException('User recipient ID must be positive.');\n }\n\n return 'user:' . $userId;\n }\n\n public static function email(string $email): string\n {\n $email = strtolower(trim($email));\n if ($email === '')\n {\n throw new \\InvalidArgumentException('Email recipient must not be empty.');\n }\n\n // Never persist raw guest email in the delivery key.\n return 'email-sha256:' . hash('sha256', $email);\n }\n}\n"}},"ok":true},"source_transport":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},"target_email":"aleisaev@yandex.ru","transport_evidence":[{"slug":"codevipe","t":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},{"slug":"dsmods","t":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},{"slug":"gamevipe","t":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},{"slug":"hkmods","t":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},{"slug":"kingofwolk","t":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},{"slug":"zakrutim","t":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}},{"slug":"newfi","t":{"err_truncated":false,"guest_stderr_present":false,"inner_rc":0,"ok":true,"out_truncated":false,"transport_rc":0,"transport_stderr_bytes":0,"transport_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}}]}
|
|
GRAM516R1R4_DECISION=PASS_STAFF_EMAIL_SOURCE_READONLY
|
|
GRAM516R1R4_NEXT_GATE=BUILD_MINIMAL_STAFF_EMAIL_CANARY_PATCH
|
|
HOMELAB_RESULT_CONTRACT={"version":1,"command_id":"SUPPORT-260918-GRAM-SUPPORT-STAFF-EMAIL-SOURCE-READONLY-516R1R4","status":"OK","changes_made":false,"rollback_started":false,"rollback_restored":null}
|
|
|
|
OUTPUT_END
|
|
CHAT_OUTPUT_END
|