|
71767
|
2573
|
67
|
2026-05-26T07:07:23.220446+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779243220_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Untracked files:
(use "git add <file>. Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/vin.HEIC --out ~/Downloads/Photos-3-001\(2\)/vin.png
/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/vin.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-f.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-b.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.HEIC --out ~/Downloads/Photos-3-001\(2\)/gtp.png
Warning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.heif --out ~/Downloads/Photos-3-001\(2\)/gtp.png
/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif
/Users/lukas/Downloads/Photos-3-001(2)/gtp.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop
screenpipe stopped
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull
remote: Enumerating objects: 445, done.
remote: Counting objects: 100% (265/265), done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)
Receiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.
Resolving deltas: 100% (285/285), completed with 67 local objects.
From github.com:jiminny/app
d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay
32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
* [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app
3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
+ 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)
* [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings
* [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads
71b36decb6..6818adf8e2 master -> origin/master
Updating d3ee1a203b..caba325b1a
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-
app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++---
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Services/Activity/RingCentral/Service.php | 38 +++++++--
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----
app/Services/RecallAI/RecallAIService.php | 11 +++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++
29 files changed, 1009 insertions(+), 218 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
@@ -5,14 +5,11 @@
namespace Tests\Unit\Notifications\Activities;
use Illuminate\Support\Facades\Config;
-use Jiminny\Mail\Activities\SmsReceived as SmsMailable;
use Jiminny\Models\Activity;
use Jiminny\Notifications\Activities\SmsReceived;
use Mockery;
use PHPUnit\Framework\Attributes\CoversClass;
-use ReflectionClass;
use Tests\TestCase;
-use Vinkla\Hashids\Facades\Hashids;
#[CoversClass(SmsReceived::class)]
class SmsReceivedTest extends TestCase
----------- end diff -----------
2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
@@ -198,7 +198,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -229,7 +229,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -242,7 +242,7 @@
private function createTextRelayService(): TextRelayService
{
- $service = new class extends TextRelayService {
+ $service = new class () extends TextRelayService {
public function __construct()
{
}
----------- end diff -----------
Fixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1461, done.
remote: Counting objects: 100% (841/841), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)
Receiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.
Resolving deltas: 100% (963/963), completed with 95 local objects.
From github.com:jiminny/app
6818adf8e2..1aee7aad9a master -> origin/master
8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports
+ a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)
* [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions
3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
* [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es
729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
+ 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)
* [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events
* [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522
Updating 177ea29b8d..1aee7aad9a
Fast-forward
.github/claude-reviewer/no-ticket-warning.txt | 8 +
.github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++
.github/workflows/claude.yml | 72 +++-
.github/workflows/secfix.yml | 2 +-
app/Component/AskAnything/AskAnythingPromptService.php | 10 +-
app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/BatchStatusInterface.php | 48 ---
app/Component/ES/Processor/BatchStatusManager.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-
app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++
app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-
app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -
app/Component/ES/QueuePriorityEnum.php | 43 ++
app/Component/ES/UpdateProcessManager.php | 102 ++---
app/Component/Transcription/Service/TranscriptionService.php | 24 +-
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 11 +-
app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/SearchTransformer.php | 1 +
app/Jobs/Activity/Import/DownloadTrack.php | 8 +-
app/Models/AskAnything/AskAnythingPrompt.php | 9 +
app/Notifications/Activities/SmsReceived.php | 6 +-
app/Providers/JiminnyServiceProvider.php | 6 -
app/Repositories/AskAnythingRepository.php | 8 +-
app/Services/Activity/RingCentral/Service.php | 38 +-
app/Services/Activity/Salesforce/Service.php | 5 -
app/Services/Activity/TwilioVideo/Service.php | 26 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +
app/Services/Mail/Office/EmailApiClient.php | 78 +++-
app/Services/Mail/TextRelayService.php | 33 ++
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-
app/Services/RecallAI/RecallAIService.php | 11 +
front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-
front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++
front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +
front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-
front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +
front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-
front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +
front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------
front-end/src/components/shared/AskAnything/prompts.js | 1 +
front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-
front-end/src/store/modules/playback/mutations.js | 1 +
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---
tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-
tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--
tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-
tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-
tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-
tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-
tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-
tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++
79 files changed, 4254 insertions(+), 2020 deletions(-)
create mode 100644 .github/claude-reviewer/no-ticket-warning.txt
create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs
delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php
create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php
delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js
create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html
delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap
create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php
delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php
create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity
Switched to a new branch 'JY-20963-fix-import-on-deleted-entity'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker:worker_00: stopped
worker-emails:worker-emails_00: stopped
worker-audio:worker-audio_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.30, Copyright (c) Zend Technologies
with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-c...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Untracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001(2)\nzsh: unknown file attribute: 2\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/vin.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/vin.png \n/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/vin.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png \nWarning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.heif --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png\n/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif\n /Users/lukas/Downloads/Photos-3-001(2)/gtp.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop\nscreenpipe stopped\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull\nremote: Enumerating objects: 445, done.\nremote: Counting objects: 100% (265/265), done.\nremote: Compressing objects: 100% (64/64), done.\nremote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)\nReceiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.\nResolving deltas: 100% (285/285), completed with 67 local objects.\nFrom github.com:jiminny/app\n d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay\n 32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n 4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n * [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app\n 3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n 6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n + 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)\n * [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings\n * [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads\n 71b36decb6..6818adf8e2 master -> origin/master\nUpdating d3ee1a203b..caba325b1a\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-\n app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++---\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Services/Activity/RingCentral/Service.php | 38 +++++++--\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----\n app/Services/RecallAI/RecallAIService.php | 11 +++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++\n 29 files changed, 1009 insertions(+), 218 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n@@ -5,14 +5,11 @@\n namespace Tests\\Unit\\Notifications\\Activities;\n \n use Illuminate\\Support\\Facades\\Config;\n-use Jiminny\\Mail\\Activities\\SmsReceived as SmsMailable;\n use Jiminny\\Models\\Activity;\n use Jiminny\\Notifications\\Activities\\SmsReceived;\n use Mockery;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n-use ReflectionClass;\n use Tests\\TestCase;\n-use Vinkla\\Hashids\\Facades\\Hashids;\n \n #[CoversClass(SmsReceived::class)]\n class SmsReceivedTest extends TestCase\n\n ----------- end diff -----------\n\n 2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -198,7 +198,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -229,7 +229,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -242,7 +242,7 @@\n \n private function createTextRelayService(): TextRelayService\n {\n- $service = new class extends TextRelayService {\n+ $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n\n ----------- end diff -----------\n\n\nFixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch master\nYour branch is behind 'origin/master' by 74 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Salesforce/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1461, done.\nremote: Counting objects: 100% (841/841), done.\nremote: Compressing objects: 100% (257/257), done.\nremote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)\nReceiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.\nResolving deltas: 100% (963/963), completed with 95 local objects.\nFrom github.com:jiminny/app\n 6818adf8e2..1aee7aad9a master -> origin/master\n 8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports\n + a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)\n * [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions\n 3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n * [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es\n 729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n + 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)\n * [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events\n * [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522\nUpdating 177ea29b8d..1aee7aad9a\nFast-forward\n .github/claude-reviewer/no-ticket-warning.txt | 8 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++\n .github/workflows/claude.yml | 72 +++-\n .github/workflows/secfix.yml | 2 +-\n app/Component/AskAnything/AskAnythingPromptService.php | 10 +-\n app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/BatchStatusInterface.php | 48 ---\n app/Component/ES/Processor/BatchStatusManager.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-\n app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++\n app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-\n app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -\n app/Component/ES/QueuePriorityEnum.php | 43 ++\n app/Component/ES/UpdateProcessManager.php | 102 ++---\n app/Component/Transcription/Service/TranscriptionService.php | 24 +-\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 11 +-\n app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/SearchTransformer.php | 1 +\n app/Jobs/Activity/Import/DownloadTrack.php | 8 +-\n app/Models/AskAnything/AskAnythingPrompt.php | 9 +\n app/Notifications/Activities/SmsReceived.php | 6 +-\n app/Providers/JiminnyServiceProvider.php | 6 -\n app/Repositories/AskAnythingRepository.php | 8 +-\n app/Services/Activity/RingCentral/Service.php | 38 +-\n app/Services/Activity/Salesforce/Service.php | 5 -\n app/Services/Activity/TwilioVideo/Service.php | 26 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +\n app/Services/Mail/Office/EmailApiClient.php | 78 +++-\n app/Services/Mail/TextRelayService.php | 33 ++\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-\n app/Services/RecallAI/RecallAIService.php | 11 +\n front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-\n front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++\n front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +\n front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-\n front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +\n front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-\n front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +\n front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------\n front-end/src/components/shared/AskAnything/prompts.js | 1 +\n front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-\n front-end/src/store/modules/playback/mutations.js | 1 +\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---\n tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-\n tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--\n tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-\n tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-\n tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-\n tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-\n tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++\n 79 files changed, 4254 insertions(+), 2020 deletions(-)\n create mode 100644 .github/claude-reviewer/no-ticket-warning.txt\n create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs\n delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php\n create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php\n delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js\n create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html\n delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap\n create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php\n delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php\n create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity\nSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports, no_whitespace_in_blank_line)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -10,10 +10,6 @@\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n use Illuminate\\Support\\Facades\\Log;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Jiminny\\Component\\Queue\\Constants;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n-use Jiminny\\Models\\TextRelay;\n use Jiminny\\Services\\Mail\\TextRelayService;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n@@ -346,7 +342,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n@@ -389,7 +385,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n\n ----------- end diff -----------\n\n\nFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 11, done.\nremote: Counting objects: 100% (11/11), done.\nremote: Compressing objects: 100% (4/4), done.\nremote: Total 11 (delta 7), reused 8 (delta 7), pack-reused 0 (from 0)\nUnpacking objects: 100% (11/11), 4.29 KiB | 366.00 KiB/s, done.\nFrom github.com:jiminny/app\n 65fe479f9f..96be090229 JY-208020-salesforce-zoom-integration -> origin/JY-208020-salesforce-zoom-integration\n d3dd77afee..02b98c0850 JY-20960-lemon-something-went-wrong-error -> origin/JY-20960-lemon-something-went-wrong-error\nUpdating 1aee7aad9a..23fdef5aa9\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Http/Controllers/API/ActivityController.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Services/Mail/TextRelayService.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 1aee7aad9a..23fdef5aa9\nFast-forward\n .circleci/config_continue.yml | 10 +-\n .github/claude-reviewer/no-ticket-warning.txt | 5 +-\n .github/claude-reviewer/prompts/no-requirements.txt | 25 +\n .github/claude-reviewer/prompts/with-requirements.txt | 31 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 45 +-\n .github/workflows/claude.yml | 9 +-\n .php-cs-fixer.dist.php | 1 +\n Makefile | 17 +-\n app/Component/ActionItems/Notifications/ActionItemsNotification.php | 2 +-\n app/Component/ActivityAnalytics/Service/TopicTriggerService.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityActualDate.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityRecordingStopped.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityScheduledDate.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/AiCallScoreFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/AutoScoreFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/ClosedDealsFilter.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/CoachingFeedbackAverageScore.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/CrmFieldCollection.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/CurrentStage.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/Customer.php | 12 +-\n app/Component/ActivitySearch/FilterDefinition/HasTopicTriggersFilterDefinition.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/HasTranscription.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/LoggedToCrm.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/OnlyActiveUsers.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/OrganiserUserIn.php | 10 +-\n app/Component/ActivitySearch/FilterDefinition/OrganiserUserNotIn.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/PartnerFilterDefinition.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/Security/PrivateMeetingsForCurrentUserOnly.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/Security/RestrictPublicActivitiesOnly.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/Security/RestrictTeam.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/ShowInternalExternalActivitiesFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/DateRangeFilter.php | 10 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/Exists.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/UserGroupInFilter.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/UserInFilter.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/TeamMemberUserIn.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/TranscriptionComposite.php | 2 +-\n app/Component/ActivitySearch/FilterDefinitionCollection.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/ActivityCrmContactsHandler.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/ActivityCrmFieldsHandler.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/ActivityCrmTaskEventHandler.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/OpportunityCrmFieldHandler.php | 2 +-\n app/Component/AiCallScoring/Services/GetAiCallScoringService.php | 2 +-\n app/Component/BillingManagement/MaxioClient.php | 2 +-\n app/Component/DealInsights/DealInsightsCriteriaBuilder.php | 4 +-\n app/Component/DealInsights/DealService.php | 4 +-\n app/Component/DealInsights/DealsRepository.php | 2 +-\n app/Component/DealInsights/Forecast/ForecastService.php | 2 +-\n app/Component/DealInsights/PeriodService.php | 2 +-\n app/Component/ElasticSearch/Client.php | 2 +-\n app/Component/Encoding/Service/ParseSpeechFromSilenceService.php | 2 +-\n app/Component/Nudge/Repository/NudgeRunRepository.php | 2 +-\n app/Component/ProphetAi/Services/DealDetailsContextProvider.php | 2 +-\n app/Component/Queue/Job/RateLimitAware.php | 2 +-\n app/Component/SCIM/Builders/GroupFilterQueryBuilder.php | 2 +-\n app/Component/SCIM/Builders/UsersFilterQueryBuilder.php | 2 +-\n app/Component/SCIM/Mutators/GroupPatchOperation.php | 2 +-\n app/Component/SCIM/Mutators/UserPatchOperation.php | 2 +-\n app/Component/SCIM/ScimProvisioning.php | 12 +-\n app/Component/Sidekick/SidekickSettingsRepository.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Action.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Channel.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Message.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Team.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/User.php | 2 +-\n app/Component/TeamInsights/AutomatedCallScoreRepository.php | 8 +-\n app/Component/TeamInsights/TopicTrigger/TeamInsightsTopicTriggerRepository.php | 10 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/CallTranscript.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/Records.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/Transcript.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/TranscriptSentence.php | 2 +-\n app/Component/Transcription/Formatter/TranscriptionFormatter.php | 2 +-\n app/Component/Transcription/Service/SearchService.php | 18 +-\n app/Component/Twilio/Conference/ConferenceHandler/SpecificationCallbackHandler.php | 2 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 2 +-\n app/Component/Uploader/Notifications/ActivityUploadedNotification.php | 2 +-\n app/Console/Commands/Activities/JustCall/SyncPlaybackLinkToCrmCommand.php | 4 +-\n app/Console/Commands/Analytics/NumberOfActivitiesPerActivityTypeCommand.php | 20 +-\n app/Console/Commands/Analytics/TranscriptionWordMatchCommand.php | 22 +-\n app/Console/Commands/Dev/AddRateLimitCommand.php | 2 +-\n app/Console/Commands/EngagementStats/JiminnyEngagementStatsExplainCommand.php | 2 +-\n app/Console/Commands/Mailboxes/BatchProcess.php | 2 +-\n app/Console/Commands/Reports/GenerateMarketingReport.php | 6 +-\n app/Contracts/Services/Calendar/CalendarTrait.php | 2 +-\n app/DTO/ImportCall/ZoomPhone/CallDenormalizer.php | 2 +-\n app/DTO/SCIM/AAD/Response.php | 2 +-\n app/DTO/SCIM/AAD/Response/ListResponse.php | 4 +-\n app/Events/Users/UserRolesChangedEvent.php | 2 +-\n app/Http/Controllers/API/ActivityController.php | 6 +-\n app/Http/Controllers/API/CrmController.php | 2 +-\n app/Http/Controllers/API/DealInsights/DealsController.php | 2 +-\n app/Http/Controllers/API/Page/PlaybackController.php | 2 +-\n app/Http/Controllers/API/ScimController.php | 78 +-\n app/Http/Controllers/API/TeamController.php | 2 +-\n app/Http/Controllers/API/TeamInsights/CoachingFeedbacksController.php | 2 +-\n app/Http/Controllers/API/TranscriptionController.php | 2 +-\n app/Http/Controllers/CustomerApi/CustomerApiController.php | 12 +-\n app/Http/Controllers/GeocodingController.php | 2 +-\n app/Http/Controllers/Kiosk/ProfileController.php | 2 +-\n app/Http/Controllers/Kiosk/SearchController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Transformers/ActivityTransformer.php | 6 +-\n app/Http/Transformers/CustomerApi/CustomerApiActivityTransformer.php | 2 +-\n app/Http/Transformers/CustomerApi/CustomerApiLeadTransformer.php | 2 +-\n app/Http/Transformers/MessageTransformer.php | 2 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PlaybookTreeTransformer.php | 4 +-\n app/Integrations/Releases.php | 2 +-\n app/Jobs/Activity/SyncActivity.php | 2 +-\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 2 +-\n app/Jobs/Crm/SaveActivity.php | 4 +-\n app/Jobs/Crm/SyncTeamMetadata.php | 2 +-\n app/Jobs/Mailbox/EmailTextRelay.php | 4 +-\n app/Jobs/MeetingBot/ConfigureLiveStream.php | 2 +-\n app/Listeners/Activities/Conferences/Ended.php | 2 +-\n app/Listeners/Activities/Conferences/Locked.php | 2 +-\n app/Listeners/Activities/Conferences/Started.php | 2 +-\n app/Listeners/Activities/Connections/Closed.php | 2 +-\n app/Listeners/Activities/Connections/Held.php | 2 +-\n app/Listeners/Activities/Connections/Muted.php | 2 +-\n app/Listeners/Activities/Connections/Opened.php | 2 +-\n app/Listeners/Activities/Connections/Unheld.php | 2 +-\n app/Listeners/Activities/Connections/Unmuted.php | 2 +-\n app/Listeners/Activities/SendExportEmail.php | 2 +-\n app/Listeners/Transcription/SendTranscriptionToCrmActivity.php | 2 +-\n app/Mcp/Repositories/McpElasticCallRepository.php | 4 +-\n app/Models/Activity/ActivityImport.php | 2 +-\n app/Notifications/Activities/Available.php | 2 +-\n app/Notifications/Activities/ExportViewed.php | 2 +-\n app/Notifications/Activities/MailBoxFailedToConnect.php | 2 +-\n app/Notifications/Activities/NotifyContributor.php | 2 +-\n app/Notifications/Activities/ParticipantDeclinedRecording.php | 2 +-\n app/Notifications/Activities/SmsReceived.php | 2 +-\n app/Notifications/ActivityCommented.php | 2 +-\n app/Notifications/ActivityLiveCoached.php | 4 +-\n app/Notifications/ActivityLiveCoachingNote.php | 2 +-\n app/Notifications/ActivityMentioned.php | 4 +-\n app/Notifications/ActivityNotLogged.php | 2 +-\n app/Notifications/ActivityScheduled.php | 4 +-\n app/Notifications/ActivityScored.php | 4 +-\n app/Notifications/ActivityShared.php | 4 +-\n app/Notifications/AiAutomation/AiCrmExportReady.php | 2 +-\n app/Notifications/AiAutomation/CrmFillingAutomationMisconfiguredNotification.php | 2 +-\n app/Notifications/Calendars/CalendarFailedToConnect.php | 2 +-\n app/Notifications/CoachRequested.php | 4 +-\n app/Notifications/Crm/AccountOwnerDisconnected.php | 2 +-\n app/Notifications/Crm/ActivityLogFailed.php | 2 +-\n app/Notifications/Crm/ApiDisabled.php | 2 +-\n app/Notifications/Crm/FieldUpdateFailed.php | 2 +-\n app/Notifications/Crm/ProviderChanged.php | 2 +-\n app/Notifications/Crm/QuotaExceeded.php | 2 +-\n app/Notifications/Crm/StageUpdateFailed.php | 2 +-\n app/Notifications/Crm/SyncedFieldsChanged.php | 2 +-\n app/Notifications/NewCustomerApiToken.php | 2 +-\n app/Notifications/OpportunityAlsoCommented.php | 2 +-\n app/Notifications/OpportunityCommented.php | 2 +-\n app/Notifications/OpportunityMentioned.php | 4 +-\n app/Notifications/OpportunityUpdateNotification.php | 2 +-\n app/Notifications/Playlists/ActivityAdded.php | 4 +-\n app/Notifications/Playlists/PlaylistSharedNotification.php | 4 +-\n app/Notifications/SlackBotAdded.php | 2 +-\n app/Notifications/SlackBotRemoved.php | 2 +-\n app/Notifications/Tracks/Restored.php | 2 +-\n app/Notifications/UserInvitedToTeam.php | 2 +-\n app/Notifications/UserInvitedToTeamWithEmailOnly.php | 2 +-\n app/Notifications/UserPromotedTeamOwner.php | 2 +-\n app/Providers/SsoServiceProvider.php | 2 +-\n app/Providers/ViewerGuardServiceProvider.php | 4 +-\n app/Repositories/ElasticActivityRepository.php | 118 +-\n app/Repositories/PlaylistActivityRepository.php | 2 +-\n app/Repositories/TeamInsightsRepository.php | 178 +--\n app/Repositories/TeamRepository.php | 2 +-\n app/Services/Activity/Gmail/Service.php | 6 +-\n app/Services/Activity/Office/Service.php | 2 +-\n app/Services/Activity/RingCentral/Client.php | 2 +-\n app/Services/Activity/Talkdesk/Api/DataClient.php | 2 +-\n app/Services/Activity/Vonage/Import/DataImportHandler.php | 2 +-\n app/Services/ActivityService.php | 2 +-\n app/Services/Calendar/OfficeCalendarService.php | 2 +-\n app/Services/Crm/Close/Service.php | 2 +-\n app/Services/Crm/Close/Translator/AccountMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/FieldMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/OpportunityMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/OrganisationMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/PipelineMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/ProfileMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/StageMetadataTranslator.php | 2 +-\n app/Services/Crm/Copper/Service.php | 2 +-\n app/Services/Crm/Hubspot/ServiceTraits/WriteCrmTrait.php | 2 +-\n app/Services/Crm/Salesforce/Client.php | 2 +-\n app/Services/Crm/Salesforce/Service.php | 2 +-\n app/Services/Mail/TextRelayService.php | 2 +-\n app/Services/MeetingGenerator/AbstractMeetingProvider.php | 2 +-\n app/Services/MeetingGenerator/TeamsMeetingProvider.php | 2 +-\n app/Services/Security/Authy.php | 6 +-\n app/Traits/RequiresUUID.php | 4 +-\n app/VO/Repository/TranscriptionKeywordParser.php | 6 +-\n composer.json | 8 +-\n composer.lock | 4638 ++++++++++++++++++++++--------------------------------------------\n config/database.php | 12 +-\n tests/Feature/Component/Notification/ActivityFollowUpSlackMessageBuilderTest.php | 2 +-\n tests/Feature/Services/Crm/Close/ClientTest.php | 6 +-\n tests/Unit/Actions/UpdateUserRolesActionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ActivityScheduledDateTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/AiCallScoreFilterTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/AutoScoreFilterTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ClosedDealsFilterTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/CoachingFeedbackAverageScoreTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/CrmFieldCollectionTest.php | 24 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ExternalIdTest.php | 8 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/HasTopicTriggersFilterDefinitionTest.php | 4 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/HasTranscriptionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/LanguageFilterDefinitionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/PartnerFilterDefinitionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/Security/PrivateMeetingsForCurrentUserOnlyTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ShowInternalExternalActivitiesFilterTest.php | 4 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/TeamInsights/DateRangeFilterTest.php | 16 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/TeamInsights/UserInFilterTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/TeamMemberUserInTest.php | 4 +-\n tests/Unit/Component/ActivitySearch/FilterDefinitionCollectionTest.php | 16 +-\n tests/Unit/Component/AiAutomation/SaveCrmTemplateRunsServiceTest.php | 10 +-\n tests/Unit/Component/DateTime/DateTimeZoneManagerTest.php | 2 +-\n tests/Unit/Component/DealInsights/Forecast/ForecastServiceTest.php | 12 +-\n tests/Unit/Component/FFMpeg/Services/SwitchAudioChannelsTest.php | 3 +-\n tests/Unit/Component/Nudge/Notification/NudgeEmailNotificationTest.php | 4 +-\n tests/Unit/Component/Nudge/Notification/NudgeSlackNotificationTest.php | 4 +-\n tests/Unit/Component/Playlist/Http/Request/MovePlaylistActivityRequestTest.php | 2 +-\n tests/Unit/Component/Sidekick/SidekickServiceTest.php | 2 +-\n tests/Unit/Component/TeamInsights/TopicsInDeals/EsQueries/TopicsInDealsAggregationTest.php | 2 +-\n tests/Unit/Component/TeamInsights/TopicsInDeals/TopicsInDealsComparisonRepositoryTest.php | 8 +-\n tests/Unit/Component/TeamInsights/TopicsInDeals/TopicsInDealsRepositoryTest.php | 2 +-\n tests/Unit/Contracts/Services/Calendar/CalendarTraitTest.php | 24 +-\n tests/Unit/DTO/ImportCall/JustCall/CallDenormalizerTest.php | 12 +-\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 10 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 4 +-\n tests/Unit/Jobs/Activity/Import/ImportCallTest.php | 4 +-\n tests/Unit/Jobs/Activity/Import/MatchCrmDataTest.php | 2 +-\n tests/Unit/Jobs/Activity/SyncActivityTest.php | 8 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 2 +-\n tests/Unit/Jobs/User/SyncToIntercomTest.php | 2 +-\n tests/Unit/Listeners/Import/ActivityImportSubscriberTest.php | 8 +-\n tests/Unit/Listeners/Users/SetupMailSyncTest.php | 2 +-\n tests/Unit/Notifications/AiAutomation/AiCrmExportReadyTest.php | 6 +-\n tests/Unit/Notifications/AiAutomation/CrmFillingAutomationMisconfiguredNotificationTest.php | 6 +-\n tests/Unit/Notifications/OpportunityUpdateNotificationTest.php | 4 +-\n tests/Unit/Notifications/UserInvitedToTeamWithEmailOnlyTest.php | 2 +-\n tests/Unit/Services/Activity/Bloobirds/CallDenormalizerTest.php | 4 +-\n tests/Unit/Services/Activity/CloudCall/ClientTest.php | 2 +-\n tests/Unit/Services/Activity/CloudCall/ServiceTest.php | 2 +-\n tests/Unit/Services/Activity/FiveNine/DataClientTest.php | 2 +-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 2 +-\n tests/Unit/Services/Activity/Vonage/Import/CallDenormalizerTest.php | 4 +-\n tests/Unit/Services/Activity/Vonage/Import/DataImportHandlerTest.php | 2 +-\n tests/Unit/Services/Calendar/Command/ValidateGoogleEventAttendeePresenceTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Processor/MetadataProcessorTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Processor/OpportunityProcessorTest.php | 2 +-\n tests/Unit/Services/Crm/Close/ServiceTest.php | 4 +-\n tests/Unit/Services/Crm/Close/Translator/AccountMetadataTranslatorTest.php | 10 +-\n tests/Unit/Services/Crm/Close/Translator/FieldMetadataTranslatorTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Translator/OpportunityMetadataTranslatorTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Translator/OrganisationMetadataTranslatorTest.php | 6 +-\n tests/Unit/Services/Crm/Close/Translator/PipelineMetadataTranslatorTest.php | 16 +-\n tests/Unit/Services/Crm/Close/Translator/ProfileMetadataTranslatorTest.php | 2 +-\n tests/Unit/Services/Crm/CrmObjectsResolverTest.php | 4 +-\n tests/Unit/Traits/TestPrivateMethod.php | 2 +-\n 268 files changed, 2321 insertions(+), 3819 deletions(-)\n create mode 100644 .github/claude-reviewer/prompts/no-requirements.txt\n create mode 100644 .github/claude-reviewer/prompts/with-requirements.txt\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20891-","depth":4,"on_screen":true,"value":"Untracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001(2)\nzsh: unknown file attribute: 2\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/vin.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/vin.png \n/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/vin.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png \nWarning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.heif --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png\n/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif\n /Users/lukas/Downloads/Photos-3-001(2)/gtp.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop\nscreenpipe stopped\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull\nremote: Enumerating objects: 445, done.\nremote: Counting objects: 100% (265/265), done.\nremote: Compressing objects: 100% (64/64), done.\nremote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)\nReceiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.\nResolving deltas: 100% (285/285), completed with 67 local objects.\nFrom github.com:jiminny/app\n d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay\n 32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n 4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n * [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app\n 3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n 6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n + 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)\n * [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings\n * [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads\n 71b36decb6..6818adf8e2 master -> origin/master\nUpdating d3ee1a203b..caba325b1a\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-\n app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++---\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Services/Activity/RingCentral/Service.php | 38 +++++++--\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----\n app/Services/RecallAI/RecallAIService.php | 11 +++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++\n 29 files changed, 1009 insertions(+), 218 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n@@ -5,14 +5,11 @@\n namespace Tests\\Unit\\Notifications\\Activities;\n \n use Illuminate\\Support\\Facades\\Config;\n-use Jiminny\\Mail\\Activities\\SmsReceived as SmsMailable;\n use Jiminny\\Models\\Activity;\n use Jiminny\\Notifications\\Activities\\SmsReceived;\n use Mockery;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n-use ReflectionClass;\n use Tests\\TestCase;\n-use Vinkla\\Hashids\\Facades\\Hashids;\n \n #[CoversClass(SmsReceived::class)]\n class SmsReceivedTest extends TestCase\n\n ----------- end diff -----------\n\n 2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -198,7 +198,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -229,7 +229,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -242,7 +242,7 @@\n \n private function createTextRelayService(): TextRelayService\n {\n- $service = new class extends TextRelayService {\n+ $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n\n ----------- end diff -----------\n\n\nFixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch master\nYour branch is behind 'origin/master' by 74 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Salesforce/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1461, done.\nremote: Counting objects: 100% (841/841), done.\nremote: Compressing objects: 100% (257/257), done.\nremote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)\nReceiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.\nResolving deltas: 100% (963/963), completed with 95 local objects.\nFrom github.com:jiminny/app\n 6818adf8e2..1aee7aad9a master -> origin/master\n 8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports\n + a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)\n * [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions\n 3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n * [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es\n 729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n + 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)\n * [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events\n * [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522\nUpdating 177ea29b8d..1aee7aad9a\nFast-forward\n .github/claude-reviewer/no-ticket-warning.txt | 8 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++\n .github/workflows/claude.yml | 72 +++-\n .github/workflows/secfix.yml | 2 +-\n app/Component/AskAnything/AskAnythingPromptService.php | 10 +-\n app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/BatchStatusInterface.php | 48 ---\n app/Component/ES/Processor/BatchStatusManager.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-\n app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++\n app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-\n app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -\n app/Component/ES/QueuePriorityEnum.php | 43 ++\n app/Component/ES/UpdateProcessManager.php | 102 ++---\n app/Component/Transcription/Service/TranscriptionService.php | 24 +-\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 11 +-\n app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/SearchTransformer.php | 1 +\n app/Jobs/Activity/Import/DownloadTrack.php | 8 +-\n app/Models/AskAnything/AskAnythingPrompt.php | 9 +\n app/Notifications/Activities/SmsReceived.php | 6 +-\n app/Providers/JiminnyServiceProvider.php | 6 -\n app/Repositories/AskAnythingRepository.php | 8 +-\n app/Services/Activity/RingCentral/Service.php | 38 +-\n app/Services/Activity/Salesforce/Service.php | 5 -\n app/Services/Activity/TwilioVideo/Service.php | 26 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +\n app/Services/Mail/Office/EmailApiClient.php | 78 +++-\n app/Services/Mail/TextRelayService.php | 33 ++\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-\n app/Services/RecallAI/RecallAIService.php | 11 +\n front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-\n front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++\n front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +\n front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-\n front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +\n front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-\n front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +\n front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------\n front-end/src/components/shared/AskAnything/prompts.js | 1 +\n front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-\n front-end/src/store/modules/playback/mutations.js | 1 +\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---\n tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-\n tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--\n tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-\n tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-\n tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-\n tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-\n tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++\n 79 files changed, 4254 insertions(+), 2020 deletions(-)\n create mode 100644 .github/claude-reviewer/no-ticket-warning.txt\n create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs\n delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php\n create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php\n delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js\n create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html\n delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap\n create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php\n delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php\n create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity\nSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports, no_whitespace_in_blank_line)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -10,10 +10,6 @@\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n use Illuminate\\Support\\Facades\\Log;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Jiminny\\Component\\Queue\\Constants;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n-use Jiminny\\Models\\TextRelay;\n use Jiminny\\Services\\Mail\\TextRelayService;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n@@ -346,7 +342,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n@@ -389,7 +385,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n\n ----------- end diff -----------\n\n\nFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 11, done.\nremote: Counting objects: 100% (11/11), done.\nremote: Compressing objects: 100% (4/4), done.\nremote: Total 11 (delta 7), reused 8 (delta 7), pack-reused 0 (from 0)\nUnpacking objects: 100% (11/11), 4.29 KiB | 366.00 KiB/s, done.\nFrom github.com:jiminny/app\n 65fe479f9f..96be090229 JY-208020-salesforce-zoom-integration -> origin/JY-208020-salesforce-zoom-integration\n d3dd77afee..02b98c0850 JY-20960-lemon-something-went-wrong-error -> origin/JY-20960-lemon-something-went-wrong-error\nUpdating 1aee7aad9a..23fdef5aa9\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Http/Controllers/API/ActivityController.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Services/Mail/TextRelayService.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 1aee7aad9a..23fdef5aa9\nFast-forward\n .circleci/config_continue.yml | 10 +-\n .github/claude-reviewer/no-ticket-warning.txt | 5 +-\n .github/claude-reviewer/prompts/no-requirements.txt | 25 +\n .github/claude-reviewer/prompts/with-requirements.txt | 31 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 45 +-\n .github/workflows/claude.yml | 9 +-\n .php-cs-fixer.dist.php | 1 +\n Makefile | 17 +-\n app/Component/ActionItems/Notifications/ActionItemsNotification.php | 2 +-\n app/Component/ActivityAnalytics/Service/TopicTriggerService.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityActualDate.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityRecordingStopped.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityScheduledDate.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/AiCallScoreFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/AutoScoreFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/ClosedDealsFilter.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/CoachingFeedbackAverageScore.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/CrmFieldCollection.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/CurrentStage.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/Customer.php | 12 +-\n app/Component/ActivitySearch/FilterDefinition/HasTopicTriggersFilterDefinition.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/HasTranscription.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/LoggedToCrm.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/OnlyActiveUsers.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/OrganiserUserIn.php | 10 +-\n app/Component/ActivitySearch/FilterDefinition/OrganiserUserNotIn.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/PartnerFilterDefinition.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/Security/PrivateMeetingsForCurrentUserOnly.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/Security/RestrictPublicActivitiesOnly.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/Security/RestrictTeam.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/ShowInternalExternalActivitiesFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/DateRangeFilter.php | 10 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/Exists.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/UserGroupInFilter.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/UserInFilter.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/TeamMemberUserIn.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/TranscriptionComposite.php | 2 +-\n app/Component/ActivitySearch/FilterDefinitionCollection.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/ActivityCrmContactsHandler.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/ActivityCrmFieldsHandler.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/ActivityCrmTaskEventHandler.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/OpportunityCrmFieldHandler.php | 2 +-\n app/Component/AiCallScoring/Services/GetAiCallScoringService.php | 2 +-\n app/Component/BillingManagement/MaxioClient.php | 2 +-\n app/Component/DealInsights/DealInsightsCriteriaBuilder.php | 4 +-\n app/Component/DealInsights/DealService.php | 4 +-\n app/Component/DealInsights/DealsRepository.php | 2 +-\n app/Component/DealInsights/Forecast/ForecastService.php | 2 +-\n app/Component/DealInsights/PeriodService.php | 2 +-\n app/Component/ElasticSearch/Client.php | 2 +-\n app/Component/Encoding/Service/ParseSpeechFromSilenceService.php | 2 +-\n app/Component/Nudge/Repository/NudgeRunRepository.php | 2 +-\n app/Component/ProphetAi/Services/DealDetailsContextProvider.php | 2 +-\n app/Component/Queue/Job/RateLimitAware.php | 2 +-\n app/Component/SCIM/Builders/GroupFilterQueryBuilder.php | 2 +-\n app/Component/SCIM/Builders/UsersFilterQueryBuilder.php | 2 +-\n app/Component/SCIM/Mutators/GroupPatchOperation.php | 2 +-\n app/Component/SCIM/Mutators/UserPatchOperation.php | 2 +-\n app/Component/SCIM/ScimProvisioning.php | 12 +-\n app/Component/Sidekick/SidekickSettingsRepository.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Action.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Channel.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Message.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Team.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/User.php | 2 +-\n app/Component/TeamInsights/AutomatedCallScoreRepository.php | 8 +-\n app/Component/TeamInsights/TopicTrigger/TeamInsightsTopicTriggerRepository.php | 10 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/CallTranscript.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/Records.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/Transcript.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/TranscriptSentence.php | 2 +-\n app/Component/Transcription/Formatter/TranscriptionFormatter.php | 2 +-\n app/Component/Transcription/Service/SearchService.php | 18 +-\n app/Component/Twilio/Conference/ConferenceHandler/SpecificationCallbackHandler.php | 2 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 2 +-\n app/Component/Uploader/Notifications/ActivityUploadedNotification.php | 2 +-\n app/Console/Commands/Activities/JustCall/SyncPlaybackLinkToCrmCommand.php | 4 +-\n app/Console/Commands/Analytics/NumberOfActivitiesPerActivityTypeCommand.php | 20 +-\n app/Console/Commands/Analytics/TranscriptionWordMatchCommand.php | 22 +-\n app/Console/Commands/Dev/AddRateLimitCommand.php | 2 +-\n app/Console/Commands/EngagementStats/JiminnyEngagementStatsExplainCommand.php | 2 +-\n app/Console/Commands/Mailboxes/BatchProcess.php | 2 +-\n app/Console/Commands/Reports/GenerateMarketingReport.php | 6 +-\n app/Contracts/Services/Calendar/CalendarTrait.php | 2 +-\n app/DTO/ImportCall/ZoomPhone/CallDenormalizer.php | 2 +-\n app/DTO/SCIM/AAD/Response.php | 2 +-\n app/DTO/SCIM/AAD/Response/ListResponse.php | 4 +-\n app/Events/Users/UserRolesChangedEvent.php | 2 +-\n app/Http/Controllers/API/ActivityController.php | 6 +-\n app/Http/Controllers/API/CrmController.php | 2 +-\n app/Http/Controllers/API/DealInsights/DealsController.php | 2 +-\n app/Http/Controllers/API/Page/PlaybackController.php | 2 +-\n app/Http/Controllers/API/ScimController.php | 78 +-\n app/Http/Controllers/API/TeamController.php | 2 +-\n app/Http/Controllers/API/TeamInsights/CoachingFeedbacksController.php | 2 +-\n app/Http/Controllers/API/TranscriptionController.php | 2 +-\n app/Http/Controllers/CustomerApi/CustomerApiController.php | 12 +-\n app/Http/Controllers/GeocodingController.php | 2 +-\n app/Http/Controllers/Kiosk/ProfileController.php | 2 +-\n app/Http/Controllers/Kiosk/SearchController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Transformers/ActivityTransformer.php | 6 +-\n app/Http/Transformers/CustomerApi/CustomerApiActivityTransformer.php | 2 +-\n app/Http/Transformers/CustomerApi/CustomerApiLeadTransformer.php | 2 +-\n app/Http/Transformers/MessageTransformer.php | 2 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PlaybookTreeTransformer.php | 4 +-\n app/Integrations/Releases.php | 2 +-\n app/Jobs/Activity/SyncActivity.php | 2 +-\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 2 +-\n app/Jobs/Crm/SaveActivity.php | 4 +-\n app/Jobs/Crm/SyncTeamMetadata.php | 2 +-\n app/Jobs/Mailbox/EmailTextRelay.php | 4 +-\n app/Jobs/MeetingBot/ConfigureLiveStream.php | 2 +-\n app/Listeners/Activities/Conferences/Ended.php | 2 +-\n app/Listeners/Activities/Conferences/Locked.php | 2 +-\n app/Listeners/Activities/Conferences/Started.php | 2 +-\n app/Listeners/Activities/Connections/Closed.php | 2 +-\n app/Listeners/Activities/Connections/Held.php | 2 +-\n app/Listeners/Activities/Connections/Muted.php | 2 +-\n app/Listeners/Activities/Connections/Opened.php | 2 +-\n app/Listeners/Activities/Connections/Unheld.php | 2 +-\n app/Listeners/Activities/Connections/Unmuted.php | 2 +-\n app/Listeners/Activities/SendExportEmail.php | 2 +-\n app/Listeners/Transcription/SendTranscriptionToCrmActivity.php | 2 +-\n app/Mcp/Repositories/McpElasticCallRepository.php | 4 +-\n app/Models/Activity/ActivityImport.php | 2 +-\n app/Notifications/Activities/Available.php | 2 +-\n app/Notifications/Activities/ExportViewed.php | 2 +-\n app/Notifications/Activities/MailBoxFailedToConnect.php | 2 +-\n app/Notifications/Activities/NotifyContributor.php | 2 +-\n app/Notifications/Activities/ParticipantDeclinedRecording.php | 2 +-\n app/Notifications/Activities/SmsReceived.php | 2 +-\n app/Notifications/ActivityCommented.php | 2 +-\n app/Notifications/ActivityLiveCoached.php | 4 +-\n app/Notifications/ActivityLiveCoachingNote.php | 2 +-\n app/Notifications/ActivityMentioned.php | 4 +-\n app/Notifications/ActivityNotLogged.php | 2 +-\n app/Notifications/ActivityScheduled.php | 4 +-\n app/Notifications/ActivityScored.php | 4 +-\n app/Notifications/ActivityShared.php | 4 +-\n app/Notifications/AiAutomation/AiCrmExportReady.php | 2 +-\n app/Notifications/AiAutomation/CrmFillingAutomationMisconfiguredNotification.php | 2 +-\n app/Notifications/Calendars/CalendarFailedToConnect.php | 2 +-\n app/Notifications/CoachRequested.php | 4 +-\n app/Notifications/Crm/AccountOwnerDisconnected.php | 2 +-\n app/Notifications/Crm/ActivityLogFailed.php | 2 +-\n app/Notifications/Crm/ApiDisabled.php | 2 +-\n app/Notifications/Crm/FieldUpdateFailed.php | 2 +-\n app/Notifications/Crm/ProviderChanged.php | 2 +-\n app/Notifications/Crm/QuotaExceeded.php | 2 +-\n app/Notifications/Crm/StageUpdateFailed.php | 2 +-\n app/Notifications/Crm/SyncedFieldsChanged.php | 2 +-\n app/Notifications/NewCustomerApiToken.php | 2 +-\n app/Notifications/OpportunityAlsoCommented.php | 2 +-\n app/Notifications/OpportunityCommented.php | 2 +-\n app/Notifications/OpportunityMentioned.php | 4 +-\n app/Notifications/OpportunityUpdateNotification.php | 2 +-\n app/Notifications/Playlists/ActivityAdded.php | 4 +-\n app/Notifications/Playlists/PlaylistSharedNotification.php | 4 +-\n app/Notifications/SlackBotAdded.php | 2 +-\n app/Notifications/SlackBotRemoved.php | 2 +-\n app/Notifications/Tracks/Restored.php | 2 +-\n app/Notifications/UserInvitedToTeam.php | 2 +-\n app/Notifications/UserInvitedToTeamWithEmailOnly.php | 2 +-\n app/Notifications/UserPromotedTeamOwner.php | 2 +-\n app/Providers/SsoServiceProvider.php | 2 +-\n app/Providers/ViewerGuardServiceProvider.php | 4 +-\n app/Repositories/ElasticActivityRepository.php | 118 +-\n app/Repositories/PlaylistActivityRepository.php | 2 +-\n app/Repositories/TeamInsightsRepository.php | 178 +--\n app/Repositories/TeamRepository.php | 2 +-\n app/Services/Activity/Gmail/Service.php | 6 +-\n app/Services/Activity/Office/Service.php | 2 +-\n app/Services/Activity/RingCentral/Client.php | 2 +-\n app/Services/Activity/Talkdesk/Api/DataClient.php | 2 +-\n app/Services/Activity/Vonage/Import/DataImportHandler.php | 2 +-\n app/Services/ActivityService.php | 2 +-\n app/Services/Calendar/OfficeCalendarService.php | 2 +-\n app/Services/Crm/Close/Service.php | 2 +-\n app/Services/Crm/Close/Translator/AccountMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/FieldMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/OpportunityMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/OrganisationMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/PipelineMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/ProfileMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/StageMetadataTranslator.php | 2 +-\n app/Services/Crm/Copper/Service.php | 2 +-\n app/Services/Crm/Hubspot/ServiceTraits/WriteCrmTrait.php | 2 +-\n app/Services/Crm/Salesforce/Client.php | 2 +-\n app/Services/Crm/Salesforce/Service.php | 2 +-\n app/Services/Mail/TextRelayService.php | 2 +-\n app/Services/MeetingGenerator/AbstractMeetingProvider.php | 2 +-\n app/Services/MeetingGenerator/TeamsMeetingProvider.php | 2 +-\n app/Services/Security/Authy.php | 6 +-\n app/Traits/RequiresUUID.php | 4 +-\n app/VO/Repository/TranscriptionKeywordParser.php | 6 +-\n composer.json | 8 +-\n composer.lock | 4638 ++++++++++++++++++++++--------------------------------------------\n config/database.php | 12 +-\n tests/Feature/Component/Notification/ActivityFollowUpSlackMessageBuilderTest.php | 2 +-\n tests/Feature/Services/Crm/Close/ClientTest.php | 6 +-\n tests/Unit/Actions/UpdateUserRolesActionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ActivityScheduledDateTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/AiCallScoreFilterTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/AutoScoreFilterTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ClosedDealsFilterTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/CoachingFeedbackAverageScoreTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/CrmFieldCollectionTest.php | 24 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ExternalIdTest.php | 8 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/HasTopicTriggersFilterDefinitionTest.php | 4 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/HasTranscriptionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/LanguageFilterDefinitionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/PartnerFilterDefinitionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/Security/PrivateMeetingsForCurrentUserOnlyTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ShowInternalExternalActivitiesFilterTest.php | 4 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/TeamInsights/DateRangeFilterTest.php | 16 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/TeamInsights/UserInFilterTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/TeamMemberUserInTest.php | 4 +-\n tests/Unit/Component/ActivitySearch/FilterDefinitionCollectionTest.php | 16 +-\n tests/Unit/Component/AiAutomation/SaveCrmTemplateRunsServiceTest.php | 10 +-\n tests/Unit/Component/DateTime/DateTimeZoneManagerTest.php | 2 +-\n tests/Unit/Component/DealInsights/Forecast/ForecastServiceTest.php | 12 +-\n tests/Unit/Component/FFMpeg/Services/SwitchAudioChannelsTest.php | 3 +-\n tests/Unit/Component/Nudge/Notification/NudgeEmailNotificationTest.php | 4 +-\n tests/Unit/Component/Nudge/Notification/NudgeSlackNotificationTest.php | 4 +-\n tests/Unit/Component/Playlist/Http/Request/MovePlaylistActivityRequestTest.php | 2 +-\n tests/Unit/Component/Sidekick/SidekickServiceTest.php | 2 +-\n tests/Unit/Component/TeamInsights/TopicsInDeals/EsQueries/TopicsInDealsAggregationTest.php | 2 +-\n tests/Unit/Component/TeamInsights/TopicsInDeals/TopicsInDealsComparisonRepositoryTest.php | 8 +-\n tests/Unit/Component/TeamInsights/TopicsInDeals/TopicsInDealsRepositoryTest.php | 2 +-\n tests/Unit/Contracts/Services/Calendar/CalendarTraitTest.php | 24 +-\n tests/Unit/DTO/ImportCall/JustCall/CallDenormalizerTest.php | 12 +-\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 10 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 4 +-\n tests/Unit/Jobs/Activity/Import/ImportCallTest.php | 4 +-\n tests/Unit/Jobs/Activity/Import/MatchCrmDataTest.php | 2 +-\n tests/Unit/Jobs/Activity/SyncActivityTest.php | 8 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 2 +-\n tests/Unit/Jobs/User/SyncToIntercomTest.php | 2 +-\n tests/Unit/Listeners/Import/ActivityImportSubscriberTest.php | 8 +-\n tests/Unit/Listeners/Users/SetupMailSyncTest.php | 2 +-\n tests/Unit/Notifications/AiAutomation/AiCrmExportReadyTest.php | 6 +-\n tests/Unit/Notifications/AiAutomation/CrmFillingAutomationMisconfiguredNotificationTest.php | 6 +-\n tests/Unit/Notifications/OpportunityUpdateNotificationTest.php | 4 +-\n tests/Unit/Notifications/UserInvitedToTeamWithEmailOnlyTest.php | 2 +-\n tests/Unit/Services/Activity/Bloobirds/CallDenormalizerTest.php | 4 +-\n tests/Unit/Services/Activity/CloudCall/ClientTest.php | 2 +-\n tests/Unit/Services/Activity/CloudCall/ServiceTest.php | 2 +-\n tests/Unit/Services/Activity/FiveNine/DataClientTest.php | 2 +-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 2 +-\n tests/Unit/Services/Activity/Vonage/Import/CallDenormalizerTest.php | 4 +-\n tests/Unit/Services/Activity/Vonage/Import/DataImportHandlerTest.php | 2 +-\n tests/Unit/Services/Calendar/Command/ValidateGoogleEventAttendeePresenceTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Processor/MetadataProcessorTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Processor/OpportunityProcessorTest.php | 2 +-\n tests/Unit/Services/Crm/Close/ServiceTest.php | 4 +-\n tests/Unit/Services/Crm/Close/Translator/AccountMetadataTranslatorTest.php | 10 +-\n tests/Unit/Services/Crm/Close/Translator/FieldMetadataTranslatorTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Translator/OpportunityMetadataTranslatorTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Translator/OrganisationMetadataTranslatorTest.php | 6 +-\n tests/Unit/Services/Crm/Close/Translator/PipelineMetadataTranslatorTest.php | 16 +-\n tests/Unit/Services/Crm/Close/Translator/ProfileMetadataTranslatorTest.php | 2 +-\n tests/Unit/Services/Crm/CrmObjectsResolverTest.php | 4 +-\n tests/Unit/Traits/TestPrivateMethod.php | 2 +-\n 268 files changed, 2321 insertions(+), 3819 deletions(-)\n create mode 100644 .github/claude-reviewer/prompts/no-requirements.txt\n create mode 100644 .github/claude-reviewer/prompts/with-requirements.txt\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20891-","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.26894948,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27094415,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.36336437,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.36535904,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.45777926,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.45977393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5521942,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.55418885,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.64660907,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.64860374,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.49667552,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
1738179714311874313
|
-824448891317507332
|
visual_change
|
accessibility
|
NULL
|
Untracked files:
(use "git add <file>. Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/vin.HEIC --out ~/Downloads/Photos-3-001\(2\)/vin.png
/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/vin.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-f.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-b.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.HEIC --out ~/Downloads/Photos-3-001\(2\)/gtp.png
Warning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.heif --out ~/Downloads/Photos-3-001\(2\)/gtp.png
/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif
/Users/lukas/Downloads/Photos-3-001(2)/gtp.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop
screenpipe stopped
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull
remote: Enumerating objects: 445, done.
remote: Counting objects: 100% (265/265), done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)
Receiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.
Resolving deltas: 100% (285/285), completed with 67 local objects.
From github.com:jiminny/app
d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay
32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
* [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app
3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
+ 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)
* [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings
* [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads
71b36decb6..6818adf8e2 master -> origin/master
Updating d3ee1a203b..caba325b1a
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-
app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++---
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Services/Activity/RingCentral/Service.php | 38 +++++++--
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----
app/Services/RecallAI/RecallAIService.php | 11 +++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++
29 files changed, 1009 insertions(+), 218 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
@@ -5,14 +5,11 @@
namespace Tests\Unit\Notifications\Activities;
use Illuminate\Support\Facades\Config;
-use Jiminny\Mail\Activities\SmsReceived as SmsMailable;
use Jiminny\Models\Activity;
use Jiminny\Notifications\Activities\SmsReceived;
use Mockery;
use PHPUnit\Framework\Attributes\CoversClass;
-use ReflectionClass;
use Tests\TestCase;
-use Vinkla\Hashids\Facades\Hashids;
#[CoversClass(SmsReceived::class)]
class SmsReceivedTest extends TestCase
----------- end diff -----------
2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
@@ -198,7 +198,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -229,7 +229,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -242,7 +242,7 @@
private function createTextRelayService(): TextRelayService
{
- $service = new class extends TextRelayService {
+ $service = new class () extends TextRelayService {
public function __construct()
{
}
----------- end diff -----------
Fixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1461, done.
remote: Counting objects: 100% (841/841), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)
Receiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.
Resolving deltas: 100% (963/963), completed with 95 local objects.
From github.com:jiminny/app
6818adf8e2..1aee7aad9a master -> origin/master
8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports
+ a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)
* [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions
3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
* [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es
729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
+ 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)
* [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events
* [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522
Updating 177ea29b8d..1aee7aad9a
Fast-forward
.github/claude-reviewer/no-ticket-warning.txt | 8 +
.github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++
.github/workflows/claude.yml | 72 +++-
.github/workflows/secfix.yml | 2 +-
app/Component/AskAnything/AskAnythingPromptService.php | 10 +-
app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/BatchStatusInterface.php | 48 ---
app/Component/ES/Processor/BatchStatusManager.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-
app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++
app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-
app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -
app/Component/ES/QueuePriorityEnum.php | 43 ++
app/Component/ES/UpdateProcessManager.php | 102 ++---
app/Component/Transcription/Service/TranscriptionService.php | 24 +-
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 11 +-
app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/SearchTransformer.php | 1 +
app/Jobs/Activity/Import/DownloadTrack.php | 8 +-
app/Models/AskAnything/AskAnythingPrompt.php | 9 +
app/Notifications/Activities/SmsReceived.php | 6 +-
app/Providers/JiminnyServiceProvider.php | 6 -
app/Repositories/AskAnythingRepository.php | 8 +-
app/Services/Activity/RingCentral/Service.php | 38 +-
app/Services/Activity/Salesforce/Service.php | 5 -
app/Services/Activity/TwilioVideo/Service.php | 26 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +
app/Services/Mail/Office/EmailApiClient.php | 78 +++-
app/Services/Mail/TextRelayService.php | 33 ++
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-
app/Services/RecallAI/RecallAIService.php | 11 +
front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-
front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++
front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +
front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-
front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +
front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-
front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +
front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------
front-end/src/components/shared/AskAnything/prompts.js | 1 +
front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-
front-end/src/store/modules/playback/mutations.js | 1 +
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---
tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-
tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--
tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-
tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-
tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-
tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-
tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-
tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++
79 files changed, 4254 insertions(+), 2020 deletions(-)
create mode 100644 .github/claude-reviewer/no-ticket-warning.txt
create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs
delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php
create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php
delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js
create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html
delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap
create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php
delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php
create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity
Switched to a new branch 'JY-20963-fix-import-on-deleted-entity'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker:worker_00: stopped
worker-emails:worker-emails_00: stopped
worker-audio:worker-audio_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.30, Copyright (c) Zend Technologies
with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-c...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71766
|
2572
|
28
|
2026-05-26T07:07:13.537611+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779233537_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Untracked files:
(use "git add <file>. Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/vin.HEIC --out ~/Downloads/Photos-3-001\(2\)/vin.png
/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/vin.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-f.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-b.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.HEIC --out ~/Downloads/Photos-3-001\(2\)/gtp.png
Warning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.heif --out ~/Downloads/Photos-3-001\(2\)/gtp.png
/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif
/Users/lukas/Downloads/Photos-3-001(2)/gtp.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop
screenpipe stopped
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull
remote: Enumerating objects: 445, done.
remote: Counting objects: 100% (265/265), done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)
Receiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.
Resolving deltas: 100% (285/285), completed with 67 local objects.
From github.com:jiminny/app
d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay
32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
* [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app
3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
+ 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)
* [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings
* [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads
71b36decb6..6818adf8e2 master -> origin/master
Updating d3ee1a203b..caba325b1a
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-
app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++---
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Services/Activity/RingCentral/Service.php | 38 +++++++--
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----
app/Services/RecallAI/RecallAIService.php | 11 +++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++
29 files changed, 1009 insertions(+), 218 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
@@ -5,14 +5,11 @@
namespace Tests\Unit\Notifications\Activities;
use Illuminate\Support\Facades\Config;
-use Jiminny\Mail\Activities\SmsReceived as SmsMailable;
use Jiminny\Models\Activity;
use Jiminny\Notifications\Activities\SmsReceived;
use Mockery;
use PHPUnit\Framework\Attributes\CoversClass;
-use ReflectionClass;
use Tests\TestCase;
-use Vinkla\Hashids\Facades\Hashids;
#[CoversClass(SmsReceived::class)]
class SmsReceivedTest extends TestCase
----------- end diff -----------
2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
@@ -198,7 +198,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -229,7 +229,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -242,7 +242,7 @@
private function createTextRelayService(): TextRelayService
{
- $service = new class extends TextRelayService {
+ $service = new class () extends TextRelayService {
public function __construct()
{
}
----------- end diff -----------
Fixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1461, done.
remote: Counting objects: 100% (841/841), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)
Receiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.
Resolving deltas: 100% (963/963), completed with 95 local objects.
From github.com:jiminny/app
6818adf8e2..1aee7aad9a master -> origin/master
8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports
+ a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)
* [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions
3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
* [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es
729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
+ 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)
* [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events
* [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522
Updating 177ea29b8d..1aee7aad9a
Fast-forward
.github/claude-reviewer/no-ticket-warning.txt | 8 +
.github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++
.github/workflows/claude.yml | 72 +++-
.github/workflows/secfix.yml | 2 +-
app/Component/AskAnything/AskAnythingPromptService.php | 10 +-
app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/BatchStatusInterface.php | 48 ---
app/Component/ES/Processor/BatchStatusManager.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-
app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++
app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-
app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -
app/Component/ES/QueuePriorityEnum.php | 43 ++
app/Component/ES/UpdateProcessManager.php | 102 ++---
app/Component/Transcription/Service/TranscriptionService.php | 24 +-
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 11 +-
app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/SearchTransformer.php | 1 +
app/Jobs/Activity/Import/DownloadTrack.php | 8 +-
app/Models/AskAnything/AskAnythingPrompt.php | 9 +
app/Notifications/Activities/SmsReceived.php | 6 +-
app/Providers/JiminnyServiceProvider.php | 6 -
app/Repositories/AskAnythingRepository.php | 8 +-
app/Services/Activity/RingCentral/Service.php | 38 +-
app/Services/Activity/Salesforce/Service.php | 5 -
app/Services/Activity/TwilioVideo/Service.php | 26 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +
app/Services/Mail/Office/EmailApiClient.php | 78 +++-
app/Services/Mail/TextRelayService.php | 33 ++
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-
app/Services/RecallAI/RecallAIService.php | 11 +
front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-
front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++
front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +
front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-
front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +
front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-
front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +
front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------
front-end/src/components/shared/AskAnything/prompts.js | 1 +
front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-
front-end/src/store/modules/playback/mutations.js | 1 +
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---
tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-
tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--
tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-
tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-
tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-
tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-
tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-
tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++
79 files changed, 4254 insertions(+), 2020 deletions(-)
create mode 100644 .github/claude-reviewer/no-ticket-warning.txt
create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs
delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php
create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php
delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js
create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html
delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap
create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php
delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php
create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity
Switched to a new branch 'JY-20963-fix-import-on-deleted-entity'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker:worker_00: stopped
worker-emails:worker-emails_00: stopped
worker-audio:worker-audio_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.30, Copyright (c) Zend Technologies
with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-c...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Untracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001(2)\nzsh: unknown file attribute: 2\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/vin.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/vin.png \n/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/vin.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png \nWarning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.heif --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png\n/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif\n /Users/lukas/Downloads/Photos-3-001(2)/gtp.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop\nscreenpipe stopped\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull\nremote: Enumerating objects: 445, done.\nremote: Counting objects: 100% (265/265), done.\nremote: Compressing objects: 100% (64/64), done.\nremote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)\nReceiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.\nResolving deltas: 100% (285/285), completed with 67 local objects.\nFrom github.com:jiminny/app\n d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay\n 32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n 4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n * [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app\n 3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n 6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n + 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)\n * [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings\n * [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads\n 71b36decb6..6818adf8e2 master -> origin/master\nUpdating d3ee1a203b..caba325b1a\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-\n app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++---\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Services/Activity/RingCentral/Service.php | 38 +++++++--\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----\n app/Services/RecallAI/RecallAIService.php | 11 +++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++\n 29 files changed, 1009 insertions(+), 218 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n@@ -5,14 +5,11 @@\n namespace Tests\\Unit\\Notifications\\Activities;\n \n use Illuminate\\Support\\Facades\\Config;\n-use Jiminny\\Mail\\Activities\\SmsReceived as SmsMailable;\n use Jiminny\\Models\\Activity;\n use Jiminny\\Notifications\\Activities\\SmsReceived;\n use Mockery;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n-use ReflectionClass;\n use Tests\\TestCase;\n-use Vinkla\\Hashids\\Facades\\Hashids;\n \n #[CoversClass(SmsReceived::class)]\n class SmsReceivedTest extends TestCase\n\n ----------- end diff -----------\n\n 2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -198,7 +198,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -229,7 +229,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -242,7 +242,7 @@\n \n private function createTextRelayService(): TextRelayService\n {\n- $service = new class extends TextRelayService {\n+ $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n\n ----------- end diff -----------\n\n\nFixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch master\nYour branch is behind 'origin/master' by 74 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Salesforce/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1461, done.\nremote: Counting objects: 100% (841/841), done.\nremote: Compressing objects: 100% (257/257), done.\nremote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)\nReceiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.\nResolving deltas: 100% (963/963), completed with 95 local objects.\nFrom github.com:jiminny/app\n 6818adf8e2..1aee7aad9a master -> origin/master\n 8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports\n + a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)\n * [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions\n 3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n * [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es\n 729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n + 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)\n * [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events\n * [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522\nUpdating 177ea29b8d..1aee7aad9a\nFast-forward\n .github/claude-reviewer/no-ticket-warning.txt | 8 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++\n .github/workflows/claude.yml | 72 +++-\n .github/workflows/secfix.yml | 2 +-\n app/Component/AskAnything/AskAnythingPromptService.php | 10 +-\n app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/BatchStatusInterface.php | 48 ---\n app/Component/ES/Processor/BatchStatusManager.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-\n app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++\n app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-\n app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -\n app/Component/ES/QueuePriorityEnum.php | 43 ++\n app/Component/ES/UpdateProcessManager.php | 102 ++---\n app/Component/Transcription/Service/TranscriptionService.php | 24 +-\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 11 +-\n app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/SearchTransformer.php | 1 +\n app/Jobs/Activity/Import/DownloadTrack.php | 8 +-\n app/Models/AskAnything/AskAnythingPrompt.php | 9 +\n app/Notifications/Activities/SmsReceived.php | 6 +-\n app/Providers/JiminnyServiceProvider.php | 6 -\n app/Repositories/AskAnythingRepository.php | 8 +-\n app/Services/Activity/RingCentral/Service.php | 38 +-\n app/Services/Activity/Salesforce/Service.php | 5 -\n app/Services/Activity/TwilioVideo/Service.php | 26 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +\n app/Services/Mail/Office/EmailApiClient.php | 78 +++-\n app/Services/Mail/TextRelayService.php | 33 ++\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-\n app/Services/RecallAI/RecallAIService.php | 11 +\n front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-\n front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++\n front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +\n front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-\n front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +\n front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-\n front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +\n front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------\n front-end/src/components/shared/AskAnything/prompts.js | 1 +\n front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-\n front-end/src/store/modules/playback/mutations.js | 1 +\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---\n tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-\n tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--\n tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-\n tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-\n tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-\n tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-\n tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++\n 79 files changed, 4254 insertions(+), 2020 deletions(-)\n create mode 100644 .github/claude-reviewer/no-ticket-warning.txt\n create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs\n delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php\n create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php\n delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js\n create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html\n delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap\n create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php\n delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php\n create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity\nSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports, no_whitespace_in_blank_line)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -10,10 +10,6 @@\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n use Illuminate\\Support\\Facades\\Log;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Jiminny\\Component\\Queue\\Constants;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n-use Jiminny\\Models\\TextRelay;\n use Jiminny\\Services\\Mail\\TextRelayService;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n@@ -346,7 +342,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n@@ -389,7 +385,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n\n ----------- end diff -----------\n\n\nFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 11, done.\nremote: Counting objects: 100% (11/11), done.\nremote: Compressing objects: 100% (4/4), done.\nremote: Total 11 (delta 7), reused 8 (delta 7), pack-reused 0 (from 0)\nUnpacking objects: 100% (11/11), 4.29 KiB | 366.00 KiB/s, done.\nFrom github.com:jiminny/app\n 65fe479f9f..96be090229 JY-208020-salesforce-zoom-integration -> origin/JY-208020-salesforce-zoom-integration\n d3dd77afee..02b98c0850 JY-20960-lemon-something-went-wrong-error -> origin/JY-20960-lemon-something-went-wrong-error\nUpdating 1aee7aad9a..23fdef5aa9\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Http/Controllers/API/ActivityController.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Services/Mail/TextRelayService.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 1aee7aad9a..23fdef5aa9\nFast-forward\n .circleci/config_continue.yml | 10 +-\n .github/claude-reviewer/no-ticket-warning.txt | 5 +-\n .github/claude-reviewer/prompts/no-requirements.txt | 25 +\n .github/claude-reviewer/prompts/with-requirements.txt | 31 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 45 +-\n .github/workflows/claude.yml | 9 +-\n .php-cs-fixer.dist.php | 1 +\n Makefile | 17 +-\n app/Component/ActionItems/Notifications/ActionItemsNotification.php | 2 +-\n app/Component/ActivityAnalytics/Service/TopicTriggerService.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityActualDate.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityRecordingStopped.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityScheduledDate.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/AiCallScoreFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/AutoScoreFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/ClosedDealsFilter.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/CoachingFeedbackAverageScore.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/CrmFieldCollection.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/CurrentStage.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/Customer.php | 12 +-\n app/Component/ActivitySearch/FilterDefinition/HasTopicTriggersFilterDefinition.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/HasTranscription.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/LoggedToCrm.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/OnlyActiveUsers.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/OrganiserUserIn.php | 10 +-\n app/Component/ActivitySearch/FilterDefinition/OrganiserUserNotIn.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/PartnerFilterDefinition.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/Security/PrivateMeetingsForCurrentUserOnly.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/Security/RestrictPublicActivitiesOnly.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/Security/RestrictTeam.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/ShowInternalExternalActivitiesFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/DateRangeFilter.php | 10 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/Exists.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/UserGroupInFilter.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/UserInFilter.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/TeamMemberUserIn.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/TranscriptionComposite.php | 2 +-\n app/Component/ActivitySearch/FilterDefinitionCollection.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/ActivityCrmContactsHandler.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/ActivityCrmFieldsHandler.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/ActivityCrmTaskEventHandler.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/OpportunityCrmFieldHandler.php | 2 +-\n app/Component/AiCallScoring/Services/GetAiCallScoringService.php | 2 +-\n app/Component/BillingManagement/MaxioClient.php | 2 +-\n app/Component/DealInsights/DealInsightsCriteriaBuilder.php | 4 +-\n app/Component/DealInsights/DealService.php | 4 +-\n app/Component/DealInsights/DealsRepository.php | 2 +-\n app/Component/DealInsights/Forecast/ForecastService.php | 2 +-\n app/Component/DealInsights/PeriodService.php | 2 +-\n app/Component/ElasticSearch/Client.php | 2 +-\n app/Component/Encoding/Service/ParseSpeechFromSilenceService.php | 2 +-\n app/Component/Nudge/Repository/NudgeRunRepository.php | 2 +-\n app/Component/ProphetAi/Services/DealDetailsContextProvider.php | 2 +-\n app/Component/Queue/Job/RateLimitAware.php | 2 +-\n app/Component/SCIM/Builders/GroupFilterQueryBuilder.php | 2 +-\n app/Component/SCIM/Builders/UsersFilterQueryBuilder.php | 2 +-\n app/Component/SCIM/Mutators/GroupPatchOperation.php | 2 +-\n app/Component/SCIM/Mutators/UserPatchOperation.php | 2 +-\n app/Component/SCIM/ScimProvisioning.php | 12 +-\n app/Component/Sidekick/SidekickSettingsRepository.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Action.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Channel.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Message.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Team.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/User.php | 2 +-\n app/Component/TeamInsights/AutomatedCallScoreRepository.php | 8 +-\n app/Component/TeamInsights/TopicTrigger/TeamInsightsTopicTriggerRepository.php | 10 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/CallTranscript.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/Records.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/Transcript.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/TranscriptSentence.php | 2 +-\n app/Component/Transcription/Formatter/TranscriptionFormatter.php | 2 +-\n app/Component/Transcription/Service/SearchService.php | 18 +-\n app/Component/Twilio/Conference/ConferenceHandler/SpecificationCallbackHandler.php | 2 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 2 +-\n app/Component/Uploader/Notifications/ActivityUploadedNotification.php | 2 +-\n app/Console/Commands/Activities/JustCall/SyncPlaybackLinkToCrmCommand.php | 4 +-\n app/Console/Commands/Analytics/NumberOfActivitiesPerActivityTypeCommand.php | 20 +-\n app/Console/Commands/Analytics/TranscriptionWordMatchCommand.php | 22 +-\n app/Console/Commands/Dev/AddRateLimitCommand.php | 2 +-\n app/Console/Commands/EngagementStats/JiminnyEngagementStatsExplainCommand.php | 2 +-\n app/Console/Commands/Mailboxes/BatchProcess.php | 2 +-\n app/Console/Commands/Reports/GenerateMarketingReport.php | 6 +-\n app/Contracts/Services/Calendar/CalendarTrait.php | 2 +-\n app/DTO/ImportCall/ZoomPhone/CallDenormalizer.php | 2 +-\n app/DTO/SCIM/AAD/Response.php | 2 +-\n app/DTO/SCIM/AAD/Response/ListResponse.php | 4 +-\n app/Events/Users/UserRolesChangedEvent.php | 2 +-\n app/Http/Controllers/API/ActivityController.php | 6 +-\n app/Http/Controllers/API/CrmController.php | 2 +-\n app/Http/Controllers/API/DealInsights/DealsController.php | 2 +-\n app/Http/Controllers/API/Page/PlaybackController.php | 2 +-\n app/Http/Controllers/API/ScimController.php | 78 +-\n app/Http/Controllers/API/TeamController.php | 2 +-\n app/Http/Controllers/API/TeamInsights/CoachingFeedbacksController.php | 2 +-\n app/Http/Controllers/API/TranscriptionController.php | 2 +-\n app/Http/Controllers/CustomerApi/CustomerApiController.php | 12 +-\n app/Http/Controllers/GeocodingController.php | 2 +-\n app/Http/Controllers/Kiosk/ProfileController.php | 2 +-\n app/Http/Controllers/Kiosk/SearchController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Transformers/ActivityTransformer.php | 6 +-\n app/Http/Transformers/CustomerApi/CustomerApiActivityTransformer.php | 2 +-\n app/Http/Transformers/CustomerApi/CustomerApiLeadTransformer.php | 2 +-\n app/Http/Transformers/MessageTransformer.php | 2 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PlaybookTreeTransformer.php | 4 +-\n app/Integrations/Releases.php | 2 +-\n app/Jobs/Activity/SyncActivity.php | 2 +-\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 2 +-\n app/Jobs/Crm/SaveActivity.php | 4 +-\n app/Jobs/Crm/SyncTeamMetadata.php | 2 +-\n app/Jobs/Mailbox/EmailTextRelay.php | 4 +-\n app/Jobs/MeetingBot/ConfigureLiveStream.php | 2 +-\n app/Listeners/Activities/Conferences/Ended.php | 2 +-\n app/Listeners/Activities/Conferences/Locked.php | 2 +-\n app/Listeners/Activities/Conferences/Started.php | 2 +-\n app/Listeners/Activities/Connections/Closed.php | 2 +-\n app/Listeners/Activities/Connections/Held.php | 2 +-\n app/Listeners/Activities/Connections/Muted.php | 2 +-\n app/Listeners/Activities/Connections/Opened.php | 2 +-\n app/Listeners/Activities/Connections/Unheld.php | 2 +-\n app/Listeners/Activities/Connections/Unmuted.php | 2 +-\n app/Listeners/Activities/SendExportEmail.php | 2 +-\n app/Listeners/Transcription/SendTranscriptionToCrmActivity.php | 2 +-\n app/Mcp/Repositories/McpElasticCallRepository.php | 4 +-\n app/Models/Activity/ActivityImport.php | 2 +-\n app/Notifications/Activities/Available.php | 2 +-\n app/Notifications/Activities/ExportViewed.php | 2 +-\n app/Notifications/Activities/MailBoxFailedToConnect.php | 2 +-\n app/Notifications/Activities/NotifyContributor.php | 2 +-\n app/Notifications/Activities/ParticipantDeclinedRecording.php | 2 +-\n app/Notifications/Activities/SmsReceived.php | 2 +-\n app/Notifications/ActivityCommented.php | 2 +-\n app/Notifications/ActivityLiveCoached.php | 4 +-\n app/Notifications/ActivityLiveCoachingNote.php | 2 +-\n app/Notifications/ActivityMentioned.php | 4 +-\n app/Notifications/ActivityNotLogged.php | 2 +-\n app/Notifications/ActivityScheduled.php | 4 +-\n app/Notifications/ActivityScored.php | 4 +-\n app/Notifications/ActivityShared.php | 4 +-\n app/Notifications/AiAutomation/AiCrmExportReady.php | 2 +-\n app/Notifications/AiAutomation/CrmFillingAutomationMisconfiguredNotification.php | 2 +-\n app/Notifications/Calendars/CalendarFailedToConnect.php | 2 +-\n app/Notifications/CoachRequested.php | 4 +-\n app/Notifications/Crm/AccountOwnerDisconnected.php | 2 +-\n app/Notifications/Crm/ActivityLogFailed.php | 2 +-\n app/Notifications/Crm/ApiDisabled.php | 2 +-\n app/Notifications/Crm/FieldUpdateFailed.php | 2 +-\n app/Notifications/Crm/ProviderChanged.php | 2 +-\n app/Notifications/Crm/QuotaExceeded.php | 2 +-\n app/Notifications/Crm/StageUpdateFailed.php | 2 +-\n app/Notifications/Crm/SyncedFieldsChanged.php | 2 +-\n app/Notifications/NewCustomerApiToken.php | 2 +-\n app/Notifications/OpportunityAlsoCommented.php | 2 +-\n app/Notifications/OpportunityCommented.php | 2 +-\n app/Notifications/OpportunityMentioned.php | 4 +-\n app/Notifications/OpportunityUpdateNotification.php | 2 +-\n app/Notifications/Playlists/ActivityAdded.php | 4 +-\n app/Notifications/Playlists/PlaylistSharedNotification.php | 4 +-\n app/Notifications/SlackBotAdded.php | 2 +-\n app/Notifications/SlackBotRemoved.php | 2 +-\n app/Notifications/Tracks/Restored.php | 2 +-\n app/Notifications/UserInvitedToTeam.php | 2 +-\n app/Notifications/UserInvitedToTeamWithEmailOnly.php | 2 +-\n app/Notifications/UserPromotedTeamOwner.php | 2 +-\n app/Providers/SsoServiceProvider.php | 2 +-\n app/Providers/ViewerGuardServiceProvider.php | 4 +-\n app/Repositories/ElasticActivityRepository.php | 118 +-\n app/Repositories/PlaylistActivityRepository.php | 2 +-\n app/Repositories/TeamInsightsRepository.php | 178 +--\n app/Repositories/TeamRepository.php | 2 +-\n app/Services/Activity/Gmail/Service.php | 6 +-\n app/Services/Activity/Office/Service.php | 2 +-\n app/Services/Activity/RingCentral/Client.php | 2 +-\n app/Services/Activity/Talkdesk/Api/DataClient.php | 2 +-\n app/Services/Activity/Vonage/Import/DataImportHandler.php | 2 +-\n app/Services/ActivityService.php | 2 +-\n app/Services/Calendar/OfficeCalendarService.php | 2 +-\n app/Services/Crm/Close/Service.php | 2 +-\n app/Services/Crm/Close/Translator/AccountMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/FieldMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/OpportunityMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/OrganisationMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/PipelineMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/ProfileMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/StageMetadataTranslator.php | 2 +-\n app/Services/Crm/Copper/Service.php | 2 +-\n app/Services/Crm/Hubspot/ServiceTraits/WriteCrmTrait.php | 2 +-\n app/Services/Crm/Salesforce/Client.php | 2 +-\n app/Services/Crm/Salesforce/Service.php | 2 +-\n app/Services/Mail/TextRelayService.php | 2 +-\n app/Services/MeetingGenerator/AbstractMeetingProvider.php | 2 +-\n app/Services/MeetingGenerator/TeamsMeetingProvider.php | 2 +-\n app/Services/Security/Authy.php | 6 +-\n app/Traits/RequiresUUID.php | 4 +-\n app/VO/Repository/TranscriptionKeywordParser.php | 6 +-\n composer.json | 8 +-\n composer.lock | 4638 ++++++++++++++++++++++--------------------------------------------\n config/database.php | 12 +-\n tests/Feature/Component/Notification/ActivityFollowUpSlackMessageBuilderTest.php | 2 +-\n tests/Feature/Services/Crm/Close/ClientTest.php | 6 +-\n tests/Unit/Actions/UpdateUserRolesActionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ActivityScheduledDateTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/AiCallScoreFilterTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/AutoScoreFilterTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ClosedDealsFilterTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/CoachingFeedbackAverageScoreTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/CrmFieldCollectionTest.php | 24 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ExternalIdTest.php | 8 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/HasTopicTriggersFilterDefinitionTest.php | 4 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/HasTranscriptionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/LanguageFilterDefinitionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/PartnerFilterDefinitionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/Security/PrivateMeetingsForCurrentUserOnlyTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ShowInternalExternalActivitiesFilterTest.php | 4 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/TeamInsights/DateRangeFilterTest.php | 16 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/TeamInsights/UserInFilterTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/TeamMemberUserInTest.php | 4 +-\n tests/Unit/Component/ActivitySearch/FilterDefinitionCollectionTest.php | 16 +-\n tests/Unit/Component/AiAutomation/SaveCrmTemplateRunsServiceTest.php | 10 +-\n tests/Unit/Component/DateTime/DateTimeZoneManagerTest.php | 2 +-\n tests/Unit/Component/DealInsights/Forecast/ForecastServiceTest.php | 12 +-\n tests/Unit/Component/FFMpeg/Services/SwitchAudioChannelsTest.php | 3 +-\n tests/Unit/Component/Nudge/Notification/NudgeEmailNotificationTest.php | 4 +-\n tests/Unit/Component/Nudge/Notification/NudgeSlackNotificationTest.php | 4 +-\n tests/Unit/Component/Playlist/Http/Request/MovePlaylistActivityRequestTest.php | 2 +-\n tests/Unit/Component/Sidekick/SidekickServiceTest.php | 2 +-\n tests/Unit/Component/TeamInsights/TopicsInDeals/EsQueries/TopicsInDealsAggregationTest.php | 2 +-\n tests/Unit/Component/TeamInsights/TopicsInDeals/TopicsInDealsComparisonRepositoryTest.php | 8 +-\n tests/Unit/Component/TeamInsights/TopicsInDeals/TopicsInDealsRepositoryTest.php | 2 +-\n tests/Unit/Contracts/Services/Calendar/CalendarTraitTest.php | 24 +-\n tests/Unit/DTO/ImportCall/JustCall/CallDenormalizerTest.php | 12 +-\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 10 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 4 +-\n tests/Unit/Jobs/Activity/Import/ImportCallTest.php | 4 +-\n tests/Unit/Jobs/Activity/Import/MatchCrmDataTest.php | 2 +-\n tests/Unit/Jobs/Activity/SyncActivityTest.php | 8 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 2 +-\n tests/Unit/Jobs/User/SyncToIntercomTest.php | 2 +-\n tests/Unit/Listeners/Import/ActivityImportSubscriberTest.php | 8 +-\n tests/Unit/Listeners/Users/SetupMailSyncTest.php | 2 +-\n tests/Unit/Notifications/AiAutomation/AiCrmExportReadyTest.php | 6 +-\n tests/Unit/Notifications/AiAutomation/CrmFillingAutomationMisconfiguredNotificationTest.php | 6 +-\n tests/Unit/Notifications/OpportunityUpdateNotificationTest.php | 4 +-\n tests/Unit/Notifications/UserInvitedToTeamWithEmailOnlyTest.php | 2 +-\n tests/Unit/Services/Activity/Bloobirds/CallDenormalizerTest.php | 4 +-\n tests/Unit/Services/Activity/CloudCall/ClientTest.php | 2 +-\n tests/Unit/Services/Activity/CloudCall/ServiceTest.php | 2 +-\n tests/Unit/Services/Activity/FiveNine/DataClientTest.php | 2 +-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 2 +-\n tests/Unit/Services/Activity/Vonage/Import/CallDenormalizerTest.php | 4 +-\n tests/Unit/Services/Activity/Vonage/Import/DataImportHandlerTest.php | 2 +-\n tests/Unit/Services/Calendar/Command/ValidateGoogleEventAttendeePresenceTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Processor/MetadataProcessorTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Processor/OpportunityProcessorTest.php | 2 +-\n tests/Unit/Services/Crm/Close/ServiceTest.php | 4 +-\n tests/Unit/Services/Crm/Close/Translator/AccountMetadataTranslatorTest.php | 10 +-\n tests/Unit/Services/Crm/Close/Translator/FieldMetadataTranslatorTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Translator/OpportunityMetadataTranslatorTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Translator/OrganisationMetadataTranslatorTest.php | 6 +-\n tests/Unit/Services/Crm/Close/Translator/PipelineMetadataTranslatorTest.php | 16 +-\n tests/Unit/Services/Crm/Close/Translator/ProfileMetadataTranslatorTest.php | 2 +-\n tests/Unit/Services/Crm/CrmObjectsResolverTest.php | 4 +-\n tests/Unit/Traits/TestPrivateMethod.php | 2 +-\n 268 files changed, 2321 insertions(+), 3819 deletions(-)\n create mode 100644 .github/claude-reviewer/prompts/no-requirements.txt\n create mode 100644 .github/claude-reviewer/prompts/with-requirements.txt\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co","depth":4,"on_screen":true,"value":"Untracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001(2)\nzsh: unknown file attribute: 2\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/vin.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/vin.png \n/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/vin.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png \nWarning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.heif --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png\n/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif\n /Users/lukas/Downloads/Photos-3-001(2)/gtp.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop\nscreenpipe stopped\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull\nremote: Enumerating objects: 445, done.\nremote: Counting objects: 100% (265/265), done.\nremote: Compressing objects: 100% (64/64), done.\nremote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)\nReceiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.\nResolving deltas: 100% (285/285), completed with 67 local objects.\nFrom github.com:jiminny/app\n d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay\n 32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n 4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n * [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app\n 3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n 6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n + 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)\n * [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings\n * [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads\n 71b36decb6..6818adf8e2 master -> origin/master\nUpdating d3ee1a203b..caba325b1a\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-\n app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++---\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Services/Activity/RingCentral/Service.php | 38 +++++++--\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----\n app/Services/RecallAI/RecallAIService.php | 11 +++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++\n 29 files changed, 1009 insertions(+), 218 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n@@ -5,14 +5,11 @@\n namespace Tests\\Unit\\Notifications\\Activities;\n \n use Illuminate\\Support\\Facades\\Config;\n-use Jiminny\\Mail\\Activities\\SmsReceived as SmsMailable;\n use Jiminny\\Models\\Activity;\n use Jiminny\\Notifications\\Activities\\SmsReceived;\n use Mockery;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n-use ReflectionClass;\n use Tests\\TestCase;\n-use Vinkla\\Hashids\\Facades\\Hashids;\n \n #[CoversClass(SmsReceived::class)]\n class SmsReceivedTest extends TestCase\n\n ----------- end diff -----------\n\n 2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -198,7 +198,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -229,7 +229,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -242,7 +242,7 @@\n \n private function createTextRelayService(): TextRelayService\n {\n- $service = new class extends TextRelayService {\n+ $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n\n ----------- end diff -----------\n\n\nFixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch master\nYour branch is behind 'origin/master' by 74 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Salesforce/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1461, done.\nremote: Counting objects: 100% (841/841), done.\nremote: Compressing objects: 100% (257/257), done.\nremote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)\nReceiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.\nResolving deltas: 100% (963/963), completed with 95 local objects.\nFrom github.com:jiminny/app\n 6818adf8e2..1aee7aad9a master -> origin/master\n 8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports\n + a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)\n * [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions\n 3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n * [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es\n 729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n + 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)\n * [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events\n * [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522\nUpdating 177ea29b8d..1aee7aad9a\nFast-forward\n .github/claude-reviewer/no-ticket-warning.txt | 8 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++\n .github/workflows/claude.yml | 72 +++-\n .github/workflows/secfix.yml | 2 +-\n app/Component/AskAnything/AskAnythingPromptService.php | 10 +-\n app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/BatchStatusInterface.php | 48 ---\n app/Component/ES/Processor/BatchStatusManager.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-\n app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++\n app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-\n app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -\n app/Component/ES/QueuePriorityEnum.php | 43 ++\n app/Component/ES/UpdateProcessManager.php | 102 ++---\n app/Component/Transcription/Service/TranscriptionService.php | 24 +-\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 11 +-\n app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/SearchTransformer.php | 1 +\n app/Jobs/Activity/Import/DownloadTrack.php | 8 +-\n app/Models/AskAnything/AskAnythingPrompt.php | 9 +\n app/Notifications/Activities/SmsReceived.php | 6 +-\n app/Providers/JiminnyServiceProvider.php | 6 -\n app/Repositories/AskAnythingRepository.php | 8 +-\n app/Services/Activity/RingCentral/Service.php | 38 +-\n app/Services/Activity/Salesforce/Service.php | 5 -\n app/Services/Activity/TwilioVideo/Service.php | 26 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +\n app/Services/Mail/Office/EmailApiClient.php | 78 +++-\n app/Services/Mail/TextRelayService.php | 33 ++\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-\n app/Services/RecallAI/RecallAIService.php | 11 +\n front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-\n front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++\n front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +\n front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-\n front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +\n front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-\n front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +\n front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------\n front-end/src/components/shared/AskAnything/prompts.js | 1 +\n front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-\n front-end/src/store/modules/playback/mutations.js | 1 +\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---\n tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-\n tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--\n tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-\n tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-\n tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-\n tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-\n tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++\n 79 files changed, 4254 insertions(+), 2020 deletions(-)\n create mode 100644 .github/claude-reviewer/no-ticket-warning.txt\n create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs\n delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php\n create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php\n delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js\n create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html\n delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap\n create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php\n delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php\n create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity\nSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports, no_whitespace_in_blank_line)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -10,10 +10,6 @@\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n use Illuminate\\Support\\Facades\\Log;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Jiminny\\Component\\Queue\\Constants;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n-use Jiminny\\Models\\TextRelay;\n use Jiminny\\Services\\Mail\\TextRelayService;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n@@ -346,7 +342,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n@@ -389,7 +385,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n\n ----------- end diff -----------\n\n\nFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 11, done.\nremote: Counting objects: 100% (11/11), done.\nremote: Compressing objects: 100% (4/4), done.\nremote: Total 11 (delta 7), reused 8 (delta 7), pack-reused 0 (from 0)\nUnpacking objects: 100% (11/11), 4.29 KiB | 366.00 KiB/s, done.\nFrom github.com:jiminny/app\n 65fe479f9f..96be090229 JY-208020-salesforce-zoom-integration -> origin/JY-208020-salesforce-zoom-integration\n d3dd77afee..02b98c0850 JY-20960-lemon-something-went-wrong-error -> origin/JY-20960-lemon-something-went-wrong-error\nUpdating 1aee7aad9a..23fdef5aa9\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Http/Controllers/API/ActivityController.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Services/Mail/TextRelayService.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nUpdating 1aee7aad9a..23fdef5aa9\nFast-forward\n .circleci/config_continue.yml | 10 +-\n .github/claude-reviewer/no-ticket-warning.txt | 5 +-\n .github/claude-reviewer/prompts/no-requirements.txt | 25 +\n .github/claude-reviewer/prompts/with-requirements.txt | 31 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 45 +-\n .github/workflows/claude.yml | 9 +-\n .php-cs-fixer.dist.php | 1 +\n Makefile | 17 +-\n app/Component/ActionItems/Notifications/ActionItemsNotification.php | 2 +-\n app/Component/ActivityAnalytics/Service/TopicTriggerService.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityActualDate.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityRecordingStopped.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/ActivityScheduledDate.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/AiCallScoreFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/AutoScoreFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/ClosedDealsFilter.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/CoachingFeedbackAverageScore.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/CrmFieldCollection.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/CurrentStage.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/Customer.php | 12 +-\n app/Component/ActivitySearch/FilterDefinition/HasTopicTriggersFilterDefinition.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/HasTranscription.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/LoggedToCrm.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/OnlyActiveUsers.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/OrganiserUserIn.php | 10 +-\n app/Component/ActivitySearch/FilterDefinition/OrganiserUserNotIn.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/PartnerFilterDefinition.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/Security/PrivateMeetingsForCurrentUserOnly.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/Security/RestrictPublicActivitiesOnly.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/Security/RestrictTeam.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/ShowInternalExternalActivitiesFilter.php | 2 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/DateRangeFilter.php | 10 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/Exists.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/UserGroupInFilter.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/TeamInsights/UserInFilter.php | 6 +-\n app/Component/ActivitySearch/FilterDefinition/TeamMemberUserIn.php | 4 +-\n app/Component/ActivitySearch/FilterDefinition/TranscriptionComposite.php | 2 +-\n app/Component/ActivitySearch/FilterDefinitionCollection.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/ActivityCrmContactsHandler.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/ActivityCrmFieldsHandler.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/ActivityCrmTaskEventHandler.php | 2 +-\n app/Component/AiAutomation/ProphetServiceHandlers/OpportunityCrmFieldHandler.php | 2 +-\n app/Component/AiCallScoring/Services/GetAiCallScoringService.php | 2 +-\n app/Component/BillingManagement/MaxioClient.php | 2 +-\n app/Component/DealInsights/DealInsightsCriteriaBuilder.php | 4 +-\n app/Component/DealInsights/DealService.php | 4 +-\n app/Component/DealInsights/DealsRepository.php | 2 +-\n app/Component/DealInsights/Forecast/ForecastService.php | 2 +-\n app/Component/DealInsights/PeriodService.php | 2 +-\n app/Component/ElasticSearch/Client.php | 2 +-\n app/Component/Encoding/Service/ParseSpeechFromSilenceService.php | 2 +-\n app/Component/Nudge/Repository/NudgeRunRepository.php | 2 +-\n app/Component/ProphetAi/Services/DealDetailsContextProvider.php | 2 +-\n app/Component/Queue/Job/RateLimitAware.php | 2 +-\n app/Component/SCIM/Builders/GroupFilterQueryBuilder.php | 2 +-\n app/Component/SCIM/Builders/UsersFilterQueryBuilder.php | 2 +-\n app/Component/SCIM/Mutators/GroupPatchOperation.php | 2 +-\n app/Component/SCIM/Mutators/UserPatchOperation.php | 2 +-\n app/Component/SCIM/ScimProvisioning.php | 12 +-\n app/Component/Sidekick/SidekickSettingsRepository.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Action.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Channel.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Message.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/Team.php | 2 +-\n app/Component/Slack/DTO/Event/BlockAction/User.php | 2 +-\n app/Component/TeamInsights/AutomatedCallScoreRepository.php | 8 +-\n app/Component/TeamInsights/TopicTrigger/TeamInsightsTopicTriggerRepository.php | 10 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/CallTranscript.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/Records.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/Transcript.php | 2 +-\n app/Component/Transcription/DTO/Http/Transcription/Gong/Gong/SuccessfulResponse/TranscriptSentence.php | 2 +-\n app/Component/Transcription/Formatter/TranscriptionFormatter.php | 2 +-\n app/Component/Transcription/Service/SearchService.php | 18 +-\n app/Component/Twilio/Conference/ConferenceHandler/SpecificationCallbackHandler.php | 2 +-\n app/Component/Twilio/Service/SoftPhoneService.php | 2 +-\n app/Component/Uploader/Notifications/ActivityUploadedNotification.php | 2 +-\n app/Console/Commands/Activities/JustCall/SyncPlaybackLinkToCrmCommand.php | 4 +-\n app/Console/Commands/Analytics/NumberOfActivitiesPerActivityTypeCommand.php | 20 +-\n app/Console/Commands/Analytics/TranscriptionWordMatchCommand.php | 22 +-\n app/Console/Commands/Dev/AddRateLimitCommand.php | 2 +-\n app/Console/Commands/EngagementStats/JiminnyEngagementStatsExplainCommand.php | 2 +-\n app/Console/Commands/Mailboxes/BatchProcess.php | 2 +-\n app/Console/Commands/Reports/GenerateMarketingReport.php | 6 +-\n app/Contracts/Services/Calendar/CalendarTrait.php | 2 +-\n app/DTO/ImportCall/ZoomPhone/CallDenormalizer.php | 2 +-\n app/DTO/SCIM/AAD/Response.php | 2 +-\n app/DTO/SCIM/AAD/Response/ListResponse.php | 4 +-\n app/Events/Users/UserRolesChangedEvent.php | 2 +-\n app/Http/Controllers/API/ActivityController.php | 6 +-\n app/Http/Controllers/API/CrmController.php | 2 +-\n app/Http/Controllers/API/DealInsights/DealsController.php | 2 +-\n app/Http/Controllers/API/Page/PlaybackController.php | 2 +-\n app/Http/Controllers/API/ScimController.php | 78 +-\n app/Http/Controllers/API/TeamController.php | 2 +-\n app/Http/Controllers/API/TeamInsights/CoachingFeedbacksController.php | 2 +-\n app/Http/Controllers/API/TranscriptionController.php | 2 +-\n app/Http/Controllers/CustomerApi/CustomerApiController.php | 12 +-\n app/Http/Controllers/GeocodingController.php | 2 +-\n app/Http/Controllers/Kiosk/ProfileController.php | 2 +-\n app/Http/Controllers/Kiosk/SearchController.php | 6 +-\n app/Http/Controllers/TeamSetupController.php | 4 +-\n app/Http/Transformers/ActivityTransformer.php | 6 +-\n app/Http/Transformers/CustomerApi/CustomerApiActivityTransformer.php | 2 +-\n app/Http/Transformers/CustomerApi/CustomerApiLeadTransformer.php | 2 +-\n app/Http/Transformers/MessageTransformer.php | 2 +-\n app/Http/Transformers/OnDemandActivitiesTransformer.php | 2 +-\n app/Http/Transformers/PlaybookTreeTransformer.php | 4 +-\n app/Integrations/Releases.php | 2 +-\n app/Jobs/Activity/SyncActivity.php | 2 +-\n app/Jobs/Crm/Hubspot/ImportBatchJobTrait.php | 2 +-\n app/Jobs/Crm/SaveActivity.php | 4 +-\n app/Jobs/Crm/SyncTeamMetadata.php | 2 +-\n app/Jobs/Mailbox/EmailTextRelay.php | 4 +-\n app/Jobs/MeetingBot/ConfigureLiveStream.php | 2 +-\n app/Listeners/Activities/Conferences/Ended.php | 2 +-\n app/Listeners/Activities/Conferences/Locked.php | 2 +-\n app/Listeners/Activities/Conferences/Started.php | 2 +-\n app/Listeners/Activities/Connections/Closed.php | 2 +-\n app/Listeners/Activities/Connections/Held.php | 2 +-\n app/Listeners/Activities/Connections/Muted.php | 2 +-\n app/Listeners/Activities/Connections/Opened.php | 2 +-\n app/Listeners/Activities/Connections/Unheld.php | 2 +-\n app/Listeners/Activities/Connections/Unmuted.php | 2 +-\n app/Listeners/Activities/SendExportEmail.php | 2 +-\n app/Listeners/Transcription/SendTranscriptionToCrmActivity.php | 2 +-\n app/Mcp/Repositories/McpElasticCallRepository.php | 4 +-\n app/Models/Activity/ActivityImport.php | 2 +-\n app/Notifications/Activities/Available.php | 2 +-\n app/Notifications/Activities/ExportViewed.php | 2 +-\n app/Notifications/Activities/MailBoxFailedToConnect.php | 2 +-\n app/Notifications/Activities/NotifyContributor.php | 2 +-\n app/Notifications/Activities/ParticipantDeclinedRecording.php | 2 +-\n app/Notifications/Activities/SmsReceived.php | 2 +-\n app/Notifications/ActivityCommented.php | 2 +-\n app/Notifications/ActivityLiveCoached.php | 4 +-\n app/Notifications/ActivityLiveCoachingNote.php | 2 +-\n app/Notifications/ActivityMentioned.php | 4 +-\n app/Notifications/ActivityNotLogged.php | 2 +-\n app/Notifications/ActivityScheduled.php | 4 +-\n app/Notifications/ActivityScored.php | 4 +-\n app/Notifications/ActivityShared.php | 4 +-\n app/Notifications/AiAutomation/AiCrmExportReady.php | 2 +-\n app/Notifications/AiAutomation/CrmFillingAutomationMisconfiguredNotification.php | 2 +-\n app/Notifications/Calendars/CalendarFailedToConnect.php | 2 +-\n app/Notifications/CoachRequested.php | 4 +-\n app/Notifications/Crm/AccountOwnerDisconnected.php | 2 +-\n app/Notifications/Crm/ActivityLogFailed.php | 2 +-\n app/Notifications/Crm/ApiDisabled.php | 2 +-\n app/Notifications/Crm/FieldUpdateFailed.php | 2 +-\n app/Notifications/Crm/ProviderChanged.php | 2 +-\n app/Notifications/Crm/QuotaExceeded.php | 2 +-\n app/Notifications/Crm/StageUpdateFailed.php | 2 +-\n app/Notifications/Crm/SyncedFieldsChanged.php | 2 +-\n app/Notifications/NewCustomerApiToken.php | 2 +-\n app/Notifications/OpportunityAlsoCommented.php | 2 +-\n app/Notifications/OpportunityCommented.php | 2 +-\n app/Notifications/OpportunityMentioned.php | 4 +-\n app/Notifications/OpportunityUpdateNotification.php | 2 +-\n app/Notifications/Playlists/ActivityAdded.php | 4 +-\n app/Notifications/Playlists/PlaylistSharedNotification.php | 4 +-\n app/Notifications/SlackBotAdded.php | 2 +-\n app/Notifications/SlackBotRemoved.php | 2 +-\n app/Notifications/Tracks/Restored.php | 2 +-\n app/Notifications/UserInvitedToTeam.php | 2 +-\n app/Notifications/UserInvitedToTeamWithEmailOnly.php | 2 +-\n app/Notifications/UserPromotedTeamOwner.php | 2 +-\n app/Providers/SsoServiceProvider.php | 2 +-\n app/Providers/ViewerGuardServiceProvider.php | 4 +-\n app/Repositories/ElasticActivityRepository.php | 118 +-\n app/Repositories/PlaylistActivityRepository.php | 2 +-\n app/Repositories/TeamInsightsRepository.php | 178 +--\n app/Repositories/TeamRepository.php | 2 +-\n app/Services/Activity/Gmail/Service.php | 6 +-\n app/Services/Activity/Office/Service.php | 2 +-\n app/Services/Activity/RingCentral/Client.php | 2 +-\n app/Services/Activity/Talkdesk/Api/DataClient.php | 2 +-\n app/Services/Activity/Vonage/Import/DataImportHandler.php | 2 +-\n app/Services/ActivityService.php | 2 +-\n app/Services/Calendar/OfficeCalendarService.php | 2 +-\n app/Services/Crm/Close/Service.php | 2 +-\n app/Services/Crm/Close/Translator/AccountMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/FieldMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/OpportunityMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/OrganisationMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/PipelineMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/ProfileMetadataTranslator.php | 2 +-\n app/Services/Crm/Close/Translator/StageMetadataTranslator.php | 2 +-\n app/Services/Crm/Copper/Service.php | 2 +-\n app/Services/Crm/Hubspot/ServiceTraits/WriteCrmTrait.php | 2 +-\n app/Services/Crm/Salesforce/Client.php | 2 +-\n app/Services/Crm/Salesforce/Service.php | 2 +-\n app/Services/Mail/TextRelayService.php | 2 +-\n app/Services/MeetingGenerator/AbstractMeetingProvider.php | 2 +-\n app/Services/MeetingGenerator/TeamsMeetingProvider.php | 2 +-\n app/Services/Security/Authy.php | 6 +-\n app/Traits/RequiresUUID.php | 4 +-\n app/VO/Repository/TranscriptionKeywordParser.php | 6 +-\n composer.json | 8 +-\n composer.lock | 4638 ++++++++++++++++++++++--------------------------------------------\n config/database.php | 12 +-\n tests/Feature/Component/Notification/ActivityFollowUpSlackMessageBuilderTest.php | 2 +-\n tests/Feature/Services/Crm/Close/ClientTest.php | 6 +-\n tests/Unit/Actions/UpdateUserRolesActionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ActivityScheduledDateTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/AiCallScoreFilterTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/AutoScoreFilterTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ClosedDealsFilterTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/CoachingFeedbackAverageScoreTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/CrmFieldCollectionTest.php | 24 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ExternalIdTest.php | 8 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/HasTopicTriggersFilterDefinitionTest.php | 4 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/HasTranscriptionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/LanguageFilterDefinitionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/PartnerFilterDefinitionTest.php | 2 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/Security/PrivateMeetingsForCurrentUserOnlyTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/ShowInternalExternalActivitiesFilterTest.php | 4 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/TeamInsights/DateRangeFilterTest.php | 16 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/TeamInsights/UserInFilterTest.php | 6 +-\n tests/Unit/Component/ActivitySearch/FilterDefinition/TeamMemberUserInTest.php | 4 +-\n tests/Unit/Component/ActivitySearch/FilterDefinitionCollectionTest.php | 16 +-\n tests/Unit/Component/AiAutomation/SaveCrmTemplateRunsServiceTest.php | 10 +-\n tests/Unit/Component/DateTime/DateTimeZoneManagerTest.php | 2 +-\n tests/Unit/Component/DealInsights/Forecast/ForecastServiceTest.php | 12 +-\n tests/Unit/Component/FFMpeg/Services/SwitchAudioChannelsTest.php | 3 +-\n tests/Unit/Component/Nudge/Notification/NudgeEmailNotificationTest.php | 4 +-\n tests/Unit/Component/Nudge/Notification/NudgeSlackNotificationTest.php | 4 +-\n tests/Unit/Component/Playlist/Http/Request/MovePlaylistActivityRequestTest.php | 2 +-\n tests/Unit/Component/Sidekick/SidekickServiceTest.php | 2 +-\n tests/Unit/Component/TeamInsights/TopicsInDeals/EsQueries/TopicsInDealsAggregationTest.php | 2 +-\n tests/Unit/Component/TeamInsights/TopicsInDeals/TopicsInDealsComparisonRepositoryTest.php | 8 +-\n tests/Unit/Component/TeamInsights/TopicsInDeals/TopicsInDealsRepositoryTest.php | 2 +-\n tests/Unit/Contracts/Services/Calendar/CalendarTraitTest.php | 24 +-\n tests/Unit/DTO/ImportCall/JustCall/CallDenormalizerTest.php | 12 +-\n tests/Unit/Http/Transformers/ActivityTransformerTest.php | 10 +-\n tests/Unit/Http/Transformers/PartnerTransformerTest.php | 4 +-\n tests/Unit/Jobs/Activity/Import/ImportCallTest.php | 4 +-\n tests/Unit/Jobs/Activity/Import/MatchCrmDataTest.php | 2 +-\n tests/Unit/Jobs/Activity/SyncActivityTest.php | 8 +-\n tests/Unit/Jobs/Team/SyncToIntercomTest.php | 2 +-\n tests/Unit/Jobs/User/SyncToIntercomTest.php | 2 +-\n tests/Unit/Listeners/Import/ActivityImportSubscriberTest.php | 8 +-\n tests/Unit/Listeners/Users/SetupMailSyncTest.php | 2 +-\n tests/Unit/Notifications/AiAutomation/AiCrmExportReadyTest.php | 6 +-\n tests/Unit/Notifications/AiAutomation/CrmFillingAutomationMisconfiguredNotificationTest.php | 6 +-\n tests/Unit/Notifications/OpportunityUpdateNotificationTest.php | 4 +-\n tests/Unit/Notifications/UserInvitedToTeamWithEmailOnlyTest.php | 2 +-\n tests/Unit/Services/Activity/Bloobirds/CallDenormalizerTest.php | 4 +-\n tests/Unit/Services/Activity/CloudCall/ClientTest.php | 2 +-\n tests/Unit/Services/Activity/CloudCall/ServiceTest.php | 2 +-\n tests/Unit/Services/Activity/FiveNine/DataClientTest.php | 2 +-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 2 +-\n tests/Unit/Services/Activity/Vonage/Import/CallDenormalizerTest.php | 4 +-\n tests/Unit/Services/Activity/Vonage/Import/DataImportHandlerTest.php | 2 +-\n tests/Unit/Services/Calendar/Command/ValidateGoogleEventAttendeePresenceTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Processor/MetadataProcessorTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Processor/OpportunityProcessorTest.php | 2 +-\n tests/Unit/Services/Crm/Close/ServiceTest.php | 4 +-\n tests/Unit/Services/Crm/Close/Translator/AccountMetadataTranslatorTest.php | 10 +-\n tests/Unit/Services/Crm/Close/Translator/FieldMetadataTranslatorTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Translator/OpportunityMetadataTranslatorTest.php | 8 +-\n tests/Unit/Services/Crm/Close/Translator/OrganisationMetadataTranslatorTest.php | 6 +-\n tests/Unit/Services/Crm/Close/Translator/PipelineMetadataTranslatorTest.php | 16 +-\n tests/Unit/Services/Crm/Close/Translator/ProfileMetadataTranslatorTest.php | 2 +-\n tests/Unit/Services/Crm/CrmObjectsResolverTest.php | 4 +-\n tests/Unit/Traits/TestPrivateMethod.php | 2 +-\n 268 files changed, 2321 insertions(+), 3819 deletions(-)\n create mode 100644 .github/claude-reviewer/prompts/no-requirements.txt\n create mode 100644 .github/claude-reviewer/prompts/with-requirements.txt\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.0013888889,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.19444445,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.19861111,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.39166668,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.39583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5888889,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.59305555,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7861111,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7902778,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47291666,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
1738179714311874313
|
-824448891317507332
|
visual_change
|
accessibility
|
NULL
|
Untracked files:
(use "git add <file>. Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/vin.HEIC --out ~/Downloads/Photos-3-001\(2\)/vin.png
/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/vin.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-f.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-b.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.HEIC --out ~/Downloads/Photos-3-001\(2\)/gtp.png
Warning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.heif --out ~/Downloads/Photos-3-001\(2\)/gtp.png
/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif
/Users/lukas/Downloads/Photos-3-001(2)/gtp.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop
screenpipe stopped
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull
remote: Enumerating objects: 445, done.
remote: Counting objects: 100% (265/265), done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)
Receiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.
Resolving deltas: 100% (285/285), completed with 67 local objects.
From github.com:jiminny/app
d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay
32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
* [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app
3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
+ 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)
* [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings
* [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads
71b36decb6..6818adf8e2 master -> origin/master
Updating d3ee1a203b..caba325b1a
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-
app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++---
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Services/Activity/RingCentral/Service.php | 38 +++++++--
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----
app/Services/RecallAI/RecallAIService.php | 11 +++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++
29 files changed, 1009 insertions(+), 218 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
@@ -5,14 +5,11 @@
namespace Tests\Unit\Notifications\Activities;
use Illuminate\Support\Facades\Config;
-use Jiminny\Mail\Activities\SmsReceived as SmsMailable;
use Jiminny\Models\Activity;
use Jiminny\Notifications\Activities\SmsReceived;
use Mockery;
use PHPUnit\Framework\Attributes\CoversClass;
-use ReflectionClass;
use Tests\TestCase;
-use Vinkla\Hashids\Facades\Hashids;
#[CoversClass(SmsReceived::class)]
class SmsReceivedTest extends TestCase
----------- end diff -----------
2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
@@ -198,7 +198,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -229,7 +229,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -242,7 +242,7 @@
private function createTextRelayService(): TextRelayService
{
- $service = new class extends TextRelayService {
+ $service = new class () extends TextRelayService {
public function __construct()
{
}
----------- end diff -----------
Fixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1461, done.
remote: Counting objects: 100% (841/841), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)
Receiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.
Resolving deltas: 100% (963/963), completed with 95 local objects.
From github.com:jiminny/app
6818adf8e2..1aee7aad9a master -> origin/master
8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports
+ a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)
* [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions
3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
* [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es
729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
+ 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)
* [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events
* [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522
Updating 177ea29b8d..1aee7aad9a
Fast-forward
.github/claude-reviewer/no-ticket-warning.txt | 8 +
.github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++
.github/workflows/claude.yml | 72 +++-
.github/workflows/secfix.yml | 2 +-
app/Component/AskAnything/AskAnythingPromptService.php | 10 +-
app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/BatchStatusInterface.php | 48 ---
app/Component/ES/Processor/BatchStatusManager.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-
app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++
app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-
app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -
app/Component/ES/QueuePriorityEnum.php | 43 ++
app/Component/ES/UpdateProcessManager.php | 102 ++---
app/Component/Transcription/Service/TranscriptionService.php | 24 +-
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 11 +-
app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/SearchTransformer.php | 1 +
app/Jobs/Activity/Import/DownloadTrack.php | 8 +-
app/Models/AskAnything/AskAnythingPrompt.php | 9 +
app/Notifications/Activities/SmsReceived.php | 6 +-
app/Providers/JiminnyServiceProvider.php | 6 -
app/Repositories/AskAnythingRepository.php | 8 +-
app/Services/Activity/RingCentral/Service.php | 38 +-
app/Services/Activity/Salesforce/Service.php | 5 -
app/Services/Activity/TwilioVideo/Service.php | 26 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +
app/Services/Mail/Office/EmailApiClient.php | 78 +++-
app/Services/Mail/TextRelayService.php | 33 ++
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-
app/Services/RecallAI/RecallAIService.php | 11 +
front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-
front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++
front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +
front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-
front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +
front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-
front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +
front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------
front-end/src/components/shared/AskAnything/prompts.js | 1 +
front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-
front-end/src/store/modules/playback/mutations.js | 1 +
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---
tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-
tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--
tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-
tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-
tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-
tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-
tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-
tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-
tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++
79 files changed, 4254 insertions(+), 2020 deletions(-)
create mode 100644 .github/claude-reviewer/no-ticket-warning.txt
create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs
delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php
create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php
delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js
create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html
delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap
create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php
delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php
create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity
Switched to a new branch 'JY-20963-fix-import-on-deleted-entity'
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 supervisorctl restart all
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-calendar:worker-calendar_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker:worker_00: stopped
worker-emails:worker-emails_00: stopped
worker-audio:worker-audio_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-cli/
docker exec -it docker_lamp_1 php -v
PHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.30, Copyright (c) Zend Technologies
with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at https://docs.docker.com/go/debug-c...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71765
|
2572
|
27
|
2026-05-26T07:07:06.857948+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779226857_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/vin.HEIC --out ~/Downloads/Photos-3-001\(2\)/vin.png
/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/vin.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-f.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-b.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.HEIC --out ~/Downloads/Photos-3-001\(2\)/gtp.png
Warning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.heif --out ~/Downloads/Photos-3-001\(2\)/gtp.png
/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif
/Users/lukas/Downloads/Photos-3-001(2)/gtp.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop
screenpipe stopped
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull
remote: Enumerating objects: 445, done.
remote: Counting objects: 100% (265/265), done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)
Receiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.
Resolving deltas: 100% (285/285), completed with 67 local objects.
From github.com:jiminny/app
d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay
32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
* [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app
3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
+ 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)
* [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings
* [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads
71b36decb6..6818adf8e2 master -> origin/master
Updating d3ee1a203b..caba325b1a
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-
app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++---
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Services/Activity/RingCentral/Service.php | 38 +++++++--
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----
app/Services/RecallAI/RecallAIService.php | 11 +++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++
29 files changed, 1009 insertions(+), 218 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
@@ -5,14 +5,11 @@
namespace Tests\Unit\Notifications\Activities;
use Illuminate\Support\Facades\Config;
-use Jiminny\Mail\Activities\SmsReceived as SmsMailable;
use Jiminny\Models\Activity;
use Jiminny\Notifications\Activities\SmsReceived;
use Mockery;
use PHPUnit\Framework\Attributes\CoversClass;
-use ReflectionClass;
use Tests\TestCase;
-use Vinkla\Hashids\Facades\Hashids;
#[CoversClass(SmsReceived::class)]
class SmsReceivedTest extends TestCase
----------- end diff -----------
2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
@@ -198,7 +198,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -229,7 +229,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -242,7 +242,7 @@
private function createTextRelayService(): TextRelayService
{
- $service = new class extends TextRelayService {
+ $service = new class () extends TextRelayService {
public function __construct()
{
}
----------- end diff -----------
Fixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1461, done.
remote: Counting objects: 100% (841/841), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)
Receiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.
Resolving deltas: 100% (963/963), completed with 95 local objects.
From github.com:jiminny/app
6818adf8e2..1aee7aad9a master -> origin/master
8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports
+ a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)
* [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions
3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
* [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es
729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
+ 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)
* [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events
* [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522
Updating 177ea29b8d..1aee7aad9a
Fast-forward
.github/claude-reviewer/no-ticket-warning.txt | 8 +
.github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++
.github/workflows/claude.yml | 72 +++-
.github/workflows/secfix.yml | 2 +-
app/Component/AskAnything/AskAnythingPromptService.php | 10 +-
app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/BatchStatusInterface.php | 48 ---
app/Component/ES/Processor/BatchStatusManager.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-
app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++
app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-
app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -
app/Component/ES/QueuePriorityEnum.php | 43 ++
app/Component/ES/UpdateProcessManager.php | 102 ++---
app/Component/Transcription/Service/TranscriptionService.php | 24 +-
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 11 +-
app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/SearchTransformer.php | 1 +
app/Jobs/Activity/Import/DownloadTrack.php | 8 +-
app/Models/AskAnything/AskAnythingPrompt.php | 9 +
app/Notifications/Activities/SmsReceived.php | 6 +-
app/Providers/JiminnyServiceProvider.php | 6 -
app/Repositories/AskAnythingRepository.php | 8 +-
app/Services/Activity/RingCentral/Service.php | 38 +-
app/Services/Activity/Salesforce/Service.php | 5 -
app/Services/Activity/TwilioVideo/Service.php | 26 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +
app/Services/Mail/Office/EmailApiClient.php | 78 +++-
app/Services/Mail/TextRelayService.php | 33 ++
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-
app/Services/RecallAI/RecallAIService.php | 11 +
front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-
front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++
front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +
front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-
front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +
front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-
front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +
front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------
front-end/src/components/shared/AskAnything/prompts.js | 1 +
front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-
front-end/src/store/modules/playback/mutations.js | 1 +
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---
tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-
tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--
tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-
tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-
tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-
tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001(2)\nzsh: unknown file attribute: 2\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/vin.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/vin.png \n/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/vin.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png \nWarning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.heif --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png\n/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif\n /Users/lukas/Downloads/Photos-3-001(2)/gtp.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop\nscreenpipe stopped\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull\nremote: Enumerating objects: 445, done.\nremote: Counting objects: 100% (265/265), done.\nremote: Compressing objects: 100% (64/64), done.\nremote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)\nReceiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.\nResolving deltas: 100% (285/285), completed with 67 local objects.\nFrom github.com:jiminny/app\n d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay\n 32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n 4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n * [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app\n 3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n 6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n + 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)\n * [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings\n * [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads\n 71b36decb6..6818adf8e2 master -> origin/master\nUpdating d3ee1a203b..caba325b1a\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-\n app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++---\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Services/Activity/RingCentral/Service.php | 38 +++++++--\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----\n app/Services/RecallAI/RecallAIService.php | 11 +++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++\n 29 files changed, 1009 insertions(+), 218 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n@@ -5,14 +5,11 @@\n namespace Tests\\Unit\\Notifications\\Activities;\n \n use Illuminate\\Support\\Facades\\Config;\n-use Jiminny\\Mail\\Activities\\SmsReceived as SmsMailable;\n use Jiminny\\Models\\Activity;\n use Jiminny\\Notifications\\Activities\\SmsReceived;\n use Mockery;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n-use ReflectionClass;\n use Tests\\TestCase;\n-use Vinkla\\Hashids\\Facades\\Hashids;\n \n #[CoversClass(SmsReceived::class)]\n class SmsReceivedTest extends TestCase\n\n ----------- end diff -----------\n\n 2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -198,7 +198,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -229,7 +229,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -242,7 +242,7 @@\n \n private function createTextRelayService(): TextRelayService\n {\n- $service = new class extends TextRelayService {\n+ $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n\n ----------- end diff -----------\n\n\nFixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch master\nYour branch is behind 'origin/master' by 74 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Salesforce/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1461, done.\nremote: Counting objects: 100% (841/841), done.\nremote: Compressing objects: 100% (257/257), done.\nremote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)\nReceiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.\nResolving deltas: 100% (963/963), completed with 95 local objects.\nFrom github.com:jiminny/app\n 6818adf8e2..1aee7aad9a master -> origin/master\n 8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports\n + a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)\n * [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions\n 3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n * [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es\n 729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n + 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)\n * [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events\n * [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522\nUpdating 177ea29b8d..1aee7aad9a\nFast-forward\n .github/claude-reviewer/no-ticket-warning.txt | 8 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++\n .github/workflows/claude.yml | 72 +++-\n .github/workflows/secfix.yml | 2 +-\n app/Component/AskAnything/AskAnythingPromptService.php | 10 +-\n app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/BatchStatusInterface.php | 48 ---\n app/Component/ES/Processor/BatchStatusManager.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-\n app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++\n app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-\n app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -\n app/Component/ES/QueuePriorityEnum.php | 43 ++\n app/Component/ES/UpdateProcessManager.php | 102 ++---\n app/Component/Transcription/Service/TranscriptionService.php | 24 +-\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 11 +-\n app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/SearchTransformer.php | 1 +\n app/Jobs/Activity/Import/DownloadTrack.php | 8 +-\n app/Models/AskAnything/AskAnythingPrompt.php | 9 +\n app/Notifications/Activities/SmsReceived.php | 6 +-\n app/Providers/JiminnyServiceProvider.php | 6 -\n app/Repositories/AskAnythingRepository.php | 8 +-\n app/Services/Activity/RingCentral/Service.php | 38 +-\n app/Services/Activity/Salesforce/Service.php | 5 -\n app/Services/Activity/TwilioVideo/Service.php | 26 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +\n app/Services/Mail/Office/EmailApiClient.php | 78 +++-\n app/Services/Mail/TextRelayService.php | 33 ++\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-\n app/Services/RecallAI/RecallAIService.php | 11 +\n front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-\n front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++\n front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +\n front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-\n front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +\n front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-\n front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +\n front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------\n front-end/src/components/shared/AskAnything/prompts.js | 1 +\n front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-\n front-end/src/store/modules/playback/mutations.js | 1 +\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---\n tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-\n tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--\n tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-\n tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-\n tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-\n tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-\n tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++\n 79 files changed, 4254 insertions(+), 2020 deletions(-)\n create mode 100644 .github/claude-reviewer/no-ticket-warning.txt\n create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs\n delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php\n create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php\n delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js\n create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html\n delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap\n create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php\n delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php\n create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity\nSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports, no_whitespace_in_blank_line)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -10,10 +10,6 @@\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n use Illuminate\\Support\\Facades\\Log;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Jiminny\\Component\\Queue\\Constants;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n-use Jiminny\\Models\\TextRelay;\n use Jiminny\\Services\\Mail\\TextRelayService;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n@@ -346,7 +342,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n@@ -389,7 +385,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n\n ----------- end diff -----------\n\n\nFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 11, done.\nremote: Counting objects: 100% (11/11), done.\nremote: Compressing objects: 100% (4/4), done.\nremote: Total 11 (delta 7), reused 8 (delta 7), pack-reused 0 (from 0)\nUnpacking objects: 100% (11/11), 4.29 KiB | 366.00 KiB/s, done.\nFrom github.com:jiminny/app\n 65fe479f9f..96be090229 JY-208020-salesforce-zoom-integration -> origin/JY-208020-salesforce-zoom-integration\n d3dd77afee..02b98c0850 JY-20960-lemon-something-went-wrong-error -> origin/JY-20960-lemon-something-went-wrong-error\nUpdating 1aee7aad9a..23fdef5aa9\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Http/Controllers/API/ActivityController.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Services/Mail/TextRelayService.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $","depth":4,"on_screen":true,"value":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001(2)\nzsh: unknown file attribute: 2\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/vin.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/vin.png \n/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/vin.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png \nWarning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.heif --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png\n/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif\n /Users/lukas/Downloads/Photos-3-001(2)/gtp.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop\nscreenpipe stopped\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull\nremote: Enumerating objects: 445, done.\nremote: Counting objects: 100% (265/265), done.\nremote: Compressing objects: 100% (64/64), done.\nremote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)\nReceiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.\nResolving deltas: 100% (285/285), completed with 67 local objects.\nFrom github.com:jiminny/app\n d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay\n 32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n 4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n * [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app\n 3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n 6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n + 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)\n * [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings\n * [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads\n 71b36decb6..6818adf8e2 master -> origin/master\nUpdating d3ee1a203b..caba325b1a\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-\n app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++---\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Services/Activity/RingCentral/Service.php | 38 +++++++--\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----\n app/Services/RecallAI/RecallAIService.php | 11 +++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++\n 29 files changed, 1009 insertions(+), 218 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n@@ -5,14 +5,11 @@\n namespace Tests\\Unit\\Notifications\\Activities;\n \n use Illuminate\\Support\\Facades\\Config;\n-use Jiminny\\Mail\\Activities\\SmsReceived as SmsMailable;\n use Jiminny\\Models\\Activity;\n use Jiminny\\Notifications\\Activities\\SmsReceived;\n use Mockery;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n-use ReflectionClass;\n use Tests\\TestCase;\n-use Vinkla\\Hashids\\Facades\\Hashids;\n \n #[CoversClass(SmsReceived::class)]\n class SmsReceivedTest extends TestCase\n\n ----------- end diff -----------\n\n 2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -198,7 +198,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -229,7 +229,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -242,7 +242,7 @@\n \n private function createTextRelayService(): TextRelayService\n {\n- $service = new class extends TextRelayService {\n+ $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n\n ----------- end diff -----------\n\n\nFixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch master\nYour branch is behind 'origin/master' by 74 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Salesforce/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1461, done.\nremote: Counting objects: 100% (841/841), done.\nremote: Compressing objects: 100% (257/257), done.\nremote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)\nReceiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.\nResolving deltas: 100% (963/963), completed with 95 local objects.\nFrom github.com:jiminny/app\n 6818adf8e2..1aee7aad9a master -> origin/master\n 8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports\n + a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)\n * [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions\n 3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n * [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es\n 729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n + 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)\n * [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events\n * [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522\nUpdating 177ea29b8d..1aee7aad9a\nFast-forward\n .github/claude-reviewer/no-ticket-warning.txt | 8 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++\n .github/workflows/claude.yml | 72 +++-\n .github/workflows/secfix.yml | 2 +-\n app/Component/AskAnything/AskAnythingPromptService.php | 10 +-\n app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/BatchStatusInterface.php | 48 ---\n app/Component/ES/Processor/BatchStatusManager.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-\n app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++\n app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-\n app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -\n app/Component/ES/QueuePriorityEnum.php | 43 ++\n app/Component/ES/UpdateProcessManager.php | 102 ++---\n app/Component/Transcription/Service/TranscriptionService.php | 24 +-\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 11 +-\n app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/SearchTransformer.php | 1 +\n app/Jobs/Activity/Import/DownloadTrack.php | 8 +-\n app/Models/AskAnything/AskAnythingPrompt.php | 9 +\n app/Notifications/Activities/SmsReceived.php | 6 +-\n app/Providers/JiminnyServiceProvider.php | 6 -\n app/Repositories/AskAnythingRepository.php | 8 +-\n app/Services/Activity/RingCentral/Service.php | 38 +-\n app/Services/Activity/Salesforce/Service.php | 5 -\n app/Services/Activity/TwilioVideo/Service.php | 26 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +\n app/Services/Mail/Office/EmailApiClient.php | 78 +++-\n app/Services/Mail/TextRelayService.php | 33 ++\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-\n app/Services/RecallAI/RecallAIService.php | 11 +\n front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-\n front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++\n front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +\n front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-\n front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +\n front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-\n front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +\n front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------\n front-end/src/components/shared/AskAnything/prompts.js | 1 +\n front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-\n front-end/src/store/modules/playback/mutations.js | 1 +\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---\n tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-\n tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--\n tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-\n tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-\n tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-\n tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-\n tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++\n 79 files changed, 4254 insertions(+), 2020 deletions(-)\n create mode 100644 .github/claude-reviewer/no-ticket-warning.txt\n create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs\n delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php\n create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php\n delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js\n create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html\n delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap\n create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php\n delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php\n create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity\nSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports, no_whitespace_in_blank_line)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -10,10 +10,6 @@\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n use Illuminate\\Support\\Facades\\Log;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Jiminny\\Component\\Queue\\Constants;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n-use Jiminny\\Models\\TextRelay;\n use Jiminny\\Services\\Mail\\TextRelayService;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n@@ -346,7 +342,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n@@ -389,7 +385,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n\n ----------- end diff -----------\n\n\nFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 11, done.\nremote: Counting objects: 100% (11/11), done.\nremote: Compressing objects: 100% (4/4), done.\nremote: Total 11 (delta 7), reused 8 (delta 7), pack-reused 0 (from 0)\nUnpacking objects: 100% (11/11), 4.29 KiB | 366.00 KiB/s, done.\nFrom github.com:jiminny/app\n 65fe479f9f..96be090229 JY-208020-salesforce-zoom-integration -> origin/JY-208020-salesforce-zoom-integration\n d3dd77afee..02b98c0850 JY-20960-lemon-something-went-wrong-error -> origin/JY-20960-lemon-something-went-wrong-error\nUpdating 1aee7aad9a..23fdef5aa9\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Http/Controllers/API/ActivityController.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Services/Mail/TextRelayService.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.0013888889,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.19444445,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.19861111,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.39166668,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.39583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5888889,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.59305555,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7861111,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7902778,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47291666,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
6301176414030891772
|
-824167416439362820
|
click
|
accessibility
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/vin.HEIC --out ~/Downloads/Photos-3-001\(2\)/vin.png
/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/vin.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-f.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-b.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.HEIC --out ~/Downloads/Photos-3-001\(2\)/gtp.png
Warning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.heif --out ~/Downloads/Photos-3-001\(2\)/gtp.png
/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif
/Users/lukas/Downloads/Photos-3-001(2)/gtp.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop
screenpipe stopped
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull
remote: Enumerating objects: 445, done.
remote: Counting objects: 100% (265/265), done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)
Receiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.
Resolving deltas: 100% (285/285), completed with 67 local objects.
From github.com:jiminny/app
d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay
32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
* [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app
3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
+ 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)
* [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings
* [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads
71b36decb6..6818adf8e2 master -> origin/master
Updating d3ee1a203b..caba325b1a
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-
app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++---
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Services/Activity/RingCentral/Service.php | 38 +++++++--
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----
app/Services/RecallAI/RecallAIService.php | 11 +++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++
29 files changed, 1009 insertions(+), 218 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
@@ -5,14 +5,11 @@
namespace Tests\Unit\Notifications\Activities;
use Illuminate\Support\Facades\Config;
-use Jiminny\Mail\Activities\SmsReceived as SmsMailable;
use Jiminny\Models\Activity;
use Jiminny\Notifications\Activities\SmsReceived;
use Mockery;
use PHPUnit\Framework\Attributes\CoversClass;
-use ReflectionClass;
use Tests\TestCase;
-use Vinkla\Hashids\Facades\Hashids;
#[CoversClass(SmsReceived::class)]
class SmsReceivedTest extends TestCase
----------- end diff -----------
2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
@@ -198,7 +198,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -229,7 +229,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -242,7 +242,7 @@
private function createTextRelayService(): TextRelayService
{
- $service = new class extends TextRelayService {
+ $service = new class () extends TextRelayService {
public function __construct()
{
}
----------- end diff -----------
Fixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1461, done.
remote: Counting objects: 100% (841/841), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)
Receiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.
Resolving deltas: 100% (963/963), completed with 95 local objects.
From github.com:jiminny/app
6818adf8e2..1aee7aad9a master -> origin/master
8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports
+ a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)
* [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions
3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
* [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es
729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
+ 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)
* [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events
* [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522
Updating 177ea29b8d..1aee7aad9a
Fast-forward
.github/claude-reviewer/no-ticket-warning.txt | 8 +
.github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++
.github/workflows/claude.yml | 72 +++-
.github/workflows/secfix.yml | 2 +-
app/Component/AskAnything/AskAnythingPromptService.php | 10 +-
app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/BatchStatusInterface.php | 48 ---
app/Component/ES/Processor/BatchStatusManager.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-
app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++
app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-
app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -
app/Component/ES/QueuePriorityEnum.php | 43 ++
app/Component/ES/UpdateProcessManager.php | 102 ++---
app/Component/Transcription/Service/TranscriptionService.php | 24 +-
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 11 +-
app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/SearchTransformer.php | 1 +
app/Jobs/Activity/Import/DownloadTrack.php | 8 +-
app/Models/AskAnything/AskAnythingPrompt.php | 9 +
app/Notifications/Activities/SmsReceived.php | 6 +-
app/Providers/JiminnyServiceProvider.php | 6 -
app/Repositories/AskAnythingRepository.php | 8 +-
app/Services/Activity/RingCentral/Service.php | 38 +-
app/Services/Activity/Salesforce/Service.php | 5 -
app/Services/Activity/TwilioVideo/Service.php | 26 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +
app/Services/Mail/Office/EmailApiClient.php | 78 +++-
app/Services/Mail/TextRelayService.php | 33 ++
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-
app/Services/RecallAI/RecallAIService.php | 11 +
front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-
front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++
front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +
front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-
front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +
front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-
front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +
front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------
front-end/src/components/shared/AskAnything/prompts.js | 1 +
front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-
front-end/src/store/modules/playback/mutations.js | 1 +
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---
tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-
tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--
tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-
tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-
tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-
tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php ...
|
71761
|
NULL
|
NULL
|
NULL
|
|
71764
|
2573
|
66
|
2026-05-26T07:07:06.703397+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779226703_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/vin.HEIC --out ~/Downloads/Photos-3-001\(2\)/vin.png
/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/vin.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-f.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-b.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.HEIC --out ~/Downloads/Photos-3-001\(2\)/gtp.png
Warning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.heif --out ~/Downloads/Photos-3-001\(2\)/gtp.png
/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif
/Users/lukas/Downloads/Photos-3-001(2)/gtp.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop
screenpipe stopped
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull
remote: Enumerating objects: 445, done.
remote: Counting objects: 100% (265/265), done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)
Receiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.
Resolving deltas: 100% (285/285), completed with 67 local objects.
From github.com:jiminny/app
d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay
32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
* [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app
3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
+ 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)
* [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings
* [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads
71b36decb6..6818adf8e2 master -> origin/master
Updating d3ee1a203b..caba325b1a
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-
app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++---
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Services/Activity/RingCentral/Service.php | 38 +++++++--
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----
app/Services/RecallAI/RecallAIService.php | 11 +++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++
29 files changed, 1009 insertions(+), 218 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
@@ -5,14 +5,11 @@
namespace Tests\Unit\Notifications\Activities;
use Illuminate\Support\Facades\Config;
-use Jiminny\Mail\Activities\SmsReceived as SmsMailable;
use Jiminny\Models\Activity;
use Jiminny\Notifications\Activities\SmsReceived;
use Mockery;
use PHPUnit\Framework\Attributes\CoversClass;
-use ReflectionClass;
use Tests\TestCase;
-use Vinkla\Hashids\Facades\Hashids;
#[CoversClass(SmsReceived::class)]
class SmsReceivedTest extends TestCase
----------- end diff -----------
2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
@@ -198,7 +198,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -229,7 +229,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -242,7 +242,7 @@
private function createTextRelayService(): TextRelayService
{
- $service = new class extends TextRelayService {
+ $service = new class () extends TextRelayService {
public function __construct()
{
}
----------- end diff -----------
Fixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1461, done.
remote: Counting objects: 100% (841/841), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)
Receiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.
Resolving deltas: 100% (963/963), completed with 95 local objects.
From github.com:jiminny/app
6818adf8e2..1aee7aad9a master -> origin/master
8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports
+ a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)
* [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions
3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
* [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es
729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
+ 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)
* [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events
* [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522
Updating 177ea29b8d..1aee7aad9a
Fast-forward
.github/claude-reviewer/no-ticket-warning.txt | 8 +
.github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++
.github/workflows/claude.yml | 72 +++-
.github/workflows/secfix.yml | 2 +-
app/Component/AskAnything/AskAnythingPromptService.php | 10 +-
app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/BatchStatusInterface.php | 48 ---
app/Component/ES/Processor/BatchStatusManager.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-
app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++
app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-
app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -
app/Component/ES/QueuePriorityEnum.php | 43 ++
app/Component/ES/UpdateProcessManager.php | 102 ++---
app/Component/Transcription/Service/TranscriptionService.php | 24 +-
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 11 +-
app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/SearchTransformer.php | 1 +
app/Jobs/Activity/Import/DownloadTrack.php | 8 +-
app/Models/AskAnything/AskAnythingPrompt.php | 9 +
app/Notifications/Activities/SmsReceived.php | 6 +-
app/Providers/JiminnyServiceProvider.php | 6 -
app/Repositories/AskAnythingRepository.php | 8 +-
app/Services/Activity/RingCentral/Service.php | 38 +-
app/Services/Activity/Salesforce/Service.php | 5 -
app/Services/Activity/TwilioVideo/Service.php | 26 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +
app/Services/Mail/Office/EmailApiClient.php | 78 +++-
app/Services/Mail/TextRelayService.php | 33 ++
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-
app/Services/RecallAI/RecallAIService.php | 11 +
front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-
front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++
front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +
front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-
front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +
front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-
front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +
front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------
front-end/src/components/shared/AskAnything/prompts.js | 1 +
front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-
front-end/src/store/modules/playback/mutations.js | 1 +
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---
tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-
tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--
tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-
tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-
tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-
tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001(2)\nzsh: unknown file attribute: 2\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/vin.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/vin.png \n/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/vin.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png \nWarning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.heif --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png\n/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif\n /Users/lukas/Downloads/Photos-3-001(2)/gtp.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop\nscreenpipe stopped\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull\nremote: Enumerating objects: 445, done.\nremote: Counting objects: 100% (265/265), done.\nremote: Compressing objects: 100% (64/64), done.\nremote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)\nReceiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.\nResolving deltas: 100% (285/285), completed with 67 local objects.\nFrom github.com:jiminny/app\n d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay\n 32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n 4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n * [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app\n 3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n 6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n + 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)\n * [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings\n * [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads\n 71b36decb6..6818adf8e2 master -> origin/master\nUpdating d3ee1a203b..caba325b1a\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-\n app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++---\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Services/Activity/RingCentral/Service.php | 38 +++++++--\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----\n app/Services/RecallAI/RecallAIService.php | 11 +++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++\n 29 files changed, 1009 insertions(+), 218 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n@@ -5,14 +5,11 @@\n namespace Tests\\Unit\\Notifications\\Activities;\n \n use Illuminate\\Support\\Facades\\Config;\n-use Jiminny\\Mail\\Activities\\SmsReceived as SmsMailable;\n use Jiminny\\Models\\Activity;\n use Jiminny\\Notifications\\Activities\\SmsReceived;\n use Mockery;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n-use ReflectionClass;\n use Tests\\TestCase;\n-use Vinkla\\Hashids\\Facades\\Hashids;\n \n #[CoversClass(SmsReceived::class)]\n class SmsReceivedTest extends TestCase\n\n ----------- end diff -----------\n\n 2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -198,7 +198,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -229,7 +229,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -242,7 +242,7 @@\n \n private function createTextRelayService(): TextRelayService\n {\n- $service = new class extends TextRelayService {\n+ $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n\n ----------- end diff -----------\n\n\nFixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch master\nYour branch is behind 'origin/master' by 74 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Salesforce/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1461, done.\nremote: Counting objects: 100% (841/841), done.\nremote: Compressing objects: 100% (257/257), done.\nremote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)\nReceiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.\nResolving deltas: 100% (963/963), completed with 95 local objects.\nFrom github.com:jiminny/app\n 6818adf8e2..1aee7aad9a master -> origin/master\n 8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports\n + a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)\n * [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions\n 3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n * [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es\n 729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n + 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)\n * [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events\n * [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522\nUpdating 177ea29b8d..1aee7aad9a\nFast-forward\n .github/claude-reviewer/no-ticket-warning.txt | 8 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++\n .github/workflows/claude.yml | 72 +++-\n .github/workflows/secfix.yml | 2 +-\n app/Component/AskAnything/AskAnythingPromptService.php | 10 +-\n app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/BatchStatusInterface.php | 48 ---\n app/Component/ES/Processor/BatchStatusManager.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-\n app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++\n app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-\n app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -\n app/Component/ES/QueuePriorityEnum.php | 43 ++\n app/Component/ES/UpdateProcessManager.php | 102 ++---\n app/Component/Transcription/Service/TranscriptionService.php | 24 +-\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 11 +-\n app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/SearchTransformer.php | 1 +\n app/Jobs/Activity/Import/DownloadTrack.php | 8 +-\n app/Models/AskAnything/AskAnythingPrompt.php | 9 +\n app/Notifications/Activities/SmsReceived.php | 6 +-\n app/Providers/JiminnyServiceProvider.php | 6 -\n app/Repositories/AskAnythingRepository.php | 8 +-\n app/Services/Activity/RingCentral/Service.php | 38 +-\n app/Services/Activity/Salesforce/Service.php | 5 -\n app/Services/Activity/TwilioVideo/Service.php | 26 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +\n app/Services/Mail/Office/EmailApiClient.php | 78 +++-\n app/Services/Mail/TextRelayService.php | 33 ++\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-\n app/Services/RecallAI/RecallAIService.php | 11 +\n front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-\n front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++\n front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +\n front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-\n front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +\n front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-\n front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +\n front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------\n front-end/src/components/shared/AskAnything/prompts.js | 1 +\n front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-\n front-end/src/store/modules/playback/mutations.js | 1 +\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---\n tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-\n tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--\n tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-\n tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-\n tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-\n tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-\n tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++\n 79 files changed, 4254 insertions(+), 2020 deletions(-)\n create mode 100644 .github/claude-reviewer/no-ticket-warning.txt\n create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs\n delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php\n create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php\n delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js\n create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html\n delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap\n create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php\n delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php\n create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity\nSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports, no_whitespace_in_blank_line)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -10,10 +10,6 @@\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n use Illuminate\\Support\\Facades\\Log;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Jiminny\\Component\\Queue\\Constants;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n-use Jiminny\\Models\\TextRelay;\n use Jiminny\\Services\\Mail\\TextRelayService;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n@@ -346,7 +342,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n@@ -389,7 +385,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n\n ----------- end diff -----------\n\n\nFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 11, done.\nremote: Counting objects: 100% (11/11), done.\nremote: Compressing objects: 100% (4/4), done.\nremote: Total 11 (delta 7), reused 8 (delta 7), pack-reused 0 (from 0)\nUnpacking objects: 100% (11/11), 4.29 KiB | 366.00 KiB/s, done.\nFrom github.com:jiminny/app\n 65fe479f9f..96be090229 JY-208020-salesforce-zoom-integration -> origin/JY-208020-salesforce-zoom-integration\n d3dd77afee..02b98c0850 JY-20960-lemon-something-went-wrong-error -> origin/JY-20960-lemon-something-went-wrong-error\nUpdating 1aee7aad9a..23fdef5aa9\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Http/Controllers/API/ActivityController.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Services/Mail/TextRelayService.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $","depth":4,"on_screen":true,"value":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001(2)\nzsh: unknown file attribute: 2\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/vin.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/vin.png \n/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/vin.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png \nWarning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.heif --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png\n/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif\n /Users/lukas/Downloads/Photos-3-001(2)/gtp.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop\nscreenpipe stopped\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull\nremote: Enumerating objects: 445, done.\nremote: Counting objects: 100% (265/265), done.\nremote: Compressing objects: 100% (64/64), done.\nremote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)\nReceiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.\nResolving deltas: 100% (285/285), completed with 67 local objects.\nFrom github.com:jiminny/app\n d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay\n 32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n 4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n * [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app\n 3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n 6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n + 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)\n * [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings\n * [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads\n 71b36decb6..6818adf8e2 master -> origin/master\nUpdating d3ee1a203b..caba325b1a\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-\n app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++---\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Services/Activity/RingCentral/Service.php | 38 +++++++--\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----\n app/Services/RecallAI/RecallAIService.php | 11 +++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++\n 29 files changed, 1009 insertions(+), 218 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n@@ -5,14 +5,11 @@\n namespace Tests\\Unit\\Notifications\\Activities;\n \n use Illuminate\\Support\\Facades\\Config;\n-use Jiminny\\Mail\\Activities\\SmsReceived as SmsMailable;\n use Jiminny\\Models\\Activity;\n use Jiminny\\Notifications\\Activities\\SmsReceived;\n use Mockery;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n-use ReflectionClass;\n use Tests\\TestCase;\n-use Vinkla\\Hashids\\Facades\\Hashids;\n \n #[CoversClass(SmsReceived::class)]\n class SmsReceivedTest extends TestCase\n\n ----------- end diff -----------\n\n 2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -198,7 +198,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -229,7 +229,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -242,7 +242,7 @@\n \n private function createTextRelayService(): TextRelayService\n {\n- $service = new class extends TextRelayService {\n+ $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n\n ----------- end diff -----------\n\n\nFixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch master\nYour branch is behind 'origin/master' by 74 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Salesforce/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1461, done.\nremote: Counting objects: 100% (841/841), done.\nremote: Compressing objects: 100% (257/257), done.\nremote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)\nReceiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.\nResolving deltas: 100% (963/963), completed with 95 local objects.\nFrom github.com:jiminny/app\n 6818adf8e2..1aee7aad9a master -> origin/master\n 8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports\n + a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)\n * [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions\n 3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n * [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es\n 729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n + 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)\n * [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events\n * [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522\nUpdating 177ea29b8d..1aee7aad9a\nFast-forward\n .github/claude-reviewer/no-ticket-warning.txt | 8 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++\n .github/workflows/claude.yml | 72 +++-\n .github/workflows/secfix.yml | 2 +-\n app/Component/AskAnything/AskAnythingPromptService.php | 10 +-\n app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/BatchStatusInterface.php | 48 ---\n app/Component/ES/Processor/BatchStatusManager.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-\n app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++\n app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-\n app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -\n app/Component/ES/QueuePriorityEnum.php | 43 ++\n app/Component/ES/UpdateProcessManager.php | 102 ++---\n app/Component/Transcription/Service/TranscriptionService.php | 24 +-\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 11 +-\n app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/SearchTransformer.php | 1 +\n app/Jobs/Activity/Import/DownloadTrack.php | 8 +-\n app/Models/AskAnything/AskAnythingPrompt.php | 9 +\n app/Notifications/Activities/SmsReceived.php | 6 +-\n app/Providers/JiminnyServiceProvider.php | 6 -\n app/Repositories/AskAnythingRepository.php | 8 +-\n app/Services/Activity/RingCentral/Service.php | 38 +-\n app/Services/Activity/Salesforce/Service.php | 5 -\n app/Services/Activity/TwilioVideo/Service.php | 26 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +\n app/Services/Mail/Office/EmailApiClient.php | 78 +++-\n app/Services/Mail/TextRelayService.php | 33 ++\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-\n app/Services/RecallAI/RecallAIService.php | 11 +\n front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-\n front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++\n front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +\n front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-\n front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +\n front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-\n front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +\n front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------\n front-end/src/components/shared/AskAnything/prompts.js | 1 +\n front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-\n front-end/src/store/modules/playback/mutations.js | 1 +\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---\n tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-\n tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--\n tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-\n tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-\n tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-\n tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-\n tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++\n 79 files changed, 4254 insertions(+), 2020 deletions(-)\n create mode 100644 .github/claude-reviewer/no-ticket-warning.txt\n create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs\n delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php\n create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php\n delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js\n create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html\n delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap\n create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php\n delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php\n create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity\nSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports, no_whitespace_in_blank_line)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -10,10 +10,6 @@\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n use Illuminate\\Support\\Facades\\Log;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Jiminny\\Component\\Queue\\Constants;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n-use Jiminny\\Models\\TextRelay;\n use Jiminny\\Services\\Mail\\TextRelayService;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n@@ -346,7 +342,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n@@ -389,7 +385,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n\n ----------- end diff -----------\n\n\nFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 11, done.\nremote: Counting objects: 100% (11/11), done.\nremote: Compressing objects: 100% (4/4), done.\nremote: Total 11 (delta 7), reused 8 (delta 7), pack-reused 0 (from 0)\nUnpacking objects: 100% (11/11), 4.29 KiB | 366.00 KiB/s, done.\nFrom github.com:jiminny/app\n 65fe479f9f..96be090229 JY-208020-salesforce-zoom-integration -> origin/JY-208020-salesforce-zoom-integration\n d3dd77afee..02b98c0850 JY-20960-lemon-something-went-wrong-error -> origin/JY-20960-lemon-something-went-wrong-error\nUpdating 1aee7aad9a..23fdef5aa9\nerror: Your local changes to the following files would be overwritten by merge:\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Http/Controllers/API/ActivityController.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Services/Mail/TextRelayService.php\nPlease commit your changes or stash them before you merge.\nAborting\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.26894948,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27094415,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.36336437,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.36535904,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.45777926,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.45977393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5521942,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.55418885,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.64660907,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.64860374,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.49667552,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
6301176414030891772
|
-824167416439362820
|
click
|
accessibility
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/vin.HEIC --out ~/Downloads/Photos-3-001\(2\)/vin.png
/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/vin.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-f.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-b.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.HEIC --out ~/Downloads/Photos-3-001\(2\)/gtp.png
Warning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.heif --out ~/Downloads/Photos-3-001\(2\)/gtp.png
/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif
/Users/lukas/Downloads/Photos-3-001(2)/gtp.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop
screenpipe stopped
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull
remote: Enumerating objects: 445, done.
remote: Counting objects: 100% (265/265), done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)
Receiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.
Resolving deltas: 100% (285/285), completed with 67 local objects.
From github.com:jiminny/app
d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay
32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
* [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app
3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
+ 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)
* [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings
* [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads
71b36decb6..6818adf8e2 master -> origin/master
Updating d3ee1a203b..caba325b1a
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-
app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++---
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Services/Activity/RingCentral/Service.php | 38 +++++++--
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----
app/Services/RecallAI/RecallAIService.php | 11 +++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++
29 files changed, 1009 insertions(+), 218 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
@@ -5,14 +5,11 @@
namespace Tests\Unit\Notifications\Activities;
use Illuminate\Support\Facades\Config;
-use Jiminny\Mail\Activities\SmsReceived as SmsMailable;
use Jiminny\Models\Activity;
use Jiminny\Notifications\Activities\SmsReceived;
use Mockery;
use PHPUnit\Framework\Attributes\CoversClass;
-use ReflectionClass;
use Tests\TestCase;
-use Vinkla\Hashids\Facades\Hashids;
#[CoversClass(SmsReceived::class)]
class SmsReceivedTest extends TestCase
----------- end diff -----------
2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
@@ -198,7 +198,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -229,7 +229,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -242,7 +242,7 @@
private function createTextRelayService(): TextRelayService
{
- $service = new class extends TextRelayService {
+ $service = new class () extends TextRelayService {
public function __construct()
{
}
----------- end diff -----------
Fixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1461, done.
remote: Counting objects: 100% (841/841), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)
Receiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.
Resolving deltas: 100% (963/963), completed with 95 local objects.
From github.com:jiminny/app
6818adf8e2..1aee7aad9a master -> origin/master
8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports
+ a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)
* [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions
3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
* [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es
729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
+ 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)
* [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events
* [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522
Updating 177ea29b8d..1aee7aad9a
Fast-forward
.github/claude-reviewer/no-ticket-warning.txt | 8 +
.github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++
.github/workflows/claude.yml | 72 +++-
.github/workflows/secfix.yml | 2 +-
app/Component/AskAnything/AskAnythingPromptService.php | 10 +-
app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/BatchStatusInterface.php | 48 ---
app/Component/ES/Processor/BatchStatusManager.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-
app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++
app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-
app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -
app/Component/ES/QueuePriorityEnum.php | 43 ++
app/Component/ES/UpdateProcessManager.php | 102 ++---
app/Component/Transcription/Service/TranscriptionService.php | 24 +-
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 11 +-
app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/SearchTransformer.php | 1 +
app/Jobs/Activity/Import/DownloadTrack.php | 8 +-
app/Models/AskAnything/AskAnythingPrompt.php | 9 +
app/Notifications/Activities/SmsReceived.php | 6 +-
app/Providers/JiminnyServiceProvider.php | 6 -
app/Repositories/AskAnythingRepository.php | 8 +-
app/Services/Activity/RingCentral/Service.php | 38 +-
app/Services/Activity/Salesforce/Service.php | 5 -
app/Services/Activity/TwilioVideo/Service.php | 26 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +
app/Services/Mail/Office/EmailApiClient.php | 78 +++-
app/Services/Mail/TextRelayService.php | 33 ++
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-
app/Services/RecallAI/RecallAIService.php | 11 +
front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-
front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++
front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +
front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-
front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +
front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-
front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +
front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------
front-end/src/components/shared/AskAnything/prompts.js | 1 +
front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-
front-end/src/store/modules/playback/mutations.js | 1 +
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---
tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-
tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--
tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-
tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-
tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-
tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php ...
|
71763
|
NULL
|
NULL
|
NULL
|
|
71763
|
2573
|
65
|
2026-05-26T07:06:57.228539+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779217228_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.44980052,"top":0.07581804,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"bounds":{"left":0.45910904,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.47041222,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.47772607,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.48636967,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.4950133,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.50598407,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.51462764,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.52327126,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.53424203,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.54521275,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.5718085,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5827792,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"bounds":{"left":0.6761968,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68849736,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.6978058,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"bounds":{"left":0.7094415,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3630781322581019188
|
2218652921735034439
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71761
|
2572
|
26
|
2026-05-26T07:06:55.669316+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779215669_m1.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6657283316452776535
|
1636146709928509859
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71762
|
2573
|
64
|
2026-05-26T07:06:55.566141+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779215566_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2705473364540108346
|
-7195096036619861568
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
rapstomEV favscojs ~CoocKetdcio1OOl-wincowphpunit.xmlToraw_sqL.query.sqlM: README.md0 sonar-proiect.properties=test.py<> Untitled Diagram.xmluvelur.connorMEWEBHOOK_FILTERING_IMPLEMENTATION.md>1h extemal Librane:v Scratches and Consolesy Daabise consolesY AEU# consolc EuA DEAL RISKS (EUJAOUEUA EU (EUJlliminnycco. nogiTextRelayServiceTest.phpcloss тexckeldysenvacepuoee tuncezonsynco, orroycarch-alt-cud, conttol key."12minny.goo0letexxhost"carch-alu", contral key"jiminny.google_text_host');$service = Sthis->getService(Snailbox);SmessageHistory = Sthis->getHistory(Sservice):messade ids"foreach (SnessageHistory as Shistories) (Snessages = Shistories->nessagesAdded;foreach (Snessages as Snessage) (Snessageld = Snessage-›nessage->id;if (! Sthis->isForCurrentEnvironment(Sservice, $nailbox, Snessageld, Sexpected0riginalRecipient)) {coneanueisuminny alocal nesA HS„local (jiminny@localhost)2 SF Liminny@localhost)A zoho_dev (jiminny@localhost)V & PRODA console (PROD]console 1rpgonlm rpponSrelayedText = TextRelay::ahere( colun'enail_provider_id', Snessageld)->firstO:if (SrelayedText aa= null) €SrelavedText = TextRelay::create(l'enail_provider' => TextRelay::PROVIDER_GSUITE,"Paoooworiessoo0eDo not lgnore -Highiight words →> text relay changes 2 files. Mon› Owner resolver changes 8 files, Mon› Sync Playbook categories and crm fieid values 10 files, 21.05.26, 14:04› Scim provisioning with rioles 8 flles, 19.05.26, 11:06CnownerKesowenes.oo.› JY-20891 SMS Text relay EU 3 files, 15.05.26, 14:05> JD & SendReportiob 2 fles. [IP_ADDRESS]:39› reorder calls 1 file, 11.05.26, 10:09> Move non paginator search calle to clentes cearch 1 fle, 11.05.26, 10:07•Almost ready one call to be tested 8 flies, 8.05.26, 14:54>huospot rate limit temo checkour77168. 8.051226.11143saved search delete 1 file, 27.04.26, 14:58exoiration mescnoe 6 flcs.[IP_ADDRESS]%•UP dev logs 1 fle, 24.04.26, 12:02tem test1iie. [IP_ADDRESS]:user transformer 3 ffles, 23.04.26, 15:43omalllrosdv 0 1ioe 22/012810721Email on not sent 5 files, 21.04.26, 19:38Oianhst tradlintoroet ovont 2166 2110428.12-09• UP tracking controller 1 file, 20.04.26, 9:22› Don't send it no s3 path (fix update to faled) 1 file, 17.04.26, 12:56> FEATURE CHANGES 3 files, 6.04.26, 13:40obase version aoo/Services/Mamextkelavserv.ce.oho2ondnanAsnace Esooy Saryscac Wami use Carbon\Carbon;_use Jininny\Component|Queue \Constants:ues esoov Hiore tashorottaytcalaye— use Jiminny \Models\TextRelay:use Google|Service\Gnafl as GoogleGnail;public function sync(): arraySnailbox = config('jininny.900gle_text_user');Sexpected0riginalRecipient = config('jininny-deploy_region') aa= 'eu"? 'catch-all-eue" . config('jiminny.google_text_host'): 'catch-all@'. config('jiminny.google_text_host');Sservice = Sthis-›getService(Snailbox):SmessageHistory = Sthis-›getHistory(Sservice);ATA10AYE customlogA console (EU)E laravelJog4 SF [iminny@localhost)A console (STAGING]A HS Jocal (jiminny@localhost)CONCAT(U.1d, GASE MHEN U.1d = t.ouner_1d THEN • (onner)• ELSE •0458184Y00 4711712713715-716719=720721=722- 723U.chostsa.*,t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.team_icWHERE u.tean_id = 1117 and sa.provider = 'hubspot":SELECT * FROM activities WHEREuuid_to_bin(^8024fffb-2df7-4017-91f4-d9f896050248') = uuirSELECT * FROM activities NHERE vuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c*) =SELECT * FROMcrn configurations WHERE id = 1053SELECT * FROM teans WHERE id = 1117:Useiwneitsoleyselect * fron playbooks where id = 5473;select * fron pl avbook carecortes where 10 = 457855fron playbook_categories where playbook_id = 5473;crnFelds where 10 = 655742select * fron crn_field_values where crn_field_id = 659242;=725727_729SELECT * FROM crn_field_data fd#JOTN con Sields & ON fd.oon Sield 3d = 4,3JOIN activities a ON fd.activity_id = a.idHERE aCTMITY 0= 13935459M AND f cen neavider id = 'he activity tvne":8 Shelved versionnaresoace Nasony Cemvscas. Hastuse Carbon\Carbon;use Illuminate|Support\Facades\Log;ues hesoou rosnonent tere Tonetante.use Jininny\Jobs\Mailbox\EnailTextRelay:use Jininny Models TextRelayuse 600gle\Service\Gmail as GoogleGnait;public function sync(): arraySnailbox = config('jininny.google_text_user');SexpectedAlias = config('jininny.deploy_region') a== 'eu' ? 'catch-all-eu'] : "catch-at?":SexpectedHost = config('jininny.google_text_host');Sservice = Sthis->getService(Snailbox);SnessageHistory = Sthis->getHistory(Sservice);00%L78• Tue 26 May 10:06:55U TextRelayServiceTest~DCascadeumhonerhehyorMEEE+0.Pehoee All tens are now pussint consistenty. Leume create a summahy of wist wos sccom plisncoTwtoweroeuccessuivaodtcmdrxtoI successfully added comprehensive test coverage for the enhanced logging changes in TextRelayService and fixed allisoues Fixed1. Incorrect Gmail Class Names: Fixed CooglelService\GeailVMistoryffessage to CooglelService\Geu11\MistoryfessageAddrd and uodated method calls from eessace procerty to gettessaoelo method2. Complex Model Mocking Issues: Simplified the test approach by using partial mocks instead of complex allas/overloadmockina that was causing contuets with existing classes3. Missing Log Expectations: Added proper Log facade expectations for all logging scenarios including error [EMAIL] Cowcrnac Addind,New Test Methods:"testoynone chichnonccolody anole lusts stortono comoleton loos wioh propor conkoa(> Code$ Adaptive• C13 differencesMndaur Tosme 720116 lITS.9...
|
71760
|
NULL
|
NULL
|
NULL
|
|
71760
|
2573
|
63
|
2026-05-26T07:06:35.650243+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779195650_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomEV favscojs ~$phpunit.xmlToraw_sqL.query.sq rapstomEV favscojs ~$phpunit.xmlToraw_sqL.query.sqlM: README.md0 sonar-project.properties=test.py<> Untitled Diagram.xmluvelur.connorMEWEBHOOK_FILTERING_IMPLEMENTATION.md>1h extemal Librane:v Scratches and Consolesy Daabise consolesY AEU# consolc EuA DEAL RISKS (EUJAOUEUA EU (EUJlliminnycco. nogiisuminny alocal nesA HS,local (jiminny@localhost)2 SF Liminny@localhost)A zoho_dev (jiminny@localhost)V & PRODA console (PROD]console 1rpgonlm rpponch@€> text relay changes 2 files. Mon› Owner resolver changes 8 files, Mon>Svnc Playbook categories and crm field values 10 files, 21.05.26. 14:04› Scim provisioning with rioles 8 flles, 19.05.26, 11:06CnownerKesowenes.oo.› JY-20891 SMS Text relay EU 3 files, 15.05.26, 14:05> JD & SendReportiob 2 fles. [IP_ADDRESS]:39reorder callsne.1h.[IP_ADDRESS]> Move non paginator search calle to clentes cearch 1 fle, 11.05.26, 10:07•Almost ready one call to be tested 8 flies, 8.05.26, 14:54•huospot rate limit temo checkous17168. 8.051226.11:48saved search delete 1 file, 27.04.26, 14:58exoiration mescnoe 6 flcs.[IP_ADDRESS]%•UP dev logs 1 fle, 24.04.26, 12:02user transformer 3 ffles, 23.04.26, 15:43omalllrosdv 0 1ioe 22/012810721Email on not sent 5 files, 21.04.26, 19:38Oianhst tradlintoroet ovont 2166 2110428.12-09• UP tracking controller 1 file, 20.04.26, 9:22› Don't send it no s3 path (fix update to faled) 1 file, 17.04.26, 12:56> FEATURE CHANGES 3 files, 6.04.26, 13:40CodeCaravelketactoOOl-wincow©TextRelayService.php? 'catch-all-cua' . config("fininny.googletext host"))carch-alld. contalntainny.goog.e.texthost$service = Sthis->getService(Snailbox);SmessageHistory = Sthis->getHistory(Sservice):messade ids nforeach (SnessageHistory as Shistories) (Snessages = Shistories->messagesAdded;foreach (Snessages as Snessage) (Snessageld = Snessage-›nessage->id;if (! Sthis->isForCurrentEnvironment(Sservice, Snailbox, Snessageld, Sexpected0riginalRecipient)) {concanveSrelayedText = TextRelay: :where('enail_provider_id', Snessageld)->firstO:if (SrelayedText aa= null) €SrelavedText = TextRelay::createCl'enail_provider' => TextRelay: :PROVIDER_GSUITE,"Paeroworrsssoo0oDo not lgnore -Highighe words +B€?obase version aoo/Services/Mamextkelavserv.ce.ohocohonanAsnace Esooy Saryscac Wamuse Carbon\Carbon;-use Jiminny|Component\Queue|Constants;ues esoov Hione ashorTeytcalave—use Jiminny |Models\TextReLay:_use Google|Service\Gnail as Go0gleGmail;public function sync(): arraySnailbox = config('jininny.900gle_text_user');Sexpected0riginalRecipient = config('jininny.deploy_region') aa= 'eu'? 'catch-all-eue" . config('jiminny.google_text_host'): 'catch-all@'. config('jiminny.google_text_host');Sservice = Sthis-›getService(Snailbox):SmessageHistory = Sthis-›getHistory(Sservice);ATA10AYE customlogA console (EU)E laravelJog4 SF [iminny@localhost)A console (STAGING]A HS Jocal (jiminny@localhost)CONCAT(U.1d, SASE MHEN U.1d = t.ouner_1d THEN • (onner)• ELSE •0458184Y00 4711712717721723725727U.chostsa.*,t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.team_icWHERE u.tean_id = 1117 and sa.provider = 'hubspot":SELECT * FROM activities WHEREuuid_to_bin(^8024fffb-2df7-4017-91f4-d9f896050248') = uuirSELECT * FROM activities NHERE vuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c*) =SELECT * FROMcrn configurations WHERE id = 1053SELECT * FROM teans WHERE id = 1117:Useiwneitsoleyselect x fron playbooks where id = 5473;select * fron plavbook carecortes where 10 = 457855fron playbook_categories where playbook_id = 5473;crnFelds where 10 = 655742select * fron crn_field_values where crn_field_id = 659242;729SELECT * FROM crn_field_data foJouk con flelds +owto.conteldidetJOIN activities a ON fd.activity_id = a.idHERE aCTMITY 0= 13935459M AND f cen neavider id = 'he activity tvne":8 Sheived versionnaresoace Nasony Cemvscas. Hastuse Carbon\Carbon;use Illuminate|Support\Facades\Log;ues hesoou rosnonent tere Tonetante.use Jininny\Jobs\Mailbox\EnailTextRelay:use Jininny Models TextRelayuse 600gle\Service\Gmail as GoogleGnait;public function sync(): arraySnailbox = config('jininny.google_text_user');SexpectedAlias = config('jininny.deploy_region') a== 'eu' ? 'catch-all-eu'] : "catch-at?":SexpectedHost = config('jininny.google_text_host');Sservice = Sthis->getService(Snailbox);SnessageHistory = Sthis->getHistory(Sservice);00%L78• Tue 26 May 10:06:35U TextRelayServiceTest~DCascadeumhonerhehyorMEEE+0.Pehoce All tens are now pussint consistehuy. Leume create & summany of wist wos occom pllsncoTwtoweroeuccessuivaodtcmdrxtoI successfully added comprehensive test coverage for the enhanced logging changes in TextRelayService and fixed alliscues Fixed1. Incorrect Gmail Class Names: Fixed CooglelService\GeailVMistoryffessage to CooglelService\Geu11\MistoryfessageAddrd and uodated method calls from eessace procerty to gettessaoelo method2. Complex Model Mocking Issues: Simplified the test approach by using partial mocks instead of complex allas/overloadmockina that was causing contuets with existing classes3. Missing Log Expectations: Added proper Log facade expectations for all logging scenarios including error [EMAIL] Cowcrnac Added,New Test Methods:"testoynone chichnonccolody anole lusts stortono comoleton loos wioh propor conkoao Code$ Adaptive• C13 differencesMndaur Tosme 720116 lITS.9...
|
NULL
|
4751801640411536155
|
NULL
|
visual_change
|
ocr
|
NULL
|
rapstomEV favscojs ~$phpunit.xmlToraw_sqL.query.sq rapstomEV favscojs ~$phpunit.xmlToraw_sqL.query.sqlM: README.md0 sonar-project.properties=test.py<> Untitled Diagram.xmluvelur.connorMEWEBHOOK_FILTERING_IMPLEMENTATION.md>1h extemal Librane:v Scratches and Consolesy Daabise consolesY AEU# consolc EuA DEAL RISKS (EUJAOUEUA EU (EUJlliminnycco. nogiisuminny alocal nesA HS,local (jiminny@localhost)2 SF Liminny@localhost)A zoho_dev (jiminny@localhost)V & PRODA console (PROD]console 1rpgonlm rpponch@€> text relay changes 2 files. Mon› Owner resolver changes 8 files, Mon>Svnc Playbook categories and crm field values 10 files, 21.05.26. 14:04› Scim provisioning with rioles 8 flles, 19.05.26, 11:06CnownerKesowenes.oo.› JY-20891 SMS Text relay EU 3 files, 15.05.26, 14:05> JD & SendReportiob 2 fles. [IP_ADDRESS]:39reorder callsne.1h.[IP_ADDRESS]> Move non paginator search calle to clentes cearch 1 fle, 11.05.26, 10:07•Almost ready one call to be tested 8 flies, 8.05.26, 14:54•huospot rate limit temo checkous17168. 8.051226.11:48saved search delete 1 file, 27.04.26, 14:58exoiration mescnoe 6 flcs.[IP_ADDRESS]%•UP dev logs 1 fle, 24.04.26, 12:02user transformer 3 ffles, 23.04.26, 15:43omalllrosdv 0 1ioe 22/012810721Email on not sent 5 files, 21.04.26, 19:38Oianhst tradlintoroet ovont 2166 2110428.12-09• UP tracking controller 1 file, 20.04.26, 9:22› Don't send it no s3 path (fix update to faled) 1 file, 17.04.26, 12:56> FEATURE CHANGES 3 files, 6.04.26, 13:40CodeCaravelketactoOOl-wincow©TextRelayService.php? 'catch-all-cua' . config("fininny.googletext host"))carch-alld. contalntainny.goog.e.texthost$service = Sthis->getService(Snailbox);SmessageHistory = Sthis->getHistory(Sservice):messade ids nforeach (SnessageHistory as Shistories) (Snessages = Shistories->messagesAdded;foreach (Snessages as Snessage) (Snessageld = Snessage-›nessage->id;if (! Sthis->isForCurrentEnvironment(Sservice, Snailbox, Snessageld, Sexpected0riginalRecipient)) {concanveSrelayedText = TextRelay: :where('enail_provider_id', Snessageld)->firstO:if (SrelayedText aa= null) €SrelavedText = TextRelay::createCl'enail_provider' => TextRelay: :PROVIDER_GSUITE,"Paeroworrsssoo0oDo not lgnore -Highighe words +B€?obase version aoo/Services/Mamextkelavserv.ce.ohocohonanAsnace Esooy Saryscac Wamuse Carbon\Carbon;-use Jiminny|Component\Queue|Constants;ues esoov Hione ashorTeytcalave—use Jiminny |Models\TextReLay:_use Google|Service\Gnail as Go0gleGmail;public function sync(): arraySnailbox = config('jininny.900gle_text_user');Sexpected0riginalRecipient = config('jininny.deploy_region') aa= 'eu'? 'catch-all-eue" . config('jiminny.google_text_host'): 'catch-all@'. config('jiminny.google_text_host');Sservice = Sthis-›getService(Snailbox):SmessageHistory = Sthis-›getHistory(Sservice);ATA10AYE customlogA console (EU)E laravelJog4 SF [iminny@localhost)A console (STAGING]A HS Jocal (jiminny@localhost)CONCAT(U.1d, SASE MHEN U.1d = t.ouner_1d THEN • (onner)• ELSE •0458184Y00 4711712717721723725727U.chostsa.*,t.ouner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.team_icWHERE u.tean_id = 1117 and sa.provider = 'hubspot":SELECT * FROM activities WHEREuuid_to_bin(^8024fffb-2df7-4017-91f4-d9f896050248') = uuirSELECT * FROM activities NHERE vuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c*) =SELECT * FROMcrn configurations WHERE id = 1053SELECT * FROM teans WHERE id = 1117:Useiwneitsoleyselect x fron playbooks where id = 5473;select * fron plavbook carecortes where 10 = 457855fron playbook_categories where playbook_id = 5473;crnFelds where 10 = 655742select * fron crn_field_values where crn_field_id = 659242;729SELECT * FROM crn_field_data foJouk con flelds +owto.conteldidetJOIN activities a ON fd.activity_id = a.idHERE aCTMITY 0= 13935459M AND f cen neavider id = 'he activity tvne":8 Sheived versionnaresoace Nasony Cemvscas. Hastuse Carbon\Carbon;use Illuminate|Support\Facades\Log;ues hesoou rosnonent tere Tonetante.use Jininny\Jobs\Mailbox\EnailTextRelay:use Jininny Models TextRelayuse 600gle\Service\Gmail as GoogleGnait;public function sync(): arraySnailbox = config('jininny.google_text_user');SexpectedAlias = config('jininny.deploy_region') a== 'eu' ? 'catch-all-eu'] : "catch-at?":SexpectedHost = config('jininny.google_text_host');Sservice = Sthis->getService(Snailbox);SnessageHistory = Sthis->getHistory(Sservice);00%L78• Tue 26 May 10:06:35U TextRelayServiceTest~DCascadeumhonerhehyorMEEE+0.Pehoce All tens are now pussint consistehuy. Leume create & summany of wist wos occom pllsncoTwtoweroeuccessuivaodtcmdrxtoI successfully added comprehensive test coverage for the enhanced logging changes in TextRelayService and fixed alliscues Fixed1. Incorrect Gmail Class Names: Fixed CooglelService\GeailVMistoryffessage to CooglelService\Geu11\MistoryfessageAddrd and uodated method calls from eessace procerty to gettessaoelo method2. Complex Model Mocking Issues: Simplified the test approach by using partial mocks instead of complex allas/overloadmockina that was causing contuets with existing classes3. Missing Log Expectations: Added proper Log facade expectations for all logging scenarios including error [EMAIL] Cowcrnac Added,New Test Methods:"testoynone chichnonccolody anole lusts stortono comoleton loos wioh propor conkoao Code$ Adaptive• C13 differencesMndaur Tosme 720116 lITS.9...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71759
|
2573
|
62
|
2026-05-26T07:06:34.333508+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779194333_m2.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Console/Commands 1 file, folder not checked...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.50764626,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.5319149,"top":0.15802075,"width":0.047872342,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.4870346,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.4956782,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.13328013,"width":0.03025266,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.22106944,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.273743,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":4,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"}]...
|
-6570075751426804463
|
6526393645355480388
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Console/Commands 1 file, folder not checked...
|
71757
|
NULL
|
NULL
|
NULL
|
|
71758
|
2572
|
25
|
2026-05-26T07:06:34.230401+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779194230_m1.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
text relay changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"text relay changes","depth":2,"value":"text relay changes","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3366492416316151407
|
-1875207899952763593
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
text relay changes...
|
71756
|
NULL
|
NULL
|
NULL
|
|
71756
|
2572
|
24
|
2026-05-26T07:06:31.193413+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779191193_m1.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
2 modified
text relay changes
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"text relay changes","depth":2,"on_screen":true,"value":"text relay changes","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
-5466456520277084284
|
8554246992515467563
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
2 modified
text relay changes
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71757
|
2573
|
61
|
2026-05-26T07:06:31.090952+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779191090_m2.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
2 modified
text relay changes
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.51329786,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.5375665,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.49268618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.5013298,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.13328013,"width":0.03025266,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.22106944,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.273743,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":4,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":5,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.32446808,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"text relay changes","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.3238032,"height":0.087789305},"on_screen":true,"value":"text relay changes","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.5711436,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13 differences","depth":1,"bounds":{"left":0.54920214,"top":0.41181165,"width":0.02925532,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.022606382,"height":0.013567438},"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.3118351,"height":0.57302475},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.31349733,"height":0.57302475},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.50764626,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8411883431662041070
|
8554247129954421035
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
2 modified
text relay changes
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71755
|
2572
|
23
|
2026-05-26T07:06:29.817004+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779189817_m1.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
2 modified
text relay
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"text relay","depth":2,"on_screen":true,"value":"text relay","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
1980359709648360820
|
8554246992515467563
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
2 modified
text relay
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
71753
|
NULL
|
NULL
|
NULL
|
|
71754
|
2573
|
60
|
2026-05-26T07:06:29.297186+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779189297_m2.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
2 modified
text rel
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.51329786,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.5375665,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.49268618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.5013298,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.13328013,"width":0.03025266,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.22106944,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.273743,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":4,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":5,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.32446808,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"text rel","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.3238032,"height":0.087789305},"on_screen":true,"value":"text rel","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.5711436,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13 differences","depth":1,"bounds":{"left":0.54920214,"top":0.41181165,"width":0.02925532,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.022606382,"height":0.013567438},"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.3118351,"height":0.57302475},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.31349733,"height":0.57302475},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.50764626,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"bounds":{"left":0.5355718,"top":0.65363127,"width":0.044215426,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"bounds":{"left":0.39960107,"top":0.13248204,"width":0.035904255,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
2283190590874242328
|
8554246992515467531
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
2 modified
text rel
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
71752
|
NULL
|
NULL
|
NULL
|
|
71753
|
2572
|
22
|
2026-05-26T07:06:28.765111+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779188765_m1.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
2 modified
text re
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"text re","depth":2,"on_screen":true,"value":"text re","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
4061640194644273241
|
8554246923795990827
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
2 modified
text re
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71752
|
2573
|
59
|
2026-05-26T07:06:28.663644+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779188663_m2.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
2 modified
text re
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.51329786,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.5375665,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.49268618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.5013298,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.13328013,"width":0.03025266,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.22106944,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.273743,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":4,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":5,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.32446808,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"text re","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.3238032,"height":0.087789305},"on_screen":true,"value":"text re","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.5711436,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13 differences","depth":1,"bounds":{"left":0.54920214,"top":0.41181165,"width":0.02925532,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.022606382,"height":0.013567438},"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.3118351,"height":0.57302475},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.31349733,"height":0.57302475},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.50764626,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"bounds":{"left":0.5355718,"top":0.65363127,"width":0.044215426,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"bounds":{"left":0.39960107,"top":0.13248204,"width":0.035904255,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
4061640194644273241
|
8554246923795990827
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
2 modified
text re
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71751
|
2573
|
58
|
2026-05-26T07:06:27.722563+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779187722_m2.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
TextRelayServiceTest.php, class checked
.env.local not checked
2 modified
tex
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.51329786,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.5375665,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.49268618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.5013298,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.13328013,"width":0.03025266,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.22106944,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.273743,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":4,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":4,"bounds":{"left":0.27094415,"top":0.22106944,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":4,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":5,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":4,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"bounds":{"left":0.27094415,"top":0.273743,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.32446808,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"tex","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.3238032,"height":0.087789305},"on_screen":true,"value":"tex","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.5711436,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13 differences","depth":1,"bounds":{"left":0.54920214,"top":0.41181165,"width":0.02925532,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.022606382,"height":0.013567438},"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.3118351,"height":0.57302475},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.31349733,"height":0.57302475},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.50764626,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"bounds":{"left":0.5355718,"top":0.65363127,"width":0.044215426,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"bounds":{"left":0.39960107,"top":0.13248204,"width":0.035904255,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-7545231348759451557
|
8554246993589209387
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
TextRelayServiceTest.php, class checked
.env.local not checked
2 modified
tex
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
71749
|
NULL
|
NULL
|
NULL
|
|
71750
|
2572
|
21
|
2026-05-26T07:06:27.617451+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779187617_m1.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
TextRelayServiceTest.php, class checked
.env.local not checked
2 modified
tex
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"tex","depth":2,"on_screen":true,"value":"tex","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
-7545231348759451557
|
8554246993589209387
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
TextRelayServiceTest.php, class checked
.env.local not checked
2 modified
tex
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
71748
|
NULL
|
NULL
|
NULL
|
|
71749
|
2573
|
57
|
2026-05-26T07:06:25.830205+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779185830_m2.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
TextRelayServiceTest.php, class checked
.env.local not checked
2 modified
Owner resolver changes
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.51329786,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.5375665,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.49268618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.5013298,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.13328013,"width":0.03025266,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.22106944,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.273743,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 2 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.037898935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":4,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services/Mail 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.1859537,"width":0.054853722,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":4,"bounds":{"left":0.27094415,"top":0.22106944,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":4,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":5,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":4,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"bounds":{"left":0.27094415,"top":0.273743,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.32446808,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Owner resolver changes","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.3238032,"height":0.087789305},"on_screen":true,"value":"Owner resolver changes","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.5711436,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13 differences","depth":1,"bounds":{"left":0.54920214,"top":0.41181165,"width":0.02925532,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.022606382,"height":0.013567438},"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.3118351,"height":0.57302475},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'\n ? 'catch-all-eu@' . config('jiminny.google_text_host')\n : 'catch-all@' . config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n $messageIds[] = $messageId;\n }\n }\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedOriginalRecipient\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalRecipient = $header->value;\n\n return $originalRecipient === $expectedOriginalRecipient;\n }\n }\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.31349733,"height":0.57302475},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.50764626,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"bounds":{"left":0.5355718,"top":0.65363127,"width":0.044215426,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"bounds":{"left":0.39960107,"top":0.13248204,"width":0.035904255,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
4787948003936274214
|
8554247129954421003
|
visual_change
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
5 files, folder partially checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 2 files, folder partially checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
Console/Commands 1 file, folder not checked
Services/Mail 1 file, folder checked
TextRelayService.php, class checked
TextRelayService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
TextRelayServiceTest.php, class checked
.env.local not checked
2 modified
Owner resolver changes
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
13 differences
1aee7aad
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedOriginalRecipient = config('jiminny.deploy_region') === 'eu'
? 'catch-all-eu@' . config('jiminny.google_text_host')
: 'catch-all@' . config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedOriginalRecipient)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
$messageIds[] = $messageId;
}
}
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedOriginalRecipient
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalRecipient = $header->value;
return $originalRecipient === $expectedOriginalRecipient;
}
}
} catch (\Exception $e) {
\Sentry::captureException($e);
}
return false;
}
}
Current version
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Shelve Changes
Shelve Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71748
|
2572
|
20
|
2026-05-26T07:06:24.901261+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779184901_m1.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false}]...
|
-5584885479912327618
|
-8343513941599469120
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp100% C8• Tue 26 May 10:06:24T81APP (-zsh)APP (-zsh)DOCKER881use PHPUnit\Framework Attributes\CoversClass;use PHPUnit\Framework\Attributes\DataProvider;-346,7 +342,7 0DEV (-zsh)$82// Create a partial mock of the service to control the getHistory methodSservice = Sthis->createPartialMock(TextRelayService::class, ['getHistory']);screenpipe"-zsh+APP// Mock getHistory to return empty array (no messages to process)Sservice->method('getHistory')->willReturn([]);@@-389,7+385,700// Create a partial mock of the service to control the getHistory methodSservice = Sthis->createPartialMock(TextRelayService::class, ['getHistory']);+// Mock getHistory to returnempty array (no messages to process)Sservice->method('getHistory')->willReturn((J);end diffFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] (master) $ I...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71747
|
2573
|
56
|
2026-05-26T07:06:24.798495+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779184798_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;
Project
Project
New File or Directory…...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.44980052,"top":0.07581804,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"bounds":{"left":0.45910904,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.47041222,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.47772607,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.48636967,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.4950133,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.50598407,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.51462764,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.52327126,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.53424203,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.54521275,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.5718085,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5827792,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"bounds":{"left":0.6761968,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68849736,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.6978058,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"bounds":{"left":0.7094415,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5924424429820031518
|
2218635329548990031
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;
Project
Project
New File or Directory…...
|
71746
|
NULL
|
NULL
|
NULL
|
|
71745
|
2572
|
19
|
2026-05-26T07:06:23.116345+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779183116_m1.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7032381536525887672
|
-6434304657575212661
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute...
|
71744
|
NULL
|
NULL
|
NULL
|
|
71746
|
2573
|
55
|
2026-05-26T07:06:23.003951+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779183003_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5584885479912327618
|
-8343513941599469120
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
rapstomCoocCarevelKetdcioOOl-wincowFV faVsco.|s ~<>phpunit.xmlraw-sal-query.solW.RSADME mal0 sonar-proiect.properties=test.py<> Untitled Diagram.xmluvelur.connorM: WEBHOOK FILTERING.IMPLEMENTATION.mo>1h extemal Librane:v Scratches and Consolesy Databise consolesY AEU# consolc Eu585848438&542.closs Texckeldyservach41A10AYLDEAL RISKS EUAOUEUAEU TEUlliminnycco.nosconsole liminnuc ces nosisuminny alocal nesHwswwealtminnweooanosh2 SF Liminny@localhost)zond dav tim nowe des nochlV & PRODconsole pronlconsole 1rpgonlm rppon4328LocalennoosChandes S tas wodatine.anu local aoc@ JiminnyDebugCommand.php app/Console/Commandsphe logging.php config© TextRelayService.php apo/Services/MaiC TextRelayServiceTest.pho tests/Unit/Services/Mail> Unversioned Files 9 files, updatina.puoere tunceion synces. arroySexpectedAlias = config( key. "fiminny.deploy region") === 'cu" ? 'catch-all-cu' : 'catch-all*=sexoecredhost= contralkex. "11ainny.goo0lgtexthosthSservice = sthis-xoerSerycelscar box.messadelistory "Thisoscerthstorvlseruscomessade ids"Lnar ntot messsoe'[TextRelayService) Starting sync', ["nallbox" => Smailbox,'expected_allas => SexpectedAlias,aynected hostl es Seynecherhost)foreach (SnessageHistory as Shistories)Snassades = Shistontes-s-accacpeAAdenforeach (Snessages as Smessage)Snessageld = Smessage-›message->id:if ( Sthis->isForCurnentEnvironment(Sservice, Smailbox, Snessageld, SexpectedAlias, SexpectedHost)) «conenueSrelavedText = TextRelay::ahere( column'emailprovider id'. Snessageld)->firsto,T{ 8 + → 0 Side-by-side viewer -81aee7aad app/Services/Mail/TextRelayService.phg@ <?phpDo not ignoreHighllght words -X15 € ?mnesonee uinny Semwtocsiduse wichon airdonsuse Jiminny\Component|Queue\Constants:use Jininny Jobs nazlbox \enaztTextrelay:_use Jiminny Hodels Textrelay:luse lanndle Seruncel smantas Sood esrarenubilic Cunctiion synclolearravSaiiboy contaldsiminny aoogie teyt usen')aSaynectednooinalPecinient=confalchninnv.deniov cecson'eep"eul? 'catch-all-euf' . confiq(siminny.google text host')):"catch-alla' . confia("ininny.google text host):CCnAulAe GihIORNAAtCAANSACEROSThAWSmessageHistory = Sthis->getHistory(Sservice)customuolaraveio"Sf- uminnylocalhost& HS_Jocal (jiminny@localhostA console (EUA console [STAGINGCONCAT(U.1d, CASE WHEN u.1d = t.ouner 1d THEN • (onnen)• Fise8458184Y004711U.chostsa.*t.ouner_id FROM social_accounts stJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.team_icWHERE u.tean_id = 1117 and sa.provider = 'hubspot':SELECT * FROM actávities WHEREuuidto_ bin(*8924fffb-2df7-4017-91f4-d9f896850248') = uufSELECT * FROM actávities WHERE uuid to bin([CREDIT_CARD]-927f-4f4da2a8185c') = uusSELECT * FROMcrn configurations WHERE id = 1053SELECT * FROM teans WHERE id = 1117:=721Useiwneitsoleyplaybooks where id= 5473* tron plavbook carecortes where 10 = 457855=723plavoook carecortes where playoook1d = 54055725conFelds where 10= 659242select * fron crn. field_values where crn field id = 659242,7271 728729SELECT * FROM con 6ield data €.#JOTN con Sields & ON fd.oon Sield 3d = 4,3# JOIN activities a ON fo.activity_1d = a.1dHERE aCTMITy 0=19333459WAMD € con orowiden $a = the actiuity tune!,eurrent versioncrohrmnesonee uminny senwtcee ahruse eachonl winhonsuse seeluinare sunnortFachdesllLoosuseminnyornanent "aueue Congtantsuse_Jininnvl.lohs|Mailbox)Enail TextRelav,use minny ndeus tieytrelayuse Google\Service\Gmail as GoogleGnailpublic function synco: accasSmastlhoyconta-sninny.concile teyt usentSexpectedAlias = confia('iminny.deploy region' === 'eu' ? 'catch-all-eu" : 'catch-all':SexpectedHost = confia('iminny.google text host')SmessageHistony = Sthis->getHistory(Sservice)00%L7*• Tue 26 May 10:06:22TextRelayServiceTestumhonerhehyor+0.Pehooe Alltens are now pussint consistehuy, Loume create & sumimahy of wist wos dccom plisncoTwtoweroeuccessuivaodtcmdrxtoweceenwnodon.commtahaneiuathetcauraoatorthaaohooenoll0oc.nochhoohe/tAYtRATwCarscatndmmac.nliscues Fixed1. Incorrect Gmail Class Names: Fixed GooglelService\GnailVMistoryMessage to GooglelService\Gna1l\HistoryMessageAddrd and uodated method calls from eessace property to cetlessagelo method2. Complex Model Mocking Issues: Simplified the test approach by using partial mocks instead of complex alias/overloadmockina that was causing contuets with existing classes3. Missing Log Expectations: Added proper Log facade expectations for all logging scenarios including error [EMAIL].ndexorc100ns1wirechoetniurtsTest Cowcrnac Addind,Naw Tact Methode"tes tynone chichnonccolo angl e losts stortano comciedon loos wioh propor contoa(> CodeAdsoti• C12 ditoroncodNUMMeur Tosme 720116 LITE A...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71744
|
2572
|
18
|
2026-05-26T07:06:21.862610+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779181862_m1.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1264749411961195938
|
2218635329548990031
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71743
|
2573
|
54
|
2026-05-26T07:06:20.184709+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779180184_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.44980052,"top":0.07581804,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"bounds":{"left":0.45910904,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.47041222,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.47772607,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.48636967,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.4950133,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.50598407,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.51462764,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.52327126,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.53424203,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.54521275,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.5718085,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5827792,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"bounds":{"left":0.6761968,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68849736,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.6978058,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"bounds":{"left":0.7094415,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1264749411961195938
|
2218635329548990031
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
71741
|
NULL
|
NULL
|
NULL
|
|
71742
|
2572
|
17
|
2026-05-26T07:06:20.080258+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779180080_m1.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6104515030951770378
|
-6434304657571018357
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History...
|
71740
|
NULL
|
NULL
|
NULL
|
|
71741
|
2573
|
53
|
2026-05-26T07:06:16.044239+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779176044_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7429716278976468786
|
-8636355650190325311
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
PhpStormCoocCaravelKetdcioOOl-winooFV faVsco.|s ~Proinet v© TextRelayServiceTest.phg<>phpunit.xmlraw-sal query.saW.RSADME mal0 sonar-project.properties=test.py<> Untitled Diagram.xmlisvetur.config.isM: WEBHOOK FILTERING.IMPLEMENTATION.mocloss Texckeldyservach41410Apuoere tunceion synces. arroySexpectedAlias = config( key. "fiminny.deploy region") === 'cu" ? 'catch-all-cu' : 'catch-all*=>1h extemal Librane:v Scratches and Consolesy Daabise consolesY AEU# consolc EuA DEAL RISKS (EUAOIEUAEU TEUlliminnycco. nogiisuminny alocal nesHwswcalltminnueoosmog2 SF [EMAIL] dav tim nowe des nochlV ORosconsale pronconsole 1rpgonlm rpponASE8S6LocalennoosChoe y> Owner resolver chances & files. M.> Svnc Playbook categories and crm field values 10 files, 21.05.26, 14:04> Scim provisioning with rioles 8 files, 19.05.26, 11:06> CrmOwnerResolver 2 files, 19.05.26, 11:05> JY-20891 SMS Text relay EU 3 files, 15.05.26, 14:05> JD & SendReportJob 2 files, 13.05.26, 12-38reorder callsme.1h.05.26.10.09Move non osg nator search calls to centosseerchmie.11.05-26.110-07Almost resdy one csll to de tested 81148. 8.05-23.14054> hubspot rate limit temo checkout 17 Fes. 8.05.26. 11:48saved search delete 1anie.[IP_ADDRESS]-53expiration meseage 6 flas. [IP_ADDRESS]-22UP devloos 1 nic. [IP_ADDRESS]:0%• tem test 1 file, 24.04.26, 10:36user transtormer 3. flos. [IP_ADDRESS]:46email ready 9 fles. 23.04.26. 15:31smaltlon not cont iloe 210424 10-20Planhat track interest event 2 files, 21.04.26, 13:56To tractina contrallor 116 200428.072› Don't send it no s3 path (fix update to faled) 1 file, 17.04.26, 12:56JCCAMTOS CHANGES 21106 R0A28 12310web.php fix router 1 file, 1.04.26, 9:23sexoecredhost= contralkex."11ainny.googlc.texthostSservice = sthis-xoerSerycelscar boxmessadehistory Thisoscertistorylseryscomessade idsnLnar ntot messsoe*[TextRelayService) Starting sync', ['nallbox" => Smailbox)'expected_allas => SexpectedAlias,aynected host"SeynecrerHOSTforeach (SnessageHistory as Shistories)Snassades = Shistonfes--accadpc/Adedforeach (Snessages as Smessage)Snessageld = Smessage-›message->id:if ( Sthis->isForCurnentEnvironment(Sservice, Smailbox, Snessageld, SexpectedAlias. SexpectedHost)) <SrelavedText = TextRelay::ahere( columaorowdenohesacoooeDo not ignoreX13 €obase version apouse Jiminny|Renositonies|Tea-RepositoryalISA NEonY CTHAS TIASDATARAnCAYuse esony nles tutranchonallnav.ay thanptnleusA Nesoov Samys CAcyeMirSanusCAsuse Jiminny Services\Cra\ProviderRegistry:uea nesoov Samyscaelo aumachCanuscasuse Jiminny Services \UserService:uesls.oov.WillCande.tocvhonassodterwtwSo.nehlrasthnn// Sync the opportunity with the latest data if possible.if (Sactivity->opportunity_id) ‹try {ScrnService = Sthis->providerRegistry-›get($team->crm-›provider)if (a Suser->istr-Requiredo)ScrnService-›setliser(Stean->getOnnerOD:else icwwoseuseneusenh00%L7oo lue covdy Tubore+0.customuolaravei.oSf- uminnylocalhost& HS_Jocal (jiminny@localhostA console (EUA console [STAGINGumhonerhehyorCONCAT(u.id, CASE WHEN u.id = t.ouner_id THEN • (owner)* ELSE ** END) AS user_10U.chostsa.*t.ouner_id FROM social_accounts stJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.team_icWHERE u.tean_id = 1117 and sa.provider = 'hubspot':SELECT * FROM actávities WHEREuuid to bin(*8924fffb-2df7-4017-91f4-d9f896850248') = uufSELECT * FROM actávities WHERE uuid to bin(*[CREDIT_CARD]-927f-4f4da2a8185c') =SELECT * FROMcrn configurations WHERE id = 1053SELECT * FROM teans WHERE id = 1117:Useiwneitsoley=721ron pl avbook carecortes where 10 = 457855=723plavoook carecortes where playoook1d = 54055725crnFelds where 10 = 655742select * fron con. field values where crn Sield id = 659242,729SELECT * FROM crn_field_data foJouk con flelds +owto.conteldidet# JOIN activities a ON fd.activity_id = a.idHERE aCTMITY 0= 13935459WAMD € con orowiden $a = the actiuity tune!,8 Shelved versionOLerSTAPiRuse Jiminny\Repositonies\TeanRenository,uce Smonv Puiles yioareference.use.KeinnvlPules|Mult3ainenstona1AccavMaxCharPuleuea eonv Sanusicas.ervitSanusca.use Jiminny|Services\Activity\Crm0wnerResolveruse Jiminny Serváces\Cra\ProviderRegistry:use Jiminny Serváces PlaybackService:use Jiminny Serváces UserService;use Jiminny\V0\Repository\OnDemandActivitySearch\Criteria1171171171// Sync the opportunity with the latest data if possibleif (Sactivity->opportunity_id) €SintegrationAdmin = Suser->isêrRequiredo) ? Suser : Stean->getOwnerO>ScrnResolver = app(CredunerResolver::class.fE SintearatontaaiaproviderStua' a> Stean-soetCrnConfiguration@->getProviderNameo)D:Pehoce All tens are now pussing consistenuy, Loume create a summahy of niat wos occom plisncotwtowerosuccessuiv.ndoedtimorxtweceenwnodon.commtahaneiuathetcauraoatorthaaohooenoll0oc.nochhoohe/tAYtRATwCarscatndmmac.nliscues Fixed1. Incorrect Gmail Class Names: Fixed GooglelService\GnailVMistoryMessage to GooglelService\Gna1l\HistoryMessageAsdrd and uodated method calls from sessage property to cetMessaoelo method2. Complex Model Mocking Issues: Simplified the test approach by using partial mocks instead of complex alias/overloadmockina that was causing contucts with existing classes3. Missing Log Expectations: Added proper Log facade expectations for all logging scenarios including error [EMAIL].ndexorc100ns1wirechoetniurtsTest Cowcrnac Added,Naw Tact Methode•testSyncWithEnhancedLogging() - Tests start and completion logs with proper contexto CodeAdsoti2 differences700116 LITE9 ensAo...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71740
|
2572
|
16
|
2026-05-26T07:06:12.447284+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779172447_m1.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp$0.APP (-zsh)APP (-zsh)DOCKER₴81use PHPUnit\Framework Attributes\CoversClass;use PHPUnit\Framework\Attributes\DataProvider;-346,7 +342,7 0DEV (-zsh)$82// Create a partial mock of the service to control the getHistory methodSservice = Sthis->createPartialMock(TextRelayService::class, ['getHistory']);screenpipe"864100% C8• Tue 26 May 10:06:12-zshT₴1*5 |++APP// Mock getHistory to return empty array (no messages to process)Sservice->method('getHistory')->willReturn([]);@@-389,7+385,700// Create a partial mock of the service to control the getHistory methodSservice = Sthis->createPartialMock(TextRelayService::class, ['getHistory']);+// Mock getHistory to returnempty array (no messages to process)Sservice->method('getHistory')->willReturn((J);end diffFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] (master) $ I...
|
NULL
|
637318246649365683
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp$0.APP (-zsh)APP (-zsh)DOCKER₴81use PHPUnit\Framework Attributes\CoversClass;use PHPUnit\Framework\Attributes\DataProvider;-346,7 +342,7 0DEV (-zsh)$82// Create a partial mock of the service to control the getHistory methodSservice = Sthis->createPartialMock(TextRelayService::class, ['getHistory']);screenpipe"864100% C8• Tue 26 May 10:06:12-zshT₴1*5 |++APP// Mock getHistory to return empty array (no messages to process)Sservice->method('getHistory')->willReturn([]);@@-389,7+385,700// Create a partial mock of the service to control the getHistory methodSservice = Sthis->createPartialMock(TextRelayService::class, ['getHistory']);+// Mock getHistory to returnempty array (no messages to process)Sservice->method('getHistory')->willReturn((J);end diffFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] (master) $ I...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71739
|
2573
|
52
|
2026-05-26T07:06:12.345690+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779172345_m2.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 modified...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.51329786,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.5375665,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.49268618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.5013298,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.32446808,"height":0.016759777},"on_screen":true,"role_description":"text"}]...
|
-260786140542726242
|
-9185675995107292137
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 modified...
|
71737
|
NULL
|
NULL
|
NULL
|
|
71738
|
2572
|
15
|
2026-05-26T07:06:10.537510+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779170537_m1.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 modified
Owner resolver changes
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
3 differences
1aee7aad
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
// Setup services.
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);
$crmService->setUser($roomOwner);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
$activity = $roomOwner->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE_INBOUND,
'provider' => Models\Activity::PROVIDER_TWILIO,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_RINGING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),
'scheduled_start_time' => $now,
]);
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $roomOwner->getRegionId(),
'is_muted' => false,
]);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $callerId,
'name' => $activity->prospect_name,
'enter_time' => $now,
]);
$caller->connections()->create([
'telephony_provider_id' => $callerCallSid,
'session_id' => $session->getId(),
'phone' => $callerId,
'direction' => 'inbound',
'type' => Connection::TYPE_PSTN,
'opened_at' => $now,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'user_id' => $roomOwner->id,
'email' => $roomOwner->email,
'name' => $roomOwner->name,
'phone_number' => $phoneNumber,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'direction' => 'outbound-api',
'phone' => $phoneNumber,
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$this->logger->debug(__METHOD__ . ': done', [
'callSid' => $data->getCallSid(),
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
$activity->refresh();
return $activity;
}
private function getVoiceRecordingReasonCode(Models\Team $team, string $phoneNumber): int
{
$this->logger->debug(__METHOD__, [
'team' => $team->id_string,
'phone' => $phoneNumber,
]);
if (
! $team->hasComplianceModeRecordingPrompt()
&& ! $team->hasComplianceModeRecordingRestrictToOneSide()
&& ! $team->hasComplianceModeRecordingRestrictRecording()
) {
return 0;
}
/** @var Models\VoiceConsentPrefix|null $voiceConsentPolicy */
$voiceConsentPolicy = Models\VoiceConsentPrefix::query()
->whereRaw("? LIKE CONCAT(destination_prefix, '%')", [$phoneNumber])
->where(static function (Eloquent\Builder $builder) use ($team): void {
$builder
->whereNull('team_id')
->orWhere('team_id', $team->getId());
})
->orderByDesc('team_id')
->first();
if (! $voiceConsentPolicy instanceof Models\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {
return 0;
}
if ($team->hasComplianceModeRecordingPrompt()) {
// This number requires consent from the other party before recording the call.
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;
}
if ($team->hasComplianceModeRecordingRestrictToOneSide()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;
}
if ($team->hasComplianceModeRecordingRestrictRecording()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;
}
return 0;
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOrganiser(Models\Activity $activity): void
{
$this->logger->debug(__METHOD__, [
'activity' => $activity->id_string,
]);
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$canBeDialed =
$activity->isTypeSoftPhone()
&& ! $activity->hasStarted()
&& $activity->isPending()
&& $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->exists();
if (! $canBeDialed) {
throw new OutOfBoundsException('Cannot make an outbound call to this activity.');
}
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->firstOrFail();
/** @var Models\Session $session */
$session = $organiserConnection->getSession();
$twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
]);
// What number will the organizer see as caller id
$callerId = $roomOwner->getSoftPhoneNumber();
if ($callerId === null) {
$callee = $activity->participants()->whereNull('user_id')->first();
// This is a bridge call. We make it look like the callee is calling the organizer
$callerId = $callee->phone_number;
}
// What number will the prospect see as caller id
$calleeId = $organiser->getPhoneNumber();
$this->logger->debug(__METHOD__ . ': twilio call', [
'calleeId' => $calleeId,
'callerId' => $callerId,
]);
$twilioParticipant = $twilio->createParticipantInstance(
$session->id_string,
$callerId,
$calleeId,
$twilioConferenceSettings
);
$organiserConnection
->setTelephonyProviderId($twilioParticipant->callSid)
->save();
$session
->setTelephonyProviderId($twilioParticipant->conferenceSid)
->save();
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'session' => $session->id_string,
]);
}
public function dialCoach(Models\Activity $activity, Models\User $user): void
{
throw new \LogicException('Not implemented yet');
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function terminate(VO\ConferenceManager\ActivityTerminate $data): void
{
if (! $data instanceof VO\ConferenceManager\SoftphoneTerminate) {
throw new UnexpectedValueException(sprintf(
'Expecting an instance of "%s", but "%s" given.',
VO\ConferenceManager\SoftphoneTerminate::class,
\get_class($data)
));
}
$this->logger->debug(__METHOD__, [
'activity' => $data->getActivity()->id_string,
'user' => $data->getUser()->id_string,
]);
$activity = $data->getActivity();
$roomOwner = $activity->getUser();
$isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();
if ($isCancelling) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
}
/** @var Models\Session $session */
$session = $activity->sessions->first();
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->whereIn('type', [
Connection::TYPE_WEBRTC,
Connection::TYPE_PSTN,
])
->firstOrFail();
if (! $organiserConnection->hasTelephonyProviderId()) {
throw new LogicException(
'Unable to terminate call as we do not yet know the organiser telephony provider id',
);
}
$twilioClient = $this->twilioClientBuilder->build($activity->getTeam());
$twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());
$session->hasTelephonyProviderId()
&& $twilioClient->terminateConference($session->getTelephonyProviderId());
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'session' => $session->id_string,
]);
}
public function answer(VO\ConferenceManager\ActivityAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundAnswer) {
return $this->answerInboundCall($data);
}
if ($data instanceof VO\ConferenceManager\SoftPhoneOutboundAnswer) {
return $this->answerOutboundCall($data);
}
throw new LogicException('Unable to answer - not supported activity answer instance');
}
private function answerInboundCall(VO\ConferenceManager\SoftPhoneInboundAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$dateTime = now();
$activity = $this->create(
new VO\ConferenceManager\SoftPhoneInboundCreate(
$data->getCallerId(),
$data->getCallSid(),
$data->getUser()
)
);
/** @var Models\Participant $activityGuest */
$activityGuest = $activity->from;
$activityGuest
->setEnterTime($dateTime)
->save();
/** @var Connection $connection */
$connection = $activityGuest->connections()
->where('type', Connection::TYPE_PSTN)
->firstOrFail();
$connection
->setTelephonyProviderId($data->getCallSid())
->setOpenedAt($dateTime)
->save();
event(new Events\Activities\Connections\Opened($connection));
return $this->joinTwilioConference($connection, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]);
}
private function answerOutboundCall(VO\ConferenceManager\SoftPhoneOutboundAnswer $data): TwiML\VoiceResponse
{
$connection = $data->getConnection();
$participant = $connection->getParticipant();
$activity = $data->getActivity();
$organiser = $activity->organizer;
$dateTime = now();
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
'connection' => $connection->id_string,
'participant' => $participant->id_string,
'activity' => $activity->id_string,
]);
if (! $participant->hasEnterTime()) {
$participant->update([
'enter_time' => $dateTime,
]);
}
$connection->update([
'is_muted' => $participant->isCoach(),
'opened_at' => $dateTime,
]);
event(new Events\Activities\Connections\Opened($connection));
if ($participant->isCoach()) {
/** @var Connection $roomOwnerConnection */
$roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();
return $this->joinTwilioConference(
$connection,
[
'coach' => $roomOwnerConnection->getTelephonyProviderId(),
]
);
}
return $this->joinTwilioConference(
$connection,
[
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]
);
}
private function determineUserCallerId(Models\User $user): string
{
if ($user->hasCallerId()) {
return $user->getCallerId();
}
// TODO: We should require a caller id to be verified before using the dialer
return $user->getSoftPhoneNumber() ?? $user->phone;
}
private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool
{
$startsWith = '[HTTP 404] Unable to create record: The requested resource';
$endsWith = 'was not found';
return Str::startsWith($exceptionMessage, $startsWith)
&& Str::endsWith($exceptionMessage, $endsWith);
}
/**
* @throws RestException
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOtherParty(Connection $callerConnection): self
{
$session = $callerConnection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$this->logger->debug(__METHOD__, [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {
throw new UnexpectedValueException('Unexpected activity type');
}
$callee = $activity->to;
$calleeId = $callee->getPhoneNumber();
/** @var Connection $calleeConnection */
$calleeConnection = $callee->connections()
->whereIn('type', [
Connection::TYPE_PSTN,
Connection::TYPE_WEBRTC,
])
->firstOrFail();
$caller = $activity->from;
// we need to take the number from the connection
// as the one we store on participant level might be the one we call to the rep via PSTN mode
$callerId = $callerConnection->getPhoneNumber();
if (! $caller->is($callerConnection->getParticipant())) {
throw new LogicException(sprintf(
'Unexpected caller participant. Expected: "%s", Actual: "%s"',
$caller->id_string,
$callerConnection->getParticipant()->id_string
));
}
$twilioConferenceSettings = [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
];
$hasInboundRecording = $activity->isTypeSoftphoneInbound()
&& $roomOwner->checkSoftphoneInboundRecordPreference();
$hasOutboundRecording = $activity->isTypeSoftPhone()
&& $roomOwner->checkSoftphoneOutboundRecordPreference();
$isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;
$isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()
|| $activity->hasRecordingReasonComplianceRestricted();
if ($isRecordingEnabled && ! $isRecordingRestricted) {
$twilioConferenceSettings = array_merge($twilioConferenceSettings, [
'record' => true,
'recordingChannels' => 'dual',
'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),
'recordingStatusCallbackMethod' => 'POST',
'recordingStatusCallbackEvent' => 'in-progress completed absent',
]);
if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {
$twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()
? 'outbound'
: 'inbound';
}
}
try {
if ($activity->hasRecordingReasonCompliancePrompted()) {
$this->logger->info('[Softphone] Request compliance prompt via outbound call', [
'activity_uuid' => $activity->getUuid(),
'session' => $session->id_string,
'conference_sid' => $session->getTelephonyProviderId(),
]);
$callInstance = $twilio->createCall(
$callerId,
$calleeId,
$this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [
'session' => $session->id_string,
'connection' => $calleeConnection->id_string,
]),
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $callInstance->sid,
'parent_connection_id' => $callerConnection->getId(),
]);
} else {
$twilioParticipantConnection = $twilio->createParticipantInstance(
$session->getTelephonyProviderId(),
$callerId,
$calleeId,
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $twilioParticipantConnection->callSid,
]);
}
} catch (RestException $restException) {
if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {
throw $restException;
}
if (! $activity->isInFiniteState()) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
$message = __METHOD__ . ': activity.status -> cancelled';
} else {
$message = sprintf(
__METHOD__ . ': not setting to cancelled, activity.status is %s',
$activity->getStatus()
);
}
$this->logger->info($message, [
'activity' => $activity->id_string,
'exception' => $restException,
]);
}
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
return $this;
}
private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array
{
$session = $connection->getSession();
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => false,
'record' => false,
'waitUrl' => '',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]),
'statusCallbackEvent' => [
'initiated',
'ringing',
'answered',
'completed',
],
'statusCallbackMethod' => 'POST',
'conferenceStatusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
]),
'conferenceStatusCallbackMethod' => 'POST',
'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function applyDefaultTwilioConferenceSettings(Models\Session $session, array $override = []): array
{
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => false,
'endConferenceOnExit' => false,
'waitUrl' => '',
'statusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
// 'speaker',
]),
'statusCallbackMethod' => 'POST',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function joinTwilioConference(Connection $connection, array $options = []): TwiML\VoiceResponse
{
$session = $connection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$this->logger->debug(__METHOD__, [
'connection' => $connection->id_string,
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
]);
$voiceResponse = new TwiML\VoiceResponse();
$voiceResponse->dial()
->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]))
->conference(
$session->getIdString(),
$this->applyDefaultTwilioConferenceSettings($session, $options)
);
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'result' => $voiceResponse,
]);
return $voiceResponse;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [
'exception' => $throwable,
'user_id' => $user->getId(),
'team_id' => $team->getId(),
]);
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $roomOwner->getTeam(),
'integrationAdmin' => $roomOwner,
'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Owner resolver changes","depth":2,"on_screen":true,"value":"Owner resolver changes","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n // Setup services.\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);\n $crmService->setUser($roomOwner);\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n // Setup services.\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);\n $crmService->setUser($roomOwner);\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [\n 'exception' => $throwable,\n 'user_id' => $user->getId(),\n 'team_id' => $team->getId(),\n ]);\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $roomOwner->getTeam(),\n 'integrationAdmin' => $roomOwner,\n 'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [\n 'exception' => $throwable,\n 'user_id' => $user->getId(),\n 'team_id' => $team->getId(),\n ]);\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $roomOwner->getTeam(),\n 'integrationAdmin' => $roomOwner,\n 'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
6337237157995359431
|
8095951854968406545
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 modified
Owner resolver changes
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
3 differences
1aee7aad
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
// Setup services.
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);
$crmService->setUser($roomOwner);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
$activity = $roomOwner->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE_INBOUND,
'provider' => Models\Activity::PROVIDER_TWILIO,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_RINGING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),
'scheduled_start_time' => $now,
]);
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $roomOwner->getRegionId(),
'is_muted' => false,
]);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $callerId,
'name' => $activity->prospect_name,
'enter_time' => $now,
]);
$caller->connections()->create([
'telephony_provider_id' => $callerCallSid,
'session_id' => $session->getId(),
'phone' => $callerId,
'direction' => 'inbound',
'type' => Connection::TYPE_PSTN,
'opened_at' => $now,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'user_id' => $roomOwner->id,
'email' => $roomOwner->email,
'name' => $roomOwner->name,
'phone_number' => $phoneNumber,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'direction' => 'outbound-api',
'phone' => $phoneNumber,
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$this->logger->debug(__METHOD__ . ': done', [
'callSid' => $data->getCallSid(),
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
$activity->refresh();
return $activity;
}
private function getVoiceRecordingReasonCode(Models\Team $team, string $phoneNumber): int
{
$this->logger->debug(__METHOD__, [
'team' => $team->id_string,
'phone' => $phoneNumber,
]);
if (
! $team->hasComplianceModeRecordingPrompt()
&& ! $team->hasComplianceModeRecordingRestrictToOneSide()
&& ! $team->hasComplianceModeRecordingRestrictRecording()
) {
return 0;
}
/** @var Models\VoiceConsentPrefix|null $voiceConsentPolicy */
$voiceConsentPolicy = Models\VoiceConsentPrefix::query()
->whereRaw("? LIKE CONCAT(destination_prefix, '%')", [$phoneNumber])
->where(static function (Eloquent\Builder $builder) use ($team): void {
$builder
->whereNull('team_id')
->orWhere('team_id', $team->getId());
})
->orderByDesc('team_id')
->first();
if (! $voiceConsentPolicy instanceof Models\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {
return 0;
}
if ($team->hasComplianceModeRecordingPrompt()) {
// This number requires consent from the other party before recording the call.
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;
}
if ($team->hasComplianceModeRecordingRestrictToOneSide()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;
}
if ($team->hasComplianceModeRecordingRestrictRecording()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;
}
return 0;
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOrganiser(Models\Activity $activity): void
{
$this->logger->debug(__METHOD__, [
'activity' => $activity->id_string,
]);
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$canBeDialed =
$activity->isTypeSoftPhone()
&& ! $activity->hasStarted()
&& $activity->isPending()
&& $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->exists();
if (! $canBeDialed) {
throw new OutOfBoundsException('Cannot make an outbound call to this activity.');
}
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->firstOrFail();
/** @var Models\Session $session */
$session = $organiserConnection->getSession();
$twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
]);
// What number will the organizer see as caller id
$callerId = $roomOwner->getSoftPhoneNumber();
if ($callerId === null) {
$callee = $activity->participants()->whereNull('user_id')->first();
// This is a bridge call. We make it look like the callee is calling the organizer
$callerId = $callee->phone_number;
}
// What number will the prospect see as caller id
$calleeId = $organiser->getPhoneNumber();
$this->logger->debug(__METHOD__ . ': twilio call', [
'calleeId' => $calleeId,
'callerId' => $callerId,
]);
$twilioParticipant = $twilio->createParticipantInstance(
$session->id_string,
$callerId,
$calleeId,
$twilioConferenceSettings
);
$organiserConnection
->setTelephonyProviderId($twilioParticipant->callSid)
->save();
$session
->setTelephonyProviderId($twilioParticipant->conferenceSid)
->save();
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'session' => $session->id_string,
]);
}
public function dialCoach(Models\Activity $activity, Models\User $user): void
{
throw new \LogicException('Not implemented yet');
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function terminate(VO\ConferenceManager\ActivityTerminate $data): void
{
if (! $data instanceof VO\ConferenceManager\SoftphoneTerminate) {
throw new UnexpectedValueException(sprintf(
'Expecting an instance of "%s", but "%s" given.',
VO\ConferenceManager\SoftphoneTerminate::class,
\get_class($data)
));
}
$this->logger->debug(__METHOD__, [
'activity' => $data->getActivity()->id_string,
'user' => $data->getUser()->id_string,
]);
$activity = $data->getActivity();
$roomOwner = $activity->getUser();
$isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();
if ($isCancelling) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
}
/** @var Models\Session $session */
$session = $activity->sessions->first();
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->whereIn('type', [
Connection::TYPE_WEBRTC,
Connection::TYPE_PSTN,
])
->firstOrFail();
if (! $organiserConnection->hasTelephonyProviderId()) {
throw new LogicException(
'Unable to terminate call as we do not yet know the organiser telephony provider id',
);
}
$twilioClient = $this->twilioClientBuilder->build($activity->getTeam());
$twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());
$session->hasTelephonyProviderId()
&& $twilioClient->terminateConference($session->getTelephonyProviderId());
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'session' => $session->id_string,
]);
}
public function answer(VO\ConferenceManager\ActivityAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundAnswer) {
return $this->answerInboundCall($data);
}
if ($data instanceof VO\ConferenceManager\SoftPhoneOutboundAnswer) {
return $this->answerOutboundCall($data);
}
throw new LogicException('Unable to answer - not supported activity answer instance');
}
private function answerInboundCall(VO\ConferenceManager\SoftPhoneInboundAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$dateTime = now();
$activity = $this->create(
new VO\ConferenceManager\SoftPhoneInboundCreate(
$data->getCallerId(),
$data->getCallSid(),
$data->getUser()
)
);
/** @var Models\Participant $activityGuest */
$activityGuest = $activity->from;
$activityGuest
->setEnterTime($dateTime)
->save();
/** @var Connection $connection */
$connection = $activityGuest->connections()
->where('type', Connection::TYPE_PSTN)
->firstOrFail();
$connection
->setTelephonyProviderId($data->getCallSid())
->setOpenedAt($dateTime)
->save();
event(new Events\Activities\Connections\Opened($connection));
return $this->joinTwilioConference($connection, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]);
}
private function answerOutboundCall(VO\ConferenceManager\SoftPhoneOutboundAnswer $data): TwiML\VoiceResponse
{
$connection = $data->getConnection();
$participant = $connection->getParticipant();
$activity = $data->getActivity();
$organiser = $activity->organizer;
$dateTime = now();
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
'connection' => $connection->id_string,
'participant' => $participant->id_string,
'activity' => $activity->id_string,
]);
if (! $participant->hasEnterTime()) {
$participant->update([
'enter_time' => $dateTime,
]);
}
$connection->update([
'is_muted' => $participant->isCoach(),
'opened_at' => $dateTime,
]);
event(new Events\Activities\Connections\Opened($connection));
if ($participant->isCoach()) {
/** @var Connection $roomOwnerConnection */
$roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();
return $this->joinTwilioConference(
$connection,
[
'coach' => $roomOwnerConnection->getTelephonyProviderId(),
]
);
}
return $this->joinTwilioConference(
$connection,
[
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]
);
}
private function determineUserCallerId(Models\User $user): string
{
if ($user->hasCallerId()) {
return $user->getCallerId();
}
// TODO: We should require a caller id to be verified before using the dialer
return $user->getSoftPhoneNumber() ?? $user->phone;
}
private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool
{
$startsWith = '[HTTP 404] Unable to create record: The requested resource';
$endsWith = 'was not found';
return Str::startsWith($exceptionMessage, $startsWith)
&& Str::endsWith($exceptionMessage, $endsWith);
}
/**
* @throws RestException
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOtherParty(Connection $callerConnection): self
{
$session = $callerConnection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$this->logger->debug(__METHOD__, [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {
throw new UnexpectedValueException('Unexpected activity type');
}
$callee = $activity->to;
$calleeId = $callee->getPhoneNumber();
/** @var Connection $calleeConnection */
$calleeConnection = $callee->connections()
->whereIn('type', [
Connection::TYPE_PSTN,
Connection::TYPE_WEBRTC,
])
->firstOrFail();
$caller = $activity->from;
// we need to take the number from the connection
// as the one we store on participant level might be the one we call to the rep via PSTN mode
$callerId = $callerConnection->getPhoneNumber();
if (! $caller->is($callerConnection->getParticipant())) {
throw new LogicException(sprintf(
'Unexpected caller participant. Expected: "%s", Actual: "%s"',
$caller->id_string,
$callerConnection->getParticipant()->id_string
));
}
$twilioConferenceSettings = [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
];
$hasInboundRecording = $activity->isTypeSoftphoneInbound()
&& $roomOwner->checkSoftphoneInboundRecordPreference();
$hasOutboundRecording = $activity->isTypeSoftPhone()
&& $roomOwner->checkSoftphoneOutboundRecordPreference();
$isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;
$isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()
|| $activity->hasRecordingReasonComplianceRestricted();
if ($isRecordingEnabled && ! $isRecordingRestricted) {
$twilioConferenceSettings = array_merge($twilioConferenceSettings, [
'record' => true,
'recordingChannels' => 'dual',
'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),
'recordingStatusCallbackMethod' => 'POST',
'recordingStatusCallbackEvent' => 'in-progress completed absent',
]);
if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {
$twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()
? 'outbound'
: 'inbound';
}
}
try {
if ($activity->hasRecordingReasonCompliancePrompted()) {
$this->logger->info('[Softphone] Request compliance prompt via outbound call', [
'activity_uuid' => $activity->getUuid(),
'session' => $session->id_string,
'conference_sid' => $session->getTelephonyProviderId(),
]);
$callInstance = $twilio->createCall(
$callerId,
$calleeId,
$this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [
'session' => $session->id_string,
'connection' => $calleeConnection->id_string,
]),
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $callInstance->sid,
'parent_connection_id' => $callerConnection->getId(),
]);
} else {
$twilioParticipantConnection = $twilio->createParticipantInstance(
$session->getTelephonyProviderId(),
$callerId,
$calleeId,
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $twilioParticipantConnection->callSid,
]);
}
} catch (RestException $restException) {
if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {
throw $restException;
}
if (! $activity->isInFiniteState()) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
$message = __METHOD__ . ': activity.status -> cancelled';
} else {
$message = sprintf(
__METHOD__ . ': not setting to cancelled, activity.status is %s',
$activity->getStatus()
);
}
$this->logger->info($message, [
'activity' => $activity->id_string,
'exception' => $restException,
]);
}
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
return $this;
}
private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array
{
$session = $connection->getSession();
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => false,
'record' => false,
'waitUrl' => '',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]),
'statusCallbackEvent' => [
'initiated',
'ringing',
'answered',
'completed',
],
'statusCallbackMethod' => 'POST',
'conferenceStatusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
]),
'conferenceStatusCallbackMethod' => 'POST',
'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function applyDefaultTwilioConferenceSettings(Models\Session $session, array $override = []): array
{
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => false,
'endConferenceOnExit' => false,
'waitUrl' => '',
'statusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
// 'speaker',
]),
'statusCallbackMethod' => 'POST',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function joinTwilioConference(Connection $connection, array $options = []): TwiML\VoiceResponse
{
$session = $connection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$this->logger->debug(__METHOD__, [
'connection' => $connection->id_string,
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
]);
$voiceResponse = new TwiML\VoiceResponse();
$voiceResponse->dial()
->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]))
->conference(
$session->getIdString(),
$this->applyDefaultTwilioConferenceSettings($session, $options)
);
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'result' => $voiceResponse,
]);
return $voiceResponse;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [
'exception' => $throwable,
'user_id' => $user->getId(),
'team_id' => $team->getId(),
]);
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $roomOwner->getTeam(),
'integrationAdmin' => $roomOwner,
'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */...
|
71736
|
NULL
|
NULL
|
NULL
|
|
71737
|
2573
|
51
|
2026-05-26T07:06:10.434701+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779170434_m2.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 modified
Owner resolver changes
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
3 differences
1aee7aad
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
// Setup services.
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);
$crmService->setUser($roomOwner);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
$activity = $roomOwner->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE_INBOUND,
'provider' => Models\Activity::PROVIDER_TWILIO,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_RINGING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),
'scheduled_start_time' => $now,
]);
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $roomOwner->getRegionId(),
'is_muted' => false,
]);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $callerId,
'name' => $activity->prospect_name,
'enter_time' => $now,
]);
$caller->connections()->create([
'telephony_provider_id' => $callerCallSid,
'session_id' => $session->getId(),
'phone' => $callerId,
'direction' => 'inbound',
'type' => Connection::TYPE_PSTN,
'opened_at' => $now,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'user_id' => $roomOwner->id,
'email' => $roomOwner->email,
'name' => $roomOwner->name,
'phone_number' => $phoneNumber,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'direction' => 'outbound-api',
'phone' => $phoneNumber,
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$this->logger->debug(__METHOD__ . ': done', [
'callSid' => $data->getCallSid(),
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
$activity->refresh();
return $activity;
}
private function getVoiceRecordingReasonCode(Models\Team $team, string $phoneNumber): int
{
$this->logger->debug(__METHOD__, [
'team' => $team->id_string,
'phone' => $phoneNumber,
]);
if (
! $team->hasComplianceModeRecordingPrompt()
&& ! $team->hasComplianceModeRecordingRestrictToOneSide()
&& ! $team->hasComplianceModeRecordingRestrictRecording()
) {
return 0;
}
/** @var Models\VoiceConsentPrefix|null $voiceConsentPolicy */
$voiceConsentPolicy = Models\VoiceConsentPrefix::query()
->whereRaw("? LIKE CONCAT(destination_prefix, '%')", [$phoneNumber])
->where(static function (Eloquent\Builder $builder) use ($team): void {
$builder
->whereNull('team_id')
->orWhere('team_id', $team->getId());
})
->orderByDesc('team_id')
->first();
if (! $voiceConsentPolicy instanceof Models\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {
return 0;
}
if ($team->hasComplianceModeRecordingPrompt()) {
// This number requires consent from the other party before recording the call.
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;
}
if ($team->hasComplianceModeRecordingRestrictToOneSide()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;
}
if ($team->hasComplianceModeRecordingRestrictRecording()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;
}
return 0;
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOrganiser(Models\Activity $activity): void
{
$this->logger->debug(__METHOD__, [
'activity' => $activity->id_string,
]);
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$canBeDialed =
$activity->isTypeSoftPhone()
&& ! $activity->hasStarted()
&& $activity->isPending()
&& $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->exists();
if (! $canBeDialed) {
throw new OutOfBoundsException('Cannot make an outbound call to this activity.');
}
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->firstOrFail();
/** @var Models\Session $session */
$session = $organiserConnection->getSession();
$twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
]);
// What number will the organizer see as caller id
$callerId = $roomOwner->getSoftPhoneNumber();
if ($callerId === null) {
$callee = $activity->participants()->whereNull('user_id')->first();
// This is a bridge call. We make it look like the callee is calling the organizer
$callerId = $callee->phone_number;
}
// What number will the prospect see as caller id
$calleeId = $organiser->getPhoneNumber();
$this->logger->debug(__METHOD__ . ': twilio call', [
'calleeId' => $calleeId,
'callerId' => $callerId,
]);
$twilioParticipant = $twilio->createParticipantInstance(
$session->id_string,
$callerId,
$calleeId,
$twilioConferenceSettings
);
$organiserConnection
->setTelephonyProviderId($twilioParticipant->callSid)
->save();
$session
->setTelephonyProviderId($twilioParticipant->conferenceSid)
->save();
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'session' => $session->id_string,
]);
}
public function dialCoach(Models\Activity $activity, Models\User $user): void
{
throw new \LogicException('Not implemented yet');
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function terminate(VO\ConferenceManager\ActivityTerminate $data): void
{
if (! $data instanceof VO\ConferenceManager\SoftphoneTerminate) {
throw new UnexpectedValueException(sprintf(
'Expecting an instance of "%s", but "%s" given.',
VO\ConferenceManager\SoftphoneTerminate::class,
\get_class($data)
));
}
$this->logger->debug(__METHOD__, [
'activity' => $data->getActivity()->id_string,
'user' => $data->getUser()->id_string,
]);
$activity = $data->getActivity();
$roomOwner = $activity->getUser();
$isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();
if ($isCancelling) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
}
/** @var Models\Session $session */
$session = $activity->sessions->first();
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->whereIn('type', [
Connection::TYPE_WEBRTC,
Connection::TYPE_PSTN,
])
->firstOrFail();
if (! $organiserConnection->hasTelephonyProviderId()) {
throw new LogicException(
'Unable to terminate call as we do not yet know the organiser telephony provider id',
);
}
$twilioClient = $this->twilioClientBuilder->build($activity->getTeam());
$twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());
$session->hasTelephonyProviderId()
&& $twilioClient->terminateConference($session->getTelephonyProviderId());
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'session' => $session->id_string,
]);
}
public function answer(VO\ConferenceManager\ActivityAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundAnswer) {
return $this->answerInboundCall($data);
}
if ($data instanceof VO\ConferenceManager\SoftPhoneOutboundAnswer) {
return $this->answerOutboundCall($data);
}
throw new LogicException('Unable to answer - not supported activity answer instance');
}
private function answerInboundCall(VO\ConferenceManager\SoftPhoneInboundAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$dateTime = now();
$activity = $this->create(
new VO\ConferenceManager\SoftPhoneInboundCreate(
$data->getCallerId(),
$data->getCallSid(),
$data->getUser()
)
);
/** @var Models\Participant $activityGuest */
$activityGuest = $activity->from;
$activityGuest
->setEnterTime($dateTime)
->save();
/** @var Connection $connection */
$connection = $activityGuest->connections()
->where('type', Connection::TYPE_PSTN)
->firstOrFail();
$connection
->setTelephonyProviderId($data->getCallSid())
->setOpenedAt($dateTime)
->save();
event(new Events\Activities\Connections\Opened($connection));
return $this->joinTwilioConference($connection, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]);
}
private function answerOutboundCall(VO\ConferenceManager\SoftPhoneOutboundAnswer $data): TwiML\VoiceResponse
{
$connection = $data->getConnection();
$participant = $connection->getParticipant();
$activity = $data->getActivity();
$organiser = $activity->organizer;
$dateTime = now();
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
'connection' => $connection->id_string,
'participant' => $participant->id_string,
'activity' => $activity->id_string,
]);
if (! $participant->hasEnterTime()) {
$participant->update([
'enter_time' => $dateTime,
]);
}
$connection->update([
'is_muted' => $participant->isCoach(),
'opened_at' => $dateTime,
]);
event(new Events\Activities\Connections\Opened($connection));
if ($participant->isCoach()) {
/** @var Connection $roomOwnerConnection */
$roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();
return $this->joinTwilioConference(
$connection,
[
'coach' => $roomOwnerConnection->getTelephonyProviderId(),
]
);
}
return $this->joinTwilioConference(
$connection,
[
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]
);
}
private function determineUserCallerId(Models\User $user): string
{
if ($user->hasCallerId()) {
return $user->getCallerId();
}
// TODO: We should require a caller id to be verified before using the dialer
return $user->getSoftPhoneNumber() ?? $user->phone;
}
private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool
{
$startsWith = '[HTTP 404] Unable to create record: The requested resource';
$endsWith = 'was not found';
return Str::startsWith($exceptionMessage, $startsWith)
&& Str::endsWith($exceptionMessage, $endsWith);
}
/**
* @throws RestException
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOtherParty(Connection $callerConnection): self
{
$session = $callerConnection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$this->logger->debug(__METHOD__, [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {
throw new UnexpectedValueException('Unexpected activity type');
}
$callee = $activity->to;
$calleeId = $callee->getPhoneNumber();
/** @var Connection $calleeConnection */
$calleeConnection = $callee->connections()
->whereIn('type', [
Connection::TYPE_PSTN,
Connection::TYPE_WEBRTC,
])
->firstOrFail();
$caller = $activity->from;
// we need to take the number from the connection
// as the one we store on participant level might be the one we call to the rep via PSTN mode
$callerId = $callerConnection->getPhoneNumber();
if (! $caller->is($callerConnection->getParticipant())) {
throw new LogicException(sprintf(
'Unexpected caller participant. Expected: "%s", Actual: "%s"',
$caller->id_string,
$callerConnection->getParticipant()->id_string
));
}
$twilioConferenceSettings = [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
];
$hasInboundRecording = $activity->isTypeSoftphoneInbound()
&& $roomOwner->checkSoftphoneInboundRecordPreference();
$hasOutboundRecording = $activity->isTypeSoftPhone()
&& $roomOwner->checkSoftphoneOutboundRecordPreference();
$isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;
$isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()
|| $activity->hasRecordingReasonComplianceRestricted();
if ($isRecordingEnabled && ! $isRecordingRestricted) {
$twilioConferenceSettings = array_merge($twilioConferenceSettings, [
'record' => true,
'recordingChannels' => 'dual',
'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),
'recordingStatusCallbackMethod' => 'POST',
'recordingStatusCallbackEvent' => 'in-progress completed absent',
]);
if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {
$twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()
? 'outbound'
: 'inbound';
}
}
try {
if ($activity->hasRecordingReasonCompliancePrompted()) {
$this->logger->info('[Softphone] Request compliance prompt via outbound call', [
'activity_uuid' => $activity->getUuid(),
'session' => $session->id_string,
'conference_sid' => $session->getTelephonyProviderId(),
]);
$callInstance = $twilio->createCall(
$callerId,
$calleeId,
$this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [
'session' => $session->id_string,
'connection' => $calleeConnection->id_string,
]),
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $callInstance->sid,
'parent_connection_id' => $callerConnection->getId(),
]);
} else {
$twilioParticipantConnection = $twilio->createParticipantInstance(
$session->getTelephonyProviderId(),
$callerId,
$calleeId,
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $twilioParticipantConnection->callSid,
]);
}
} catch (RestException $restException) {
if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {
throw $restException;
}
if (! $activity->isInFiniteState()) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
$message = __METHOD__ . ': activity.status -> cancelled';
} else {
$message = sprintf(
__METHOD__ . ': not setting to cancelled, activity.status is %s',
$activity->getStatus()
);
}
$this->logger->info($message, [
'activity' => $activity->id_string,
'exception' => $restException,
]);
}
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
return $this;
}
private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array
{
$session = $connection->getSession();
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => false,
'record' => false,
'waitUrl' => '',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]),
'statusCallbackEvent' => [
'initiated',
'ringing',
'answered',
'completed',
],
'statusCallbackMethod' => 'POST',
'conferenceStatusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
]),
'conferenceStatusCallbackMethod' => 'POST',
'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function applyDefaultTwilioConferenceSettings(Models\Session $session, array $override = []): array
{
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => false,
'endConferenceOnExit' => false,
'waitUrl' => '',
'statusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
// 'speaker',
]),
'statusCallbackMethod' => 'POST',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function joinTwilioConference(Connection $connection, array $options = []): TwiML\VoiceResponse
{
$session = $connection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$this->logger->debug(__METHOD__, [
'connection' => $connection->id_string,
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
]);
$voiceResponse = new TwiML\VoiceResponse();
$voiceResponse->dial()
->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]))
->conference(
$session->getIdString(),
$this->applyDefaultTwilioConferenceSettings($session, $options)
);
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'result' => $voiceResponse,
]);
return $voiceResponse;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [
'exception' => $throwable,
'user_id' => $user->getId(),
'team_id' => $team->getId(),
]);
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $roomOwner->getTeam(),
'integrationAdmin' => $roomOwner,
'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.51329786,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.5375665,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.49268618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.5013298,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.32446808,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Owner resolver changes","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.3238032,"height":0.087789305},"on_screen":true,"value":"Owner resolver changes","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.5711436,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3 differences","depth":1,"bounds":{"left":0.5511968,"top":0.41181165,"width":0.027260639,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.022606382,"height":0.013567438},"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n // Setup services.\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);\n $crmService->setUser($roomOwner);\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.26230052,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n // Setup services.\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);\n $crmService->setUser($roomOwner);\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [\n 'exception' => $throwable,\n 'user_id' => $user->getId(),\n 'team_id' => $team->getId(),\n ]);\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $roomOwner->getTeam(),\n 'integrationAdmin' => $roomOwner,\n 'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.26695478,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [\n 'exception' => $throwable,\n 'user_id' => $user->getId(),\n 'team_id' => $team->getId(),\n ]);\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $roomOwner->getTeam(),\n 'integrationAdmin' => $roomOwner,\n 'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.50764626,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"bounds":{"left":0.5355718,"top":0.65363127,"width":0.044215426,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"bounds":{"left":0.39960107,"top":0.13248204,"width":0.035904255,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
6337237157995359431
|
8095951854968406545
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 modified
Owner resolver changes
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
3 differences
1aee7aad
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
// Setup services.
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);
$crmService->setUser($roomOwner);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
$activity = $roomOwner->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE_INBOUND,
'provider' => Models\Activity::PROVIDER_TWILIO,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_RINGING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),
'scheduled_start_time' => $now,
]);
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $roomOwner->getRegionId(),
'is_muted' => false,
]);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $callerId,
'name' => $activity->prospect_name,
'enter_time' => $now,
]);
$caller->connections()->create([
'telephony_provider_id' => $callerCallSid,
'session_id' => $session->getId(),
'phone' => $callerId,
'direction' => 'inbound',
'type' => Connection::TYPE_PSTN,
'opened_at' => $now,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'user_id' => $roomOwner->id,
'email' => $roomOwner->email,
'name' => $roomOwner->name,
'phone_number' => $phoneNumber,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'direction' => 'outbound-api',
'phone' => $phoneNumber,
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$this->logger->debug(__METHOD__ . ': done', [
'callSid' => $data->getCallSid(),
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
$activity->refresh();
return $activity;
}
private function getVoiceRecordingReasonCode(Models\Team $team, string $phoneNumber): int
{
$this->logger->debug(__METHOD__, [
'team' => $team->id_string,
'phone' => $phoneNumber,
]);
if (
! $team->hasComplianceModeRecordingPrompt()
&& ! $team->hasComplianceModeRecordingRestrictToOneSide()
&& ! $team->hasComplianceModeRecordingRestrictRecording()
) {
return 0;
}
/** @var Models\VoiceConsentPrefix|null $voiceConsentPolicy */
$voiceConsentPolicy = Models\VoiceConsentPrefix::query()
->whereRaw("? LIKE CONCAT(destination_prefix, '%')", [$phoneNumber])
->where(static function (Eloquent\Builder $builder) use ($team): void {
$builder
->whereNull('team_id')
->orWhere('team_id', $team->getId());
})
->orderByDesc('team_id')
->first();
if (! $voiceConsentPolicy instanceof Models\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {
return 0;
}
if ($team->hasComplianceModeRecordingPrompt()) {
// This number requires consent from the other party before recording the call.
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;
}
if ($team->hasComplianceModeRecordingRestrictToOneSide()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;
}
if ($team->hasComplianceModeRecordingRestrictRecording()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;
}
return 0;
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOrganiser(Models\Activity $activity): void
{
$this->logger->debug(__METHOD__, [
'activity' => $activity->id_string,
]);
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$canBeDialed =
$activity->isTypeSoftPhone()
&& ! $activity->hasStarted()
&& $activity->isPending()
&& $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->exists();
if (! $canBeDialed) {
throw new OutOfBoundsException('Cannot make an outbound call to this activity.');
}
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->firstOrFail();
/** @var Models\Session $session */
$session = $organiserConnection->getSession();
$twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
]);
// What number will the organizer see as caller id
$callerId = $roomOwner->getSoftPhoneNumber();
if ($callerId === null) {
$callee = $activity->participants()->whereNull('user_id')->first();
// This is a bridge call. We make it look like the callee is calling the organizer
$callerId = $callee->phone_number;
}
// What number will the prospect see as caller id
$calleeId = $organiser->getPhoneNumber();
$this->logger->debug(__METHOD__ . ': twilio call', [
'calleeId' => $calleeId,
'callerId' => $callerId,
]);
$twilioParticipant = $twilio->createParticipantInstance(
$session->id_string,
$callerId,
$calleeId,
$twilioConferenceSettings
);
$organiserConnection
->setTelephonyProviderId($twilioParticipant->callSid)
->save();
$session
->setTelephonyProviderId($twilioParticipant->conferenceSid)
->save();
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'session' => $session->id_string,
]);
}
public function dialCoach(Models\Activity $activity, Models\User $user): void
{
throw new \LogicException('Not implemented yet');
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function terminate(VO\ConferenceManager\ActivityTerminate $data): void
{
if (! $data instanceof VO\ConferenceManager\SoftphoneTerminate) {
throw new UnexpectedValueException(sprintf(
'Expecting an instance of "%s", but "%s" given.',
VO\ConferenceManager\SoftphoneTerminate::class,
\get_class($data)
));
}
$this->logger->debug(__METHOD__, [
'activity' => $data->getActivity()->id_string,
'user' => $data->getUser()->id_string,
]);
$activity = $data->getActivity();
$roomOwner = $activity->getUser();
$isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();
if ($isCancelling) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
}
/** @var Models\Session $session */
$session = $activity->sessions->first();
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->whereIn('type', [
Connection::TYPE_WEBRTC,
Connection::TYPE_PSTN,
])
->firstOrFail();
if (! $organiserConnection->hasTelephonyProviderId()) {
throw new LogicException(
'Unable to terminate call as we do not yet know the organiser telephony provider id',
);
}
$twilioClient = $this->twilioClientBuilder->build($activity->getTeam());
$twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());
$session->hasTelephonyProviderId()
&& $twilioClient->terminateConference($session->getTelephonyProviderId());
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'session' => $session->id_string,
]);
}
public function answer(VO\ConferenceManager\ActivityAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundAnswer) {
return $this->answerInboundCall($data);
}
if ($data instanceof VO\ConferenceManager\SoftPhoneOutboundAnswer) {
return $this->answerOutboundCall($data);
}
throw new LogicException('Unable to answer - not supported activity answer instance');
}
private function answerInboundCall(VO\ConferenceManager\SoftPhoneInboundAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$dateTime = now();
$activity = $this->create(
new VO\ConferenceManager\SoftPhoneInboundCreate(
$data->getCallerId(),
$data->getCallSid(),
$data->getUser()
)
);
/** @var Models\Participant $activityGuest */
$activityGuest = $activity->from;
$activityGuest
->setEnterTime($dateTime)
->save();
/** @var Connection $connection */
$connection = $activityGuest->connections()
->where('type', Connection::TYPE_PSTN)
->firstOrFail();
$connection
->setTelephonyProviderId($data->getCallSid())
->setOpenedAt($dateTime)
->save();
event(new Events\Activities\Connections\Opened($connection));
return $this->joinTwilioConference($connection, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]);
}
private function answerOutboundCall(VO\ConferenceManager\SoftPhoneOutboundAnswer $data): TwiML\VoiceResponse
{
$connection = $data->getConnection();
$participant = $connection->getParticipant();
$activity = $data->getActivity();
$organiser = $activity->organizer;
$dateTime = now();
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
'connection' => $connection->id_string,
'participant' => $participant->id_string,
'activity' => $activity->id_string,
]);
if (! $participant->hasEnterTime()) {
$participant->update([
'enter_time' => $dateTime,
]);
}
$connection->update([
'is_muted' => $participant->isCoach(),
'opened_at' => $dateTime,
]);
event(new Events\Activities\Connections\Opened($connection));
if ($participant->isCoach()) {
/** @var Connection $roomOwnerConnection */
$roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();
return $this->joinTwilioConference(
$connection,
[
'coach' => $roomOwnerConnection->getTelephonyProviderId(),
]
);
}
return $this->joinTwilioConference(
$connection,
[
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]
);
}
private function determineUserCallerId(Models\User $user): string
{
if ($user->hasCallerId()) {
return $user->getCallerId();
}
// TODO: We should require a caller id to be verified before using the dialer
return $user->getSoftPhoneNumber() ?? $user->phone;
}
private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool
{
$startsWith = '[HTTP 404] Unable to create record: The requested resource';
$endsWith = 'was not found';
return Str::startsWith($exceptionMessage, $startsWith)
&& Str::endsWith($exceptionMessage, $endsWith);
}
/**
* @throws RestException
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOtherParty(Connection $callerConnection): self
{
$session = $callerConnection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$this->logger->debug(__METHOD__, [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {
throw new UnexpectedValueException('Unexpected activity type');
}
$callee = $activity->to;
$calleeId = $callee->getPhoneNumber();
/** @var Connection $calleeConnection */
$calleeConnection = $callee->connections()
->whereIn('type', [
Connection::TYPE_PSTN,
Connection::TYPE_WEBRTC,
])
->firstOrFail();
$caller = $activity->from;
// we need to take the number from the connection
// as the one we store on participant level might be the one we call to the rep via PSTN mode
$callerId = $callerConnection->getPhoneNumber();
if (! $caller->is($callerConnection->getParticipant())) {
throw new LogicException(sprintf(
'Unexpected caller participant. Expected: "%s", Actual: "%s"',
$caller->id_string,
$callerConnection->getParticipant()->id_string
));
}
$twilioConferenceSettings = [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
];
$hasInboundRecording = $activity->isTypeSoftphoneInbound()
&& $roomOwner->checkSoftphoneInboundRecordPreference();
$hasOutboundRecording = $activity->isTypeSoftPhone()
&& $roomOwner->checkSoftphoneOutboundRecordPreference();
$isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;
$isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()
|| $activity->hasRecordingReasonComplianceRestricted();
if ($isRecordingEnabled && ! $isRecordingRestricted) {
$twilioConferenceSettings = array_merge($twilioConferenceSettings, [
'record' => true,
'recordingChannels' => 'dual',
'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),
'recordingStatusCallbackMethod' => 'POST',
'recordingStatusCallbackEvent' => 'in-progress completed absent',
]);
if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {
$twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()
? 'outbound'
: 'inbound';
}
}
try {
if ($activity->hasRecordingReasonCompliancePrompted()) {
$this->logger->info('[Softphone] Request compliance prompt via outbound call', [
'activity_uuid' => $activity->getUuid(),
'session' => $session->id_string,
'conference_sid' => $session->getTelephonyProviderId(),
]);
$callInstance = $twilio->createCall(
$callerId,
$calleeId,
$this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [
'session' => $session->id_string,
'connection' => $calleeConnection->id_string,
]),
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $callInstance->sid,
'parent_connection_id' => $callerConnection->getId(),
]);
} else {
$twilioParticipantConnection = $twilio->createParticipantInstance(
$session->getTelephonyProviderId(),
$callerId,
$calleeId,
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $twilioParticipantConnection->callSid,
]);
}
} catch (RestException $restException) {
if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {
throw $restException;
}
if (! $activity->isInFiniteState()) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
$message = __METHOD__ . ': activity.status -> cancelled';
} else {
$message = sprintf(
__METHOD__ . ': not setting to cancelled, activity.status is %s',
$activity->getStatus()
);
}
$this->logger->info($message, [
'activity' => $activity->id_string,
'exception' => $restException,
]);
}
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
return $this;
}
private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array
{
$session = $connection->getSession();
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => false,
'record' => false,
'waitUrl' => '',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]),
'statusCallbackEvent' => [
'initiated',
'ringing',
'answered',
'completed',
],
'statusCallbackMethod' => 'POST',
'conferenceStatusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
]),
'conferenceStatusCallbackMethod' => 'POST',
'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function applyDefaultTwilioConferenceSettings(Models\Session $session, array $override = []): array
{
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => false,
'endConferenceOnExit' => false,
'waitUrl' => '',
'statusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
// 'speaker',
]),
'statusCallbackMethod' => 'POST',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function joinTwilioConference(Connection $connection, array $options = []): TwiML\VoiceResponse
{
$session = $connection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$this->logger->debug(__METHOD__, [
'connection' => $connection->id_string,
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
]);
$voiceResponse = new TwiML\VoiceResponse();
$voiceResponse->dial()
->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]))
->conference(
$session->getIdString(),
$this->applyDefaultTwilioConferenceSettings($session, $options)
);
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'result' => $voiceResponse,
]);
return $voiceResponse;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [
'exception' => $throwable,
'user_id' => $user->getId(),
'team_id' => $team->getId(),
]);
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $roomOwner->getTeam(),
'integrationAdmin' => $roomOwner,
'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71736
|
2572
|
14
|
2026-05-26T07:06:08.687333+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779168687_m1.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 modified
Owner resolve
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
3 differences
1aee7aad
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
// Setup services.
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);
$crmService->setUser($roomOwner);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
$activity = $roomOwner->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE_INBOUND,
'provider' => Models\Activity::PROVIDER_TWILIO,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_RINGING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),
'scheduled_start_time' => $now,
]);
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $roomOwner->getRegionId(),
'is_muted' => false,
]);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $callerId,
'name' => $activity->prospect_name,
'enter_time' => $now,
]);
$caller->connections()->create([
'telephony_provider_id' => $callerCallSid,
'session_id' => $session->getId(),
'phone' => $callerId,
'direction' => 'inbound',
'type' => Connection::TYPE_PSTN,
'opened_at' => $now,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'user_id' => $roomOwner->id,
'email' => $roomOwner->email,
'name' => $roomOwner->name,
'phone_number' => $phoneNumber,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'direction' => 'outbound-api',
'phone' => $phoneNumber,
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$this->logger->debug(__METHOD__ . ': done', [
'callSid' => $data->getCallSid(),
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
$activity->refresh();
return $activity;
}
private function getVoiceRecordingReasonCode(Models\Team $team, string $phoneNumber): int
{
$this->logger->debug(__METHOD__, [
'team' => $team->id_string,
'phone' => $phoneNumber,
]);
if (
! $team->hasComplianceModeRecordingPrompt()
&& ! $team->hasComplianceModeRecordingRestrictToOneSide()
&& ! $team->hasComplianceModeRecordingRestrictRecording()
) {
return 0;
}
/** @var Models\VoiceConsentPrefix|null $voiceConsentPolicy */
$voiceConsentPolicy = Models\VoiceConsentPrefix::query()
->whereRaw("? LIKE CONCAT(destination_prefix, '%')", [$phoneNumber])
->where(static function (Eloquent\Builder $builder) use ($team): void {
$builder
->whereNull('team_id')
->orWhere('team_id', $team->getId());
})
->orderByDesc('team_id')
->first();
if (! $voiceConsentPolicy instanceof Models\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {
return 0;
}
if ($team->hasComplianceModeRecordingPrompt()) {
// This number requires consent from the other party before recording the call.
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;
}
if ($team->hasComplianceModeRecordingRestrictToOneSide()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;
}
if ($team->hasComplianceModeRecordingRestrictRecording()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;
}
return 0;
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOrganiser(Models\Activity $activity): void
{
$this->logger->debug(__METHOD__, [
'activity' => $activity->id_string,
]);
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$canBeDialed =
$activity->isTypeSoftPhone()
&& ! $activity->hasStarted()
&& $activity->isPending()
&& $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->exists();
if (! $canBeDialed) {
throw new OutOfBoundsException('Cannot make an outbound call to this activity.');
}
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->firstOrFail();
/** @var Models\Session $session */
$session = $organiserConnection->getSession();
$twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
]);
// What number will the organizer see as caller id
$callerId = $roomOwner->getSoftPhoneNumber();
if ($callerId === null) {
$callee = $activity->participants()->whereNull('user_id')->first();
// This is a bridge call. We make it look like the callee is calling the organizer
$callerId = $callee->phone_number;
}
// What number will the prospect see as caller id
$calleeId = $organiser->getPhoneNumber();
$this->logger->debug(__METHOD__ . ': twilio call', [
'calleeId' => $calleeId,
'callerId' => $callerId,
]);
$twilioParticipant = $twilio->createParticipantInstance(
$session->id_string,
$callerId,
$calleeId,
$twilioConferenceSettings
);
$organiserConnection
->setTelephonyProviderId($twilioParticipant->callSid)
->save();
$session
->setTelephonyProviderId($twilioParticipant->conferenceSid)
->save();
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'session' => $session->id_string,
]);
}
public function dialCoach(Models\Activity $activity, Models\User $user): void
{
throw new \LogicException('Not implemented yet');
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function terminate(VO\ConferenceManager\ActivityTerminate $data): void
{
if (! $data instanceof VO\ConferenceManager\SoftphoneTerminate) {
throw new UnexpectedValueException(sprintf(
'Expecting an instance of "%s", but "%s" given.',
VO\ConferenceManager\SoftphoneTerminate::class,
\get_class($data)
));
}
$this->logger->debug(__METHOD__, [
'activity' => $data->getActivity()->id_string,
'user' => $data->getUser()->id_string,
]);
$activity = $data->getActivity();
$roomOwner = $activity->getUser();
$isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();
if ($isCancelling) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
}
/** @var Models\Session $session */
$session = $activity->sessions->first();
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->whereIn('type', [
Connection::TYPE_WEBRTC,
Connection::TYPE_PSTN,
])
->firstOrFail();
if (! $organiserConnection->hasTelephonyProviderId()) {
throw new LogicException(
'Unable to terminate call as we do not yet know the organiser telephony provider id',
);
}
$twilioClient = $this->twilioClientBuilder->build($activity->getTeam());
$twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());
$session->hasTelephonyProviderId()
&& $twilioClient->terminateConference($session->getTelephonyProviderId());
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'session' => $session->id_string,
]);
}
public function answer(VO\ConferenceManager\ActivityAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundAnswer) {
return $this->answerInboundCall($data);
}
if ($data instanceof VO\ConferenceManager\SoftPhoneOutboundAnswer) {
return $this->answerOutboundCall($data);
}
throw new LogicException('Unable to answer - not supported activity answer instance');
}
private function answerInboundCall(VO\ConferenceManager\SoftPhoneInboundAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$dateTime = now();
$activity = $this->create(
new VO\ConferenceManager\SoftPhoneInboundCreate(
$data->getCallerId(),
$data->getCallSid(),
$data->getUser()
)
);
/** @var Models\Participant $activityGuest */
$activityGuest = $activity->from;
$activityGuest
->setEnterTime($dateTime)
->save();
/** @var Connection $connection */
$connection = $activityGuest->connections()
->where('type', Connection::TYPE_PSTN)
->firstOrFail();
$connection
->setTelephonyProviderId($data->getCallSid())
->setOpenedAt($dateTime)
->save();
event(new Events\Activities\Connections\Opened($connection));
return $this->joinTwilioConference($connection, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]);
}
private function answerOutboundCall(VO\ConferenceManager\SoftPhoneOutboundAnswer $data): TwiML\VoiceResponse
{
$connection = $data->getConnection();
$participant = $connection->getParticipant();
$activity = $data->getActivity();
$organiser = $activity->organizer;
$dateTime = now();
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
'connection' => $connection->id_string,
'participant' => $participant->id_string,
'activity' => $activity->id_string,
]);
if (! $participant->hasEnterTime()) {
$participant->update([
'enter_time' => $dateTime,
]);
}
$connection->update([
'is_muted' => $participant->isCoach(),
'opened_at' => $dateTime,
]);
event(new Events\Activities\Connections\Opened($connection));
if ($participant->isCoach()) {
/** @var Connection $roomOwnerConnection */
$roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();
return $this->joinTwilioConference(
$connection,
[
'coach' => $roomOwnerConnection->getTelephonyProviderId(),
]
);
}
return $this->joinTwilioConference(
$connection,
[
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]
);
}
private function determineUserCallerId(Models\User $user): string
{
if ($user->hasCallerId()) {
return $user->getCallerId();
}
// TODO: We should require a caller id to be verified before using the dialer
return $user->getSoftPhoneNumber() ?? $user->phone;
}
private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool
{
$startsWith = '[HTTP 404] Unable to create record: The requested resource';
$endsWith = 'was not found';
return Str::startsWith($exceptionMessage, $startsWith)
&& Str::endsWith($exceptionMessage, $endsWith);
}
/**
* @throws RestException
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOtherParty(Connection $callerConnection): self
{
$session = $callerConnection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$this->logger->debug(__METHOD__, [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {
throw new UnexpectedValueException('Unexpected activity type');
}
$callee = $activity->to;
$calleeId = $callee->getPhoneNumber();
/** @var Connection $calleeConnection */
$calleeConnection = $callee->connections()
->whereIn('type', [
Connection::TYPE_PSTN,
Connection::TYPE_WEBRTC,
])
->firstOrFail();
$caller = $activity->from;
// we need to take the number from the connection
// as the one we store on participant level might be the one we call to the rep via PSTN mode
$callerId = $callerConnection->getPhoneNumber();
if (! $caller->is($callerConnection->getParticipant())) {
throw new LogicException(sprintf(
'Unexpected caller participant. Expected: "%s", Actual: "%s"',
$caller->id_string,
$callerConnection->getParticipant()->id_string
));
}
$twilioConferenceSettings = [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
];
$hasInboundRecording = $activity->isTypeSoftphoneInbound()
&& $roomOwner->checkSoftphoneInboundRecordPreference();
$hasOutboundRecording = $activity->isTypeSoftPhone()
&& $roomOwner->checkSoftphoneOutboundRecordPreference();
$isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;
$isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()
|| $activity->hasRecordingReasonComplianceRestricted();
if ($isRecordingEnabled && ! $isRecordingRestricted) {
$twilioConferenceSettings = array_merge($twilioConferenceSettings, [
'record' => true,
'recordingChannels' => 'dual',
'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),
'recordingStatusCallbackMethod' => 'POST',
'recordingStatusCallbackEvent' => 'in-progress completed absent',
]);
if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {
$twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()
? 'outbound'
: 'inbound';
}
}
try {
if ($activity->hasRecordingReasonCompliancePrompted()) {
$this->logger->info('[Softphone] Request compliance prompt via outbound call', [
'activity_uuid' => $activity->getUuid(),
'session' => $session->id_string,
'conference_sid' => $session->getTelephonyProviderId(),
]);
$callInstance = $twilio->createCall(
$callerId,
$calleeId,
$this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [
'session' => $session->id_string,
'connection' => $calleeConnection->id_string,
]),
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $callInstance->sid,
'parent_connection_id' => $callerConnection->getId(),
]);
} else {
$twilioParticipantConnection = $twilio->createParticipantInstance(
$session->getTelephonyProviderId(),
$callerId,
$calleeId,
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $twilioParticipantConnection->callSid,
]);
}
} catch (RestException $restException) {
if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {
throw $restException;
}
if (! $activity->isInFiniteState()) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
$message = __METHOD__ . ': activity.status -> cancelled';
} else {
$message = sprintf(
__METHOD__ . ': not setting to cancelled, activity.status is %s',
$activity->getStatus()
);
}
$this->logger->info($message, [
'activity' => $activity->id_string,
'exception' => $restException,
]);
}
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
return $this;
}
private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array
{
$session = $connection->getSession();
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => false,
'record' => false,
'waitUrl' => '',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]),
'statusCallbackEvent' => [
'initiated',
'ringing',
'answered',
'completed',
],
'statusCallbackMethod' => 'POST',
'conferenceStatusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
]),
'conferenceStatusCallbackMethod' => 'POST',
'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function applyDefaultTwilioConferenceSettings(Models\Session $session, array $override = []): array
{
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => false,
'endConferenceOnExit' => false,
'waitUrl' => '',
'statusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
// 'speaker',
]),
'statusCallbackMethod' => 'POST',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function joinTwilioConference(Connection $connection, array $options = []): TwiML\VoiceResponse
{
$session = $connection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$this->logger->debug(__METHOD__, [
'connection' => $connection->id_string,
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
]);
$voiceResponse = new TwiML\VoiceResponse();
$voiceResponse->dial()
->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]))
->conference(
$session->getIdString(),
$this->applyDefaultTwilioConferenceSettings($session, $options)
);
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'result' => $voiceResponse,
]);
return $voiceResponse;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [
'exception' => $throwable,
'user_id' => $user->getId(),
'team_id' => $team->getId(),
]);
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $roomOwner->getTeam(),
'integrationAdmin' => $roomOwner,
'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Owner resolve","depth":2,"on_screen":true,"value":"Owner resolve","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n // Setup services.\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);\n $crmService->setUser($roomOwner);\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n // Setup services.\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);\n $crmService->setUser($roomOwner);\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [\n 'exception' => $throwable,\n 'user_id' => $user->getId(),\n 'team_id' => $team->getId(),\n ]);\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $roomOwner->getTeam(),\n 'integrationAdmin' => $roomOwner,\n 'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [\n 'exception' => $throwable,\n 'user_id' => $user->getId(),\n 'team_id' => $team->getId(),\n ]);\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $roomOwner->getTeam(),\n 'integrationAdmin' => $roomOwner,\n 'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
216833808700397960
|
8095951854968406545
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 modified
Owner resolve
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
3 differences
1aee7aad
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
// Setup services.
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);
$crmService->setUser($roomOwner);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
$activity = $roomOwner->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE_INBOUND,
'provider' => Models\Activity::PROVIDER_TWILIO,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_RINGING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),
'scheduled_start_time' => $now,
]);
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $roomOwner->getRegionId(),
'is_muted' => false,
]);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $callerId,
'name' => $activity->prospect_name,
'enter_time' => $now,
]);
$caller->connections()->create([
'telephony_provider_id' => $callerCallSid,
'session_id' => $session->getId(),
'phone' => $callerId,
'direction' => 'inbound',
'type' => Connection::TYPE_PSTN,
'opened_at' => $now,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'user_id' => $roomOwner->id,
'email' => $roomOwner->email,
'name' => $roomOwner->name,
'phone_number' => $phoneNumber,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'direction' => 'outbound-api',
'phone' => $phoneNumber,
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$this->logger->debug(__METHOD__ . ': done', [
'callSid' => $data->getCallSid(),
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
$activity->refresh();
return $activity;
}
private function getVoiceRecordingReasonCode(Models\Team $team, string $phoneNumber): int
{
$this->logger->debug(__METHOD__, [
'team' => $team->id_string,
'phone' => $phoneNumber,
]);
if (
! $team->hasComplianceModeRecordingPrompt()
&& ! $team->hasComplianceModeRecordingRestrictToOneSide()
&& ! $team->hasComplianceModeRecordingRestrictRecording()
) {
return 0;
}
/** @var Models\VoiceConsentPrefix|null $voiceConsentPolicy */
$voiceConsentPolicy = Models\VoiceConsentPrefix::query()
->whereRaw("? LIKE CONCAT(destination_prefix, '%')", [$phoneNumber])
->where(static function (Eloquent\Builder $builder) use ($team): void {
$builder
->whereNull('team_id')
->orWhere('team_id', $team->getId());
})
->orderByDesc('team_id')
->first();
if (! $voiceConsentPolicy instanceof Models\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {
return 0;
}
if ($team->hasComplianceModeRecordingPrompt()) {
// This number requires consent from the other party before recording the call.
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;
}
if ($team->hasComplianceModeRecordingRestrictToOneSide()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;
}
if ($team->hasComplianceModeRecordingRestrictRecording()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;
}
return 0;
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOrganiser(Models\Activity $activity): void
{
$this->logger->debug(__METHOD__, [
'activity' => $activity->id_string,
]);
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$canBeDialed =
$activity->isTypeSoftPhone()
&& ! $activity->hasStarted()
&& $activity->isPending()
&& $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->exists();
if (! $canBeDialed) {
throw new OutOfBoundsException('Cannot make an outbound call to this activity.');
}
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->firstOrFail();
/** @var Models\Session $session */
$session = $organiserConnection->getSession();
$twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
]);
// What number will the organizer see as caller id
$callerId = $roomOwner->getSoftPhoneNumber();
if ($callerId === null) {
$callee = $activity->participants()->whereNull('user_id')->first();
// This is a bridge call. We make it look like the callee is calling the organizer
$callerId = $callee->phone_number;
}
// What number will the prospect see as caller id
$calleeId = $organiser->getPhoneNumber();
$this->logger->debug(__METHOD__ . ': twilio call', [
'calleeId' => $calleeId,
'callerId' => $callerId,
]);
$twilioParticipant = $twilio->createParticipantInstance(
$session->id_string,
$callerId,
$calleeId,
$twilioConferenceSettings
);
$organiserConnection
->setTelephonyProviderId($twilioParticipant->callSid)
->save();
$session
->setTelephonyProviderId($twilioParticipant->conferenceSid)
->save();
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'session' => $session->id_string,
]);
}
public function dialCoach(Models\Activity $activity, Models\User $user): void
{
throw new \LogicException('Not implemented yet');
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function terminate(VO\ConferenceManager\ActivityTerminate $data): void
{
if (! $data instanceof VO\ConferenceManager\SoftphoneTerminate) {
throw new UnexpectedValueException(sprintf(
'Expecting an instance of "%s", but "%s" given.',
VO\ConferenceManager\SoftphoneTerminate::class,
\get_class($data)
));
}
$this->logger->debug(__METHOD__, [
'activity' => $data->getActivity()->id_string,
'user' => $data->getUser()->id_string,
]);
$activity = $data->getActivity();
$roomOwner = $activity->getUser();
$isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();
if ($isCancelling) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
}
/** @var Models\Session $session */
$session = $activity->sessions->first();
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->whereIn('type', [
Connection::TYPE_WEBRTC,
Connection::TYPE_PSTN,
])
->firstOrFail();
if (! $organiserConnection->hasTelephonyProviderId()) {
throw new LogicException(
'Unable to terminate call as we do not yet know the organiser telephony provider id',
);
}
$twilioClient = $this->twilioClientBuilder->build($activity->getTeam());
$twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());
$session->hasTelephonyProviderId()
&& $twilioClient->terminateConference($session->getTelephonyProviderId());
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'session' => $session->id_string,
]);
}
public function answer(VO\ConferenceManager\ActivityAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundAnswer) {
return $this->answerInboundCall($data);
}
if ($data instanceof VO\ConferenceManager\SoftPhoneOutboundAnswer) {
return $this->answerOutboundCall($data);
}
throw new LogicException('Unable to answer - not supported activity answer instance');
}
private function answerInboundCall(VO\ConferenceManager\SoftPhoneInboundAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$dateTime = now();
$activity = $this->create(
new VO\ConferenceManager\SoftPhoneInboundCreate(
$data->getCallerId(),
$data->getCallSid(),
$data->getUser()
)
);
/** @var Models\Participant $activityGuest */
$activityGuest = $activity->from;
$activityGuest
->setEnterTime($dateTime)
->save();
/** @var Connection $connection */
$connection = $activityGuest->connections()
->where('type', Connection::TYPE_PSTN)
->firstOrFail();
$connection
->setTelephonyProviderId($data->getCallSid())
->setOpenedAt($dateTime)
->save();
event(new Events\Activities\Connections\Opened($connection));
return $this->joinTwilioConference($connection, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]);
}
private function answerOutboundCall(VO\ConferenceManager\SoftPhoneOutboundAnswer $data): TwiML\VoiceResponse
{
$connection = $data->getConnection();
$participant = $connection->getParticipant();
$activity = $data->getActivity();
$organiser = $activity->organizer;
$dateTime = now();
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
'connection' => $connection->id_string,
'participant' => $participant->id_string,
'activity' => $activity->id_string,
]);
if (! $participant->hasEnterTime()) {
$participant->update([
'enter_time' => $dateTime,
]);
}
$connection->update([
'is_muted' => $participant->isCoach(),
'opened_at' => $dateTime,
]);
event(new Events\Activities\Connections\Opened($connection));
if ($participant->isCoach()) {
/** @var Connection $roomOwnerConnection */
$roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();
return $this->joinTwilioConference(
$connection,
[
'coach' => $roomOwnerConnection->getTelephonyProviderId(),
]
);
}
return $this->joinTwilioConference(
$connection,
[
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]
);
}
private function determineUserCallerId(Models\User $user): string
{
if ($user->hasCallerId()) {
return $user->getCallerId();
}
// TODO: We should require a caller id to be verified before using the dialer
return $user->getSoftPhoneNumber() ?? $user->phone;
}
private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool
{
$startsWith = '[HTTP 404] Unable to create record: The requested resource';
$endsWith = 'was not found';
return Str::startsWith($exceptionMessage, $startsWith)
&& Str::endsWith($exceptionMessage, $endsWith);
}
/**
* @throws RestException
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOtherParty(Connection $callerConnection): self
{
$session = $callerConnection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$this->logger->debug(__METHOD__, [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {
throw new UnexpectedValueException('Unexpected activity type');
}
$callee = $activity->to;
$calleeId = $callee->getPhoneNumber();
/** @var Connection $calleeConnection */
$calleeConnection = $callee->connections()
->whereIn('type', [
Connection::TYPE_PSTN,
Connection::TYPE_WEBRTC,
])
->firstOrFail();
$caller = $activity->from;
// we need to take the number from the connection
// as the one we store on participant level might be the one we call to the rep via PSTN mode
$callerId = $callerConnection->getPhoneNumber();
if (! $caller->is($callerConnection->getParticipant())) {
throw new LogicException(sprintf(
'Unexpected caller participant. Expected: "%s", Actual: "%s"',
$caller->id_string,
$callerConnection->getParticipant()->id_string
));
}
$twilioConferenceSettings = [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
];
$hasInboundRecording = $activity->isTypeSoftphoneInbound()
&& $roomOwner->checkSoftphoneInboundRecordPreference();
$hasOutboundRecording = $activity->isTypeSoftPhone()
&& $roomOwner->checkSoftphoneOutboundRecordPreference();
$isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;
$isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()
|| $activity->hasRecordingReasonComplianceRestricted();
if ($isRecordingEnabled && ! $isRecordingRestricted) {
$twilioConferenceSettings = array_merge($twilioConferenceSettings, [
'record' => true,
'recordingChannels' => 'dual',
'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),
'recordingStatusCallbackMethod' => 'POST',
'recordingStatusCallbackEvent' => 'in-progress completed absent',
]);
if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {
$twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()
? 'outbound'
: 'inbound';
}
}
try {
if ($activity->hasRecordingReasonCompliancePrompted()) {
$this->logger->info('[Softphone] Request compliance prompt via outbound call', [
'activity_uuid' => $activity->getUuid(),
'session' => $session->id_string,
'conference_sid' => $session->getTelephonyProviderId(),
]);
$callInstance = $twilio->createCall(
$callerId,
$calleeId,
$this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [
'session' => $session->id_string,
'connection' => $calleeConnection->id_string,
]),
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $callInstance->sid,
'parent_connection_id' => $callerConnection->getId(),
]);
} else {
$twilioParticipantConnection = $twilio->createParticipantInstance(
$session->getTelephonyProviderId(),
$callerId,
$calleeId,
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $twilioParticipantConnection->callSid,
]);
}
} catch (RestException $restException) {
if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {
throw $restException;
}
if (! $activity->isInFiniteState()) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
$message = __METHOD__ . ': activity.status -> cancelled';
} else {
$message = sprintf(
__METHOD__ . ': not setting to cancelled, activity.status is %s',
$activity->getStatus()
);
}
$this->logger->info($message, [
'activity' => $activity->id_string,
'exception' => $restException,
]);
}
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
return $this;
}
private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array
{
$session = $connection->getSession();
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => false,
'record' => false,
'waitUrl' => '',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]),
'statusCallbackEvent' => [
'initiated',
'ringing',
'answered',
'completed',
],
'statusCallbackMethod' => 'POST',
'conferenceStatusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
]),
'conferenceStatusCallbackMethod' => 'POST',
'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function applyDefaultTwilioConferenceSettings(Models\Session $session, array $override = []): array
{
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => false,
'endConferenceOnExit' => false,
'waitUrl' => '',
'statusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
// 'speaker',
]),
'statusCallbackMethod' => 'POST',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function joinTwilioConference(Connection $connection, array $options = []): TwiML\VoiceResponse
{
$session = $connection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$this->logger->debug(__METHOD__, [
'connection' => $connection->id_string,
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
]);
$voiceResponse = new TwiML\VoiceResponse();
$voiceResponse->dial()
->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]))
->conference(
$session->getIdString(),
$this->applyDefaultTwilioConferenceSettings($session, $options)
);
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'result' => $voiceResponse,
]);
return $voiceResponse;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [
'exception' => $throwable,
'user_id' => $user->getId(),
'team_id' => $team->getId(),
]);
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $roomOwner->getTeam(),
'integrationAdmin' => $roomOwner,
'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71735
|
2573
|
50
|
2026-05-26T07:06:08.582234+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779168582_m2.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 modified
Owner resolve
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
3 differences
1aee7aad
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
// Setup services.
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);
$crmService->setUser($roomOwner);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
$activity = $roomOwner->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE_INBOUND,
'provider' => Models\Activity::PROVIDER_TWILIO,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_RINGING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),
'scheduled_start_time' => $now,
]);
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $roomOwner->getRegionId(),
'is_muted' => false,
]);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $callerId,
'name' => $activity->prospect_name,
'enter_time' => $now,
]);
$caller->connections()->create([
'telephony_provider_id' => $callerCallSid,
'session_id' => $session->getId(),
'phone' => $callerId,
'direction' => 'inbound',
'type' => Connection::TYPE_PSTN,
'opened_at' => $now,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'user_id' => $roomOwner->id,
'email' => $roomOwner->email,
'name' => $roomOwner->name,
'phone_number' => $phoneNumber,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'direction' => 'outbound-api',
'phone' => $phoneNumber,
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$this->logger->debug(__METHOD__ . ': done', [
'callSid' => $data->getCallSid(),
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
$activity->refresh();
return $activity;
}
private function getVoiceRecordingReasonCode(Models\Team $team, string $phoneNumber): int
{
$this->logger->debug(__METHOD__, [
'team' => $team->id_string,
'phone' => $phoneNumber,
]);
if (
! $team->hasComplianceModeRecordingPrompt()
&& ! $team->hasComplianceModeRecordingRestrictToOneSide()
&& ! $team->hasComplianceModeRecordingRestrictRecording()
) {
return 0;
}
/** @var Models\VoiceConsentPrefix|null $voiceConsentPolicy */
$voiceConsentPolicy = Models\VoiceConsentPrefix::query()
->whereRaw("? LIKE CONCAT(destination_prefix, '%')", [$phoneNumber])
->where(static function (Eloquent\Builder $builder) use ($team): void {
$builder
->whereNull('team_id')
->orWhere('team_id', $team->getId());
})
->orderByDesc('team_id')
->first();
if (! $voiceConsentPolicy instanceof Models\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {
return 0;
}
if ($team->hasComplianceModeRecordingPrompt()) {
// This number requires consent from the other party before recording the call.
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;
}
if ($team->hasComplianceModeRecordingRestrictToOneSide()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;
}
if ($team->hasComplianceModeRecordingRestrictRecording()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;
}
return 0;
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOrganiser(Models\Activity $activity): void
{
$this->logger->debug(__METHOD__, [
'activity' => $activity->id_string,
]);
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$canBeDialed =
$activity->isTypeSoftPhone()
&& ! $activity->hasStarted()
&& $activity->isPending()
&& $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->exists();
if (! $canBeDialed) {
throw new OutOfBoundsException('Cannot make an outbound call to this activity.');
}
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->firstOrFail();
/** @var Models\Session $session */
$session = $organiserConnection->getSession();
$twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
]);
// What number will the organizer see as caller id
$callerId = $roomOwner->getSoftPhoneNumber();
if ($callerId === null) {
$callee = $activity->participants()->whereNull('user_id')->first();
// This is a bridge call. We make it look like the callee is calling the organizer
$callerId = $callee->phone_number;
}
// What number will the prospect see as caller id
$calleeId = $organiser->getPhoneNumber();
$this->logger->debug(__METHOD__ . ': twilio call', [
'calleeId' => $calleeId,
'callerId' => $callerId,
]);
$twilioParticipant = $twilio->createParticipantInstance(
$session->id_string,
$callerId,
$calleeId,
$twilioConferenceSettings
);
$organiserConnection
->setTelephonyProviderId($twilioParticipant->callSid)
->save();
$session
->setTelephonyProviderId($twilioParticipant->conferenceSid)
->save();
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'session' => $session->id_string,
]);
}
public function dialCoach(Models\Activity $activity, Models\User $user): void
{
throw new \LogicException('Not implemented yet');
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function terminate(VO\ConferenceManager\ActivityTerminate $data): void
{
if (! $data instanceof VO\ConferenceManager\SoftphoneTerminate) {
throw new UnexpectedValueException(sprintf(
'Expecting an instance of "%s", but "%s" given.',
VO\ConferenceManager\SoftphoneTerminate::class,
\get_class($data)
));
}
$this->logger->debug(__METHOD__, [
'activity' => $data->getActivity()->id_string,
'user' => $data->getUser()->id_string,
]);
$activity = $data->getActivity();
$roomOwner = $activity->getUser();
$isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();
if ($isCancelling) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
}
/** @var Models\Session $session */
$session = $activity->sessions->first();
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->whereIn('type', [
Connection::TYPE_WEBRTC,
Connection::TYPE_PSTN,
])
->firstOrFail();
if (! $organiserConnection->hasTelephonyProviderId()) {
throw new LogicException(
'Unable to terminate call as we do not yet know the organiser telephony provider id',
);
}
$twilioClient = $this->twilioClientBuilder->build($activity->getTeam());
$twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());
$session->hasTelephonyProviderId()
&& $twilioClient->terminateConference($session->getTelephonyProviderId());
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'session' => $session->id_string,
]);
}
public function answer(VO\ConferenceManager\ActivityAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundAnswer) {
return $this->answerInboundCall($data);
}
if ($data instanceof VO\ConferenceManager\SoftPhoneOutboundAnswer) {
return $this->answerOutboundCall($data);
}
throw new LogicException('Unable to answer - not supported activity answer instance');
}
private function answerInboundCall(VO\ConferenceManager\SoftPhoneInboundAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$dateTime = now();
$activity = $this->create(
new VO\ConferenceManager\SoftPhoneInboundCreate(
$data->getCallerId(),
$data->getCallSid(),
$data->getUser()
)
);
/** @var Models\Participant $activityGuest */
$activityGuest = $activity->from;
$activityGuest
->setEnterTime($dateTime)
->save();
/** @var Connection $connection */
$connection = $activityGuest->connections()
->where('type', Connection::TYPE_PSTN)
->firstOrFail();
$connection
->setTelephonyProviderId($data->getCallSid())
->setOpenedAt($dateTime)
->save();
event(new Events\Activities\Connections\Opened($connection));
return $this->joinTwilioConference($connection, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]);
}
private function answerOutboundCall(VO\ConferenceManager\SoftPhoneOutboundAnswer $data): TwiML\VoiceResponse
{
$connection = $data->getConnection();
$participant = $connection->getParticipant();
$activity = $data->getActivity();
$organiser = $activity->organizer;
$dateTime = now();
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
'connection' => $connection->id_string,
'participant' => $participant->id_string,
'activity' => $activity->id_string,
]);
if (! $participant->hasEnterTime()) {
$participant->update([
'enter_time' => $dateTime,
]);
}
$connection->update([
'is_muted' => $participant->isCoach(),
'opened_at' => $dateTime,
]);
event(new Events\Activities\Connections\Opened($connection));
if ($participant->isCoach()) {
/** @var Connection $roomOwnerConnection */
$roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();
return $this->joinTwilioConference(
$connection,
[
'coach' => $roomOwnerConnection->getTelephonyProviderId(),
]
);
}
return $this->joinTwilioConference(
$connection,
[
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]
);
}
private function determineUserCallerId(Models\User $user): string
{
if ($user->hasCallerId()) {
return $user->getCallerId();
}
// TODO: We should require a caller id to be verified before using the dialer
return $user->getSoftPhoneNumber() ?? $user->phone;
}
private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool
{
$startsWith = '[HTTP 404] Unable to create record: The requested resource';
$endsWith = 'was not found';
return Str::startsWith($exceptionMessage, $startsWith)
&& Str::endsWith($exceptionMessage, $endsWith);
}
/**
* @throws RestException
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOtherParty(Connection $callerConnection): self
{
$session = $callerConnection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$this->logger->debug(__METHOD__, [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {
throw new UnexpectedValueException('Unexpected activity type');
}
$callee = $activity->to;
$calleeId = $callee->getPhoneNumber();
/** @var Connection $calleeConnection */
$calleeConnection = $callee->connections()
->whereIn('type', [
Connection::TYPE_PSTN,
Connection::TYPE_WEBRTC,
])
->firstOrFail();
$caller = $activity->from;
// we need to take the number from the connection
// as the one we store on participant level might be the one we call to the rep via PSTN mode
$callerId = $callerConnection->getPhoneNumber();
if (! $caller->is($callerConnection->getParticipant())) {
throw new LogicException(sprintf(
'Unexpected caller participant. Expected: "%s", Actual: "%s"',
$caller->id_string,
$callerConnection->getParticipant()->id_string
));
}
$twilioConferenceSettings = [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
];
$hasInboundRecording = $activity->isTypeSoftphoneInbound()
&& $roomOwner->checkSoftphoneInboundRecordPreference();
$hasOutboundRecording = $activity->isTypeSoftPhone()
&& $roomOwner->checkSoftphoneOutboundRecordPreference();
$isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;
$isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()
|| $activity->hasRecordingReasonComplianceRestricted();
if ($isRecordingEnabled && ! $isRecordingRestricted) {
$twilioConferenceSettings = array_merge($twilioConferenceSettings, [
'record' => true,
'recordingChannels' => 'dual',
'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),
'recordingStatusCallbackMethod' => 'POST',
'recordingStatusCallbackEvent' => 'in-progress completed absent',
]);
if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {
$twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()
? 'outbound'
: 'inbound';
}
}
try {
if ($activity->hasRecordingReasonCompliancePrompted()) {
$this->logger->info('[Softphone] Request compliance prompt via outbound call', [
'activity_uuid' => $activity->getUuid(),
'session' => $session->id_string,
'conference_sid' => $session->getTelephonyProviderId(),
]);
$callInstance = $twilio->createCall(
$callerId,
$calleeId,
$this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [
'session' => $session->id_string,
'connection' => $calleeConnection->id_string,
]),
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $callInstance->sid,
'parent_connection_id' => $callerConnection->getId(),
]);
} else {
$twilioParticipantConnection = $twilio->createParticipantInstance(
$session->getTelephonyProviderId(),
$callerId,
$calleeId,
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $twilioParticipantConnection->callSid,
]);
}
} catch (RestException $restException) {
if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {
throw $restException;
}
if (! $activity->isInFiniteState()) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
$message = __METHOD__ . ': activity.status -> cancelled';
} else {
$message = sprintf(
__METHOD__ . ': not setting to cancelled, activity.status is %s',
$activity->getStatus()
);
}
$this->logger->info($message, [
'activity' => $activity->id_string,
'exception' => $restException,
]);
}
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
return $this;
}
private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array
{
$session = $connection->getSession();
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => false,
'record' => false,
'waitUrl' => '',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]),
'statusCallbackEvent' => [
'initiated',
'ringing',
'answered',
'completed',
],
'statusCallbackMethod' => 'POST',
'conferenceStatusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
]),
'conferenceStatusCallbackMethod' => 'POST',
'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function applyDefaultTwilioConferenceSettings(Models\Session $session, array $override = []): array
{
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => false,
'endConferenceOnExit' => false,
'waitUrl' => '',
'statusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
// 'speaker',
]),
'statusCallbackMethod' => 'POST',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function joinTwilioConference(Connection $connection, array $options = []): TwiML\VoiceResponse
{
$session = $connection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$this->logger->debug(__METHOD__, [
'connection' => $connection->id_string,
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
]);
$voiceResponse = new TwiML\VoiceResponse();
$voiceResponse->dial()
->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]))
->conference(
$session->getIdString(),
$this->applyDefaultTwilioConferenceSettings($session, $options)
);
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'result' => $voiceResponse,
]);
return $voiceResponse;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [
'exception' => $throwable,
'user_id' => $user->getId(),
'team_id' => $team->getId(),
]);
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $roomOwner->getTeam(),
'integrationAdmin' => $roomOwner,
'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.51329786,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.5375665,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.49268618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.5013298,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.32446808,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Owner resolve","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.3238032,"height":0.087789305},"on_screen":true,"value":"Owner resolve","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.5711436,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3 differences","depth":1,"bounds":{"left":0.5511968,"top":0.41181165,"width":0.027260639,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.022606382,"height":0.013567438},"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n // Setup services.\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);\n $crmService->setUser($roomOwner);\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.26230052,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n // Setup services.\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);\n $crmService->setUser($roomOwner);\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [\n 'exception' => $throwable,\n 'user_id' => $user->getId(),\n 'team_id' => $team->getId(),\n ]);\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $roomOwner->getTeam(),\n 'integrationAdmin' => $roomOwner,\n 'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.26695478,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [\n 'exception' => $throwable,\n 'user_id' => $user->getId(),\n 'team_id' => $team->getId(),\n ]);\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $roomOwner->getTeam(),\n 'integrationAdmin' => $roomOwner,\n 'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.50764626,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"bounds":{"left":0.5355718,"top":0.65363127,"width":0.044215426,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"bounds":{"left":0.39960107,"top":0.13248204,"width":0.035904255,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
216833808700397960
|
8095951854968406545
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
8 modified
Owner resolve
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
3 differences
1aee7aad
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
// Setup services.
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);
$crmService->setUser($roomOwner);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
$activity = $roomOwner->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE_INBOUND,
'provider' => Models\Activity::PROVIDER_TWILIO,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_RINGING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),
'scheduled_start_time' => $now,
]);
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $roomOwner->getRegionId(),
'is_muted' => false,
]);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $callerId,
'name' => $activity->prospect_name,
'enter_time' => $now,
]);
$caller->connections()->create([
'telephony_provider_id' => $callerCallSid,
'session_id' => $session->getId(),
'phone' => $callerId,
'direction' => 'inbound',
'type' => Connection::TYPE_PSTN,
'opened_at' => $now,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'user_id' => $roomOwner->id,
'email' => $roomOwner->email,
'name' => $roomOwner->name,
'phone_number' => $phoneNumber,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'direction' => 'outbound-api',
'phone' => $phoneNumber,
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$this->logger->debug(__METHOD__ . ': done', [
'callSid' => $data->getCallSid(),
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
$activity->refresh();
return $activity;
}
private function getVoiceRecordingReasonCode(Models\Team $team, string $phoneNumber): int
{
$this->logger->debug(__METHOD__, [
'team' => $team->id_string,
'phone' => $phoneNumber,
]);
if (
! $team->hasComplianceModeRecordingPrompt()
&& ! $team->hasComplianceModeRecordingRestrictToOneSide()
&& ! $team->hasComplianceModeRecordingRestrictRecording()
) {
return 0;
}
/** @var Models\VoiceConsentPrefix|null $voiceConsentPolicy */
$voiceConsentPolicy = Models\VoiceConsentPrefix::query()
->whereRaw("? LIKE CONCAT(destination_prefix, '%')", [$phoneNumber])
->where(static function (Eloquent\Builder $builder) use ($team): void {
$builder
->whereNull('team_id')
->orWhere('team_id', $team->getId());
})
->orderByDesc('team_id')
->first();
if (! $voiceConsentPolicy instanceof Models\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {
return 0;
}
if ($team->hasComplianceModeRecordingPrompt()) {
// This number requires consent from the other party before recording the call.
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;
}
if ($team->hasComplianceModeRecordingRestrictToOneSide()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;
}
if ($team->hasComplianceModeRecordingRestrictRecording()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;
}
return 0;
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOrganiser(Models\Activity $activity): void
{
$this->logger->debug(__METHOD__, [
'activity' => $activity->id_string,
]);
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$canBeDialed =
$activity->isTypeSoftPhone()
&& ! $activity->hasStarted()
&& $activity->isPending()
&& $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->exists();
if (! $canBeDialed) {
throw new OutOfBoundsException('Cannot make an outbound call to this activity.');
}
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->firstOrFail();
/** @var Models\Session $session */
$session = $organiserConnection->getSession();
$twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
]);
// What number will the organizer see as caller id
$callerId = $roomOwner->getSoftPhoneNumber();
if ($callerId === null) {
$callee = $activity->participants()->whereNull('user_id')->first();
// This is a bridge call. We make it look like the callee is calling the organizer
$callerId = $callee->phone_number;
}
// What number will the prospect see as caller id
$calleeId = $organiser->getPhoneNumber();
$this->logger->debug(__METHOD__ . ': twilio call', [
'calleeId' => $calleeId,
'callerId' => $callerId,
]);
$twilioParticipant = $twilio->createParticipantInstance(
$session->id_string,
$callerId,
$calleeId,
$twilioConferenceSettings
);
$organiserConnection
->setTelephonyProviderId($twilioParticipant->callSid)
->save();
$session
->setTelephonyProviderId($twilioParticipant->conferenceSid)
->save();
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'session' => $session->id_string,
]);
}
public function dialCoach(Models\Activity $activity, Models\User $user): void
{
throw new \LogicException('Not implemented yet');
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function terminate(VO\ConferenceManager\ActivityTerminate $data): void
{
if (! $data instanceof VO\ConferenceManager\SoftphoneTerminate) {
throw new UnexpectedValueException(sprintf(
'Expecting an instance of "%s", but "%s" given.',
VO\ConferenceManager\SoftphoneTerminate::class,
\get_class($data)
));
}
$this->logger->debug(__METHOD__, [
'activity' => $data->getActivity()->id_string,
'user' => $data->getUser()->id_string,
]);
$activity = $data->getActivity();
$roomOwner = $activity->getUser();
$isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();
if ($isCancelling) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
}
/** @var Models\Session $session */
$session = $activity->sessions->first();
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->whereIn('type', [
Connection::TYPE_WEBRTC,
Connection::TYPE_PSTN,
])
->firstOrFail();
if (! $organiserConnection->hasTelephonyProviderId()) {
throw new LogicException(
'Unable to terminate call as we do not yet know the organiser telephony provider id',
);
}
$twilioClient = $this->twilioClientBuilder->build($activity->getTeam());
$twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());
$session->hasTelephonyProviderId()
&& $twilioClient->terminateConference($session->getTelephonyProviderId());
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'session' => $session->id_string,
]);
}
public function answer(VO\ConferenceManager\ActivityAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundAnswer) {
return $this->answerInboundCall($data);
}
if ($data instanceof VO\ConferenceManager\SoftPhoneOutboundAnswer) {
return $this->answerOutboundCall($data);
}
throw new LogicException('Unable to answer - not supported activity answer instance');
}
private function answerInboundCall(VO\ConferenceManager\SoftPhoneInboundAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$dateTime = now();
$activity = $this->create(
new VO\ConferenceManager\SoftPhoneInboundCreate(
$data->getCallerId(),
$data->getCallSid(),
$data->getUser()
)
);
/** @var Models\Participant $activityGuest */
$activityGuest = $activity->from;
$activityGuest
->setEnterTime($dateTime)
->save();
/** @var Connection $connection */
$connection = $activityGuest->connections()
->where('type', Connection::TYPE_PSTN)
->firstOrFail();
$connection
->setTelephonyProviderId($data->getCallSid())
->setOpenedAt($dateTime)
->save();
event(new Events\Activities\Connections\Opened($connection));
return $this->joinTwilioConference($connection, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]);
}
private function answerOutboundCall(VO\ConferenceManager\SoftPhoneOutboundAnswer $data): TwiML\VoiceResponse
{
$connection = $data->getConnection();
$participant = $connection->getParticipant();
$activity = $data->getActivity();
$organiser = $activity->organizer;
$dateTime = now();
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
'connection' => $connection->id_string,
'participant' => $participant->id_string,
'activity' => $activity->id_string,
]);
if (! $participant->hasEnterTime()) {
$participant->update([
'enter_time' => $dateTime,
]);
}
$connection->update([
'is_muted' => $participant->isCoach(),
'opened_at' => $dateTime,
]);
event(new Events\Activities\Connections\Opened($connection));
if ($participant->isCoach()) {
/** @var Connection $roomOwnerConnection */
$roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();
return $this->joinTwilioConference(
$connection,
[
'coach' => $roomOwnerConnection->getTelephonyProviderId(),
]
);
}
return $this->joinTwilioConference(
$connection,
[
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]
);
}
private function determineUserCallerId(Models\User $user): string
{
if ($user->hasCallerId()) {
return $user->getCallerId();
}
// TODO: We should require a caller id to be verified before using the dialer
return $user->getSoftPhoneNumber() ?? $user->phone;
}
private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool
{
$startsWith = '[HTTP 404] Unable to create record: The requested resource';
$endsWith = 'was not found';
return Str::startsWith($exceptionMessage, $startsWith)
&& Str::endsWith($exceptionMessage, $endsWith);
}
/**
* @throws RestException
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOtherParty(Connection $callerConnection): self
{
$session = $callerConnection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$this->logger->debug(__METHOD__, [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {
throw new UnexpectedValueException('Unexpected activity type');
}
$callee = $activity->to;
$calleeId = $callee->getPhoneNumber();
/** @var Connection $calleeConnection */
$calleeConnection = $callee->connections()
->whereIn('type', [
Connection::TYPE_PSTN,
Connection::TYPE_WEBRTC,
])
->firstOrFail();
$caller = $activity->from;
// we need to take the number from the connection
// as the one we store on participant level might be the one we call to the rep via PSTN mode
$callerId = $callerConnection->getPhoneNumber();
if (! $caller->is($callerConnection->getParticipant())) {
throw new LogicException(sprintf(
'Unexpected caller participant. Expected: "%s", Actual: "%s"',
$caller->id_string,
$callerConnection->getParticipant()->id_string
));
}
$twilioConferenceSettings = [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
];
$hasInboundRecording = $activity->isTypeSoftphoneInbound()
&& $roomOwner->checkSoftphoneInboundRecordPreference();
$hasOutboundRecording = $activity->isTypeSoftPhone()
&& $roomOwner->checkSoftphoneOutboundRecordPreference();
$isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;
$isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()
|| $activity->hasRecordingReasonComplianceRestricted();
if ($isRecordingEnabled && ! $isRecordingRestricted) {
$twilioConferenceSettings = array_merge($twilioConferenceSettings, [
'record' => true,
'recordingChannels' => 'dual',
'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),
'recordingStatusCallbackMethod' => 'POST',
'recordingStatusCallbackEvent' => 'in-progress completed absent',
]);
if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {
$twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()
? 'outbound'
: 'inbound';
}
}
try {
if ($activity->hasRecordingReasonCompliancePrompted()) {
$this->logger->info('[Softphone] Request compliance prompt via outbound call', [
'activity_uuid' => $activity->getUuid(),
'session' => $session->id_string,
'conference_sid' => $session->getTelephonyProviderId(),
]);
$callInstance = $twilio->createCall(
$callerId,
$calleeId,
$this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [
'session' => $session->id_string,
'connection' => $calleeConnection->id_string,
]),
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $callInstance->sid,
'parent_connection_id' => $callerConnection->getId(),
]);
} else {
$twilioParticipantConnection = $twilio->createParticipantInstance(
$session->getTelephonyProviderId(),
$callerId,
$calleeId,
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $twilioParticipantConnection->callSid,
]);
}
} catch (RestException $restException) {
if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {
throw $restException;
}
if (! $activity->isInFiniteState()) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
$message = __METHOD__ . ': activity.status -> cancelled';
} else {
$message = sprintf(
__METHOD__ . ': not setting to cancelled, activity.status is %s',
$activity->getStatus()
);
}
$this->logger->info($message, [
'activity' => $activity->id_string,
'exception' => $restException,
]);
}
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
return $this;
}
private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array
{
$session = $connection->getSession();
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => false,
'record' => false,
'waitUrl' => '',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]),
'statusCallbackEvent' => [
'initiated',
'ringing',
'answered',
'completed',
],
'statusCallbackMethod' => 'POST',
'conferenceStatusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
]),
'conferenceStatusCallbackMethod' => 'POST',
'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function applyDefaultTwilioConferenceSettings(Models\Session $session, array $override = []): array
{
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => false,
'endConferenceOnExit' => false,
'waitUrl' => '',
'statusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
// 'speaker',
]),
'statusCallbackMethod' => 'POST',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function joinTwilioConference(Connection $connection, array $options = []): TwiML\VoiceResponse
{
$session = $connection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$this->logger->debug(__METHOD__, [
'connection' => $connection->id_string,
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
]);
$voiceResponse = new TwiML\VoiceResponse();
$voiceResponse->dial()
->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]))
->conference(
$session->getIdString(),
$this->applyDefaultTwilioConferenceSettings($session, $options)
);
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'result' => $voiceResponse,
]);
return $voiceResponse;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [
'exception' => $throwable,
'user_id' => $user->getId(),
'team_id' => $team->getId(),
]);
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $roomOwner->getTeam(),
'integrationAdmin' => $roomOwner,
'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
...
|
71733
|
NULL
|
NULL
|
NULL
|
|
71734
|
2572
|
13
|
2026-05-26T07:06:03.809011+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779163809_m1.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
13 files, folder partially checked
app 10 files, folder partially checked
Component 2 files, folder checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Jobs/Activity/Import 3 files, folder checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
Listeners/Teams 1 file, folder checked
UpdateSalesforceAccount.php, final class checked
Services 2 files, folder partially checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder not checked
.env.local not checked
app 10 files, folder partially checked
Component 2 files, folder checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Jobs/Activity/Import 3 files, folder checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
Listeners/Teams 1 file, folder checked
UpdateSalesforceAccount.php, final class checked
Services 2 files, folder partially checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
Component 2 files, folder checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
CreateActivityFromUploadService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
ActivityController.php, final class checked
Jobs/Activity/Import 3 files, folder checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
Listeners/Teams 1 file, folder checked
UpdateSalesforceAccount.php, final class checked
UpdateSalesforceAccount.php, final class checked
Services 2 files, folder partially checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
TextMessagingService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder not checked
.env.local not checked
8 modified
JY-20963 fix deleted object import
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
3 differences
1aee7aad
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
// Setup services.
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);
$crmService->setUser($roomOwner);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
$activity = $roomOwner->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE_INBOUND,
'provider' => Models\Activity::PROVIDER_TWILIO,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_RINGING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),
'scheduled_start_time' => $now,
]);
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $roomOwner->getRegionId(),
'is_muted' => false,
]);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $callerId,
'name' => $activity->prospect_name,
'enter_time' => $now,
]);
$caller->connections()->create([
'telephony_provider_id' => $callerCallSid,
'session_id' => $session->getId(),
'phone' => $callerId,
'direction' => 'inbound',
'type' => Connection::TYPE_PSTN,
'opened_at' => $now,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'user_id' => $roomOwner->id,
'email' => $roomOwner->email,
'name' => $roomOwner->name,
'phone_number' => $phoneNumber,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'direction' => 'outbound-api',
'phone' => $phoneNumber,
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$this->logger->debug(__METHOD__ . ': done', [
'callSid' => $data->getCallSid(),
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
$activity->refresh();
return $activity;
}
private function getVoiceRecordingReasonCode(Models\Team $team, string $phoneNumber): int
{
$this->logger->debug(__METHOD__, [
'team' => $team->id_string,
'phone' => $phoneNumber,
]);
if (
! $team->hasComplianceModeRecordingPrompt()
&& ! $team->hasComplianceModeRecordingRestrictToOneSide()
&& ! $team->hasComplianceModeRecordingRestrictRecording()
) {
return 0;
}
/** @var Models\VoiceConsentPrefix|null $voiceConsentPolicy */
$voiceConsentPolicy = Models\VoiceConsentPrefix::query()
->whereRaw("? LIKE CONCAT(destination_prefix, '%')", [$phoneNumber])
->where(static function (Eloquent\Builder $builder) use ($team): void {
$builder
->whereNull('team_id')
->orWhere('team_id', $team->getId());
})
->orderByDesc('team_id')
->first();
if (! $voiceConsentPolicy instanceof Models\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {
return 0;
}
if ($team->hasComplianceModeRecordingPrompt()) {
// This number requires consent from the other party before recording the call.
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;
}
if ($team->hasComplianceModeRecordingRestrictToOneSide()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;
}
if ($team->hasComplianceModeRecordingRestrictRecording()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;
}
return 0;
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOrganiser(Models\Activity $activity): void
{
$this->logger->debug(__METHOD__, [
'activity' => $activity->id_string,
]);
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$canBeDialed =
$activity->isTypeSoftPhone()
&& ! $activity->hasStarted()
&& $activity->isPending()
&& $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->exists();
if (! $canBeDialed) {
throw new OutOfBoundsException('Cannot make an outbound call to this activity.');
}
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->firstOrFail();
/** @var Models\Session $session */
$session = $organiserConnection->getSession();
$twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
]);
// What number will the organizer see as caller id
$callerId = $roomOwner->getSoftPhoneNumber();
if ($callerId === null) {
$callee = $activity->participants()->whereNull('user_id')->first();
// This is a bridge call. We make it look like the callee is calling the organizer
$callerId = $callee->phone_number;
}
// What number will the prospect see as caller id
$calleeId = $organiser->getPhoneNumber();
$this->logger->debug(__METHOD__ . ': twilio call', [
'calleeId' => $calleeId,
'callerId' => $callerId,
]);
$twilioParticipant = $twilio->createParticipantInstance(
$session->id_string,
$callerId,
$calleeId,
$twilioConferenceSettings
);
$organiserConnection
->setTelephonyProviderId($twilioParticipant->callSid)
->save();
$session
->setTelephonyProviderId($twilioParticipant->conferenceSid)
->save();
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'session' => $session->id_string,
]);
}
public function dialCoach(Models\Activity $activity, Models\User $user): void
{
throw new \LogicException('Not implemented yet');
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function terminate(VO\ConferenceManager\ActivityTerminate $data): void
{
if (! $data instanceof VO\ConferenceManager\SoftphoneTerminate) {
throw new UnexpectedValueException(sprintf(
'Expecting an instance of "%s", but "%s" given.',
VO\ConferenceManager\SoftphoneTerminate::class,
\get_class($data)
));
}
$this->logger->debug(__METHOD__, [
'activity' => $data->getActivity()->id_string,
'user' => $data->getUser()->id_string,
]);
$activity = $data->getActivity();
$roomOwner = $activity->getUser();
$isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();
if ($isCancelling) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
}
/** @var Models\Session $session */
$session = $activity->sessions->first();
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->whereIn('type', [
Connection::TYPE_WEBRTC,
Connection::TYPE_PSTN,
])
->firstOrFail();
if (! $organiserConnection->hasTelephonyProviderId()) {
throw new LogicException(
'Unable to terminate call as we do not yet know the organiser telephony provider id',
);
}
$twilioClient = $this->twilioClientBuilder->build($activity->getTeam());
$twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());
$session->hasTelephonyProviderId()
&& $twilioClient->terminateConference($session->getTelephonyProviderId());
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'session' => $session->id_string,
]);
}
public function answer(VO\ConferenceManager\ActivityAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundAnswer) {
return $this->answerInboundCall($data);
}
if ($data instanceof VO\ConferenceManager\SoftPhoneOutboundAnswer) {
return $this->answerOutboundCall($data);
}
throw new LogicException('Unable to answer - not supported activity answer instance');
}
private function answerInboundCall(VO\ConferenceManager\SoftPhoneInboundAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$dateTime = now();
$activity = $this->create(
new VO\ConferenceManager\SoftPhoneInboundCreate(
$data->getCallerId(),
$data->getCallSid(),
$data->getUser()
)
);
/** @var Models\Participant $activityGuest */
$activityGuest = $activity->from;
$activityGuest
->setEnterTime($dateTime)
->save();
/** @var Connection $connection */
$connection = $activityGuest->connections()
->where('type', Connection::TYPE_PSTN)
->firstOrFail();
$connection
->setTelephonyProviderId($data->getCallSid())
->setOpenedAt($dateTime)
->save();
event(new Events\Activities\Connections\Opened($connection));
return $this->joinTwilioConference($connection, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]);
}
private function answerOutboundCall(VO\ConferenceManager\SoftPhoneOutboundAnswer $data): TwiML\VoiceResponse
{
$connection = $data->getConnection();
$participant = $connection->getParticipant();
$activity = $data->getActivity();
$organiser = $activity->organizer;
$dateTime = now();
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
'connection' => $connection->id_string,
'participant' => $participant->id_string,
'activity' => $activity->id_string,
]);
if (! $participant->hasEnterTime()) {
$participant->update([
'enter_time' => $dateTime,
]);
}
$connection->update([
'is_muted' => $participant->isCoach(),
'opened_at' => $dateTime,
]);
event(new Events\Activities\Connections\Opened($connection));
if ($participant->isCoach()) {
/** @var Connection $roomOwnerConnection */
$roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();
return $this->joinTwilioConference(
$connection,
[
'coach' => $roomOwnerConnection->getTelephonyProviderId(),
]
);
}
return $this->joinTwilioConference(
$connection,
[
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]
);
}
private function determineUserCallerId(Models\User $user): string
{
if ($user->hasCallerId()) {
return $user->getCallerId();
}
// TODO: We should require a caller id to be verified before using the dialer
return $user->getSoftPhoneNumber() ?? $user->phone;
}
private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool
{
$startsWith = '[HTTP 404] Unable to create record: The requested resource';
$endsWith = 'was not found';
return Str::startsWith($exceptionMessage, $startsWith)
&& Str::endsWith($exceptionMessage, $endsWith);
}
/**
* @throws RestException
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOtherParty(Connection $callerConnection): self
{
$session = $callerConnection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$this->logger->debug(__METHOD__, [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {
throw new UnexpectedValueException('Unexpected activity type');
}
$callee = $activity->to;
$calleeId = $callee->getPhoneNumber();
/** @var Connection $calleeConnection */
$calleeConnection = $callee->connections()
->whereIn('type', [
Connection::TYPE_PSTN,
Connection::TYPE_WEBRTC,
])
->firstOrFail();
$caller = $activity->from;
// we need to take the number from the connection
// as the one we store on participant level might be the one we call to the rep via PSTN mode
$callerId = $callerConnection->getPhoneNumber();
if (! $caller->is($callerConnection->getParticipant())) {
throw new LogicException(sprintf(
'Unexpected caller participant. Expected: "%s", Actual: "%s"',
$caller->id_string,
$callerConnection->getParticipant()->id_string
));
}
$twilioConferenceSettings = [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
];
$hasInboundRecording = $activity->isTypeSoftphoneInbound()
&& $roomOwner->checkSoftphoneInboundRecordPreference();
$hasOutboundRecording = $activity->isTypeSoftPhone()
&& $roomOwner->checkSoftphoneOutboundRecordPreference();
$isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;
$isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()
|| $activity->hasRecordingReasonComplianceRestricted();
if ($isRecordingEnabled && ! $isRecordingRestricted) {
$twilioConferenceSettings = array_merge($twilioConferenceSettings, [
'record' => true,
'recordingChannels' => 'dual',
'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),
'recordingStatusCallbackMethod' => 'POST',
'recordingStatusCallbackEvent' => 'in-progress completed absent',
]);
if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {
$twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()
? 'outbound'
: 'inbound';
}
}
try {
if ($activity->hasRecordingReasonCompliancePrompted()) {
$this->logger->info('[Softphone] Request compliance prompt via outbound call', [
'activity_uuid' => $activity->getUuid(),
'session' => $session->id_string,
'conference_sid' => $session->getTelephonyProviderId(),
]);
$callInstance = $twilio->createCall(
$callerId,
$calleeId,
$this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [
'session' => $session->id_string,
'connection' => $calleeConnection->id_string,
]),
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $callInstance->sid,
'parent_connection_id' => $callerConnection->getId(),
]);
} else {
$twilioParticipantConnection = $twilio->createParticipantInstance(
$session->getTelephonyProviderId(),
$callerId,
$calleeId,
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $twilioParticipantConnection->callSid,
]);
}
} catch (RestException $restException) {
if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {
throw $restException;
}
if (! $activity->isInFiniteState()) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
$message = __METHOD__ . ': activity.status -> cancelled';
} else {
$message = sprintf(
__METHOD__ . ': not setting to cancelled, activity.status is %s',
$activity->getStatus()
);
}
$this->logger->info($message, [
'activity' => $activity->id_string,
'exception' => $restException,
]);
}
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
return $this;
}
private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array
{
$session = $connection->getSession();
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => false,
'record' => false,
'waitUrl' => '',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]),
'statusCallbackEvent' => [
'initiated',
'ringing',
'answered',
'completed',
],
'statusCallbackMethod' => 'POST',
'conferenceStatusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
]),
'conferenceStatusCallbackMethod' => 'POST',
'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function applyDefaultTwilioConferenceSettings(Models\Session $session, array $override = []): array
{
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => false,
'endConferenceOnExit' => false,
'waitUrl' => '',
'statusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
// 'speaker',
]),
'statusCallbackMethod' => 'POST',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function joinTwilioConference(Connection $connection, array $options = []): TwiML\VoiceResponse
{
$session = $connection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$this->logger->debug(__METHOD__, [
'connection' => $connection->id_string,
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
]);
$voiceResponse = new TwiML\VoiceResponse();
$voiceResponse->dial()
->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]))
->conference(
$session->getIdString(),
$this->applyDefaultTwilioConferenceSettings($session, $options)
);
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'result' => $voiceResponse,
]);
return $voiceResponse;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [
'exception' => $throwable,
'user_id' => $user->getId(),
'team_id' => $team->getId(),
]);
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' ...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 10 files, folder partially checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component 2 files, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Twilio/Conference/ConferenceManager 1 file, folder checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SoftPhoneManager.php, final class checked","depth":8,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Uploader/Services 1 file, folder checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"CreateActivityFromUploadService.php, class checked","depth":8,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Activity/Import 3 files, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HydrateCrmDataByExternalCallIdJob.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmData.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateCrmFieldDataJob.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Listeners/Teams 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateSalesforceAccount.php, final class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder not checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Telephony 1 file, folder checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextMessagingService.php, class checked","depth":8,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 10 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component 2 files, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Twilio/Conference/ConferenceManager 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SoftPhoneManager.php, final class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Uploader/Services 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"CreateActivityFromUploadService.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Activity/Import 3 files, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HydrateCrmDataByExternalCallIdJob.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmData.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateCrmFieldDataJob.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Listeners/Teams 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateSalesforceAccount.php, final class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder not checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Telephony 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextMessagingService.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component 2 files, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Twilio/Conference/ConferenceManager 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SoftPhoneManager.php, final class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Uploader/Services 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"CreateActivityFromUploadService.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Twilio/Conference/ConferenceManager 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SoftPhoneManager.php, final class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SoftPhoneManager.php, final class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Uploader/Services 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"CreateActivityFromUploadService.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"CreateActivityFromUploadService.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Activity/Import 3 files, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HydrateCrmDataByExternalCallIdJob.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmData.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateCrmFieldDataJob.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HydrateCrmDataByExternalCallIdJob.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmData.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateCrmFieldDataJob.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Listeners/Teams 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateSalesforceAccount.php, final class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateSalesforceAccount.php, final class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Telephony 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextMessagingService.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Telephony 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextMessagingService.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextMessagingService.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"8 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20963 fix deleted object import","depth":2,"on_screen":true,"value":"JY-20963 fix deleted object import","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n // Setup services.\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);\n $crmService->setUser($roomOwner);\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n // Setup services.\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);\n $crmService->setUser($roomOwner);\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [\n 'exception' => $throwable,\n 'user_id' => $user->getId(),\n 'team_id' => $team->getId(),\n ]);\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $roomOwner->getTeam(),\n 'integrationAdmin' => $roomOwner,\n 'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n } catch (\\Throwable $throwable) {\n $this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [\n 'exception' => $throwable,\n 'user_id' => $user->getId(),\n 'team_id' => $team->getId(),\n ]);\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $roomOwner->getTeam(),\n 'integrationAdmin' => $roomOwner,\n 'providerSlug' => $roomOwner->getTeam()->getCrmConfiguration()->getProviderName(),\n ]);\n $crmService = $crmResolver->prepareCrmService();\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Shelve Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
8930607373630269366
|
8095934260644328089
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
13 files, folder partially checked
app 10 files, folder partially checked
Component 2 files, folder checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Jobs/Activity/Import 3 files, folder checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
Listeners/Teams 1 file, folder checked
UpdateSalesforceAccount.php, final class checked
Services 2 files, folder partially checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder not checked
.env.local not checked
app 10 files, folder partially checked
Component 2 files, folder checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Jobs/Activity/Import 3 files, folder checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
Listeners/Teams 1 file, folder checked
UpdateSalesforceAccount.php, final class checked
Services 2 files, folder partially checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
Component 2 files, folder checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
CreateActivityFromUploadService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
ActivityController.php, final class checked
Jobs/Activity/Import 3 files, folder checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
Listeners/Teams 1 file, folder checked
UpdateSalesforceAccount.php, final class checked
UpdateSalesforceAccount.php, final class checked
Services 2 files, folder partially checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
TextMessagingService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder not checked
.env.local not checked
8 modified
JY-20963 fix deleted object import
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
3 differences
1aee7aad
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
// Setup services.
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);
$crmService->setUser($roomOwner);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
$activity = $roomOwner->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE_INBOUND,
'provider' => Models\Activity::PROVIDER_TWILIO,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_RINGING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),
'scheduled_start_time' => $now,
]);
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $roomOwner->getRegionId(),
'is_muted' => false,
]);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $callerId,
'name' => $activity->prospect_name,
'enter_time' => $now,
]);
$caller->connections()->create([
'telephony_provider_id' => $callerCallSid,
'session_id' => $session->getId(),
'phone' => $callerId,
'direction' => 'inbound',
'type' => Connection::TYPE_PSTN,
'opened_at' => $now,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'user_id' => $roomOwner->id,
'email' => $roomOwner->email,
'name' => $roomOwner->name,
'phone_number' => $phoneNumber,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'direction' => 'outbound-api',
'phone' => $phoneNumber,
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$this->logger->debug(__METHOD__ . ': done', [
'callSid' => $data->getCallSid(),
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
$activity->refresh();
return $activity;
}
private function getVoiceRecordingReasonCode(Models\Team $team, string $phoneNumber): int
{
$this->logger->debug(__METHOD__, [
'team' => $team->id_string,
'phone' => $phoneNumber,
]);
if (
! $team->hasComplianceModeRecordingPrompt()
&& ! $team->hasComplianceModeRecordingRestrictToOneSide()
&& ! $team->hasComplianceModeRecordingRestrictRecording()
) {
return 0;
}
/** @var Models\VoiceConsentPrefix|null $voiceConsentPolicy */
$voiceConsentPolicy = Models\VoiceConsentPrefix::query()
->whereRaw("? LIKE CONCAT(destination_prefix, '%')", [$phoneNumber])
->where(static function (Eloquent\Builder $builder) use ($team): void {
$builder
->whereNull('team_id')
->orWhere('team_id', $team->getId());
})
->orderByDesc('team_id')
->first();
if (! $voiceConsentPolicy instanceof Models\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {
return 0;
}
if ($team->hasComplianceModeRecordingPrompt()) {
// This number requires consent from the other party before recording the call.
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;
}
if ($team->hasComplianceModeRecordingRestrictToOneSide()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;
}
if ($team->hasComplianceModeRecordingRestrictRecording()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;
}
return 0;
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOrganiser(Models\Activity $activity): void
{
$this->logger->debug(__METHOD__, [
'activity' => $activity->id_string,
]);
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$canBeDialed =
$activity->isTypeSoftPhone()
&& ! $activity->hasStarted()
&& $activity->isPending()
&& $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->exists();
if (! $canBeDialed) {
throw new OutOfBoundsException('Cannot make an outbound call to this activity.');
}
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->firstOrFail();
/** @var Models\Session $session */
$session = $organiserConnection->getSession();
$twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
]);
// What number will the organizer see as caller id
$callerId = $roomOwner->getSoftPhoneNumber();
if ($callerId === null) {
$callee = $activity->participants()->whereNull('user_id')->first();
// This is a bridge call. We make it look like the callee is calling the organizer
$callerId = $callee->phone_number;
}
// What number will the prospect see as caller id
$calleeId = $organiser->getPhoneNumber();
$this->logger->debug(__METHOD__ . ': twilio call', [
'calleeId' => $calleeId,
'callerId' => $callerId,
]);
$twilioParticipant = $twilio->createParticipantInstance(
$session->id_string,
$callerId,
$calleeId,
$twilioConferenceSettings
);
$organiserConnection
->setTelephonyProviderId($twilioParticipant->callSid)
->save();
$session
->setTelephonyProviderId($twilioParticipant->conferenceSid)
->save();
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'session' => $session->id_string,
]);
}
public function dialCoach(Models\Activity $activity, Models\User $user): void
{
throw new \LogicException('Not implemented yet');
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function terminate(VO\ConferenceManager\ActivityTerminate $data): void
{
if (! $data instanceof VO\ConferenceManager\SoftphoneTerminate) {
throw new UnexpectedValueException(sprintf(
'Expecting an instance of "%s", but "%s" given.',
VO\ConferenceManager\SoftphoneTerminate::class,
\get_class($data)
));
}
$this->logger->debug(__METHOD__, [
'activity' => $data->getActivity()->id_string,
'user' => $data->getUser()->id_string,
]);
$activity = $data->getActivity();
$roomOwner = $activity->getUser();
$isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();
if ($isCancelling) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
}
/** @var Models\Session $session */
$session = $activity->sessions->first();
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->whereIn('type', [
Connection::TYPE_WEBRTC,
Connection::TYPE_PSTN,
])
->firstOrFail();
if (! $organiserConnection->hasTelephonyProviderId()) {
throw new LogicException(
'Unable to terminate call as we do not yet know the organiser telephony provider id',
);
}
$twilioClient = $this->twilioClientBuilder->build($activity->getTeam());
$twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());
$session->hasTelephonyProviderId()
&& $twilioClient->terminateConference($session->getTelephonyProviderId());
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'session' => $session->id_string,
]);
}
public function answer(VO\ConferenceManager\ActivityAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundAnswer) {
return $this->answerInboundCall($data);
}
if ($data instanceof VO\ConferenceManager\SoftPhoneOutboundAnswer) {
return $this->answerOutboundCall($data);
}
throw new LogicException('Unable to answer - not supported activity answer instance');
}
private function answerInboundCall(VO\ConferenceManager\SoftPhoneInboundAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$dateTime = now();
$activity = $this->create(
new VO\ConferenceManager\SoftPhoneInboundCreate(
$data->getCallerId(),
$data->getCallSid(),
$data->getUser()
)
);
/** @var Models\Participant $activityGuest */
$activityGuest = $activity->from;
$activityGuest
->setEnterTime($dateTime)
->save();
/** @var Connection $connection */
$connection = $activityGuest->connections()
->where('type', Connection::TYPE_PSTN)
->firstOrFail();
$connection
->setTelephonyProviderId($data->getCallSid())
->setOpenedAt($dateTime)
->save();
event(new Events\Activities\Connections\Opened($connection));
return $this->joinTwilioConference($connection, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]);
}
private function answerOutboundCall(VO\ConferenceManager\SoftPhoneOutboundAnswer $data): TwiML\VoiceResponse
{
$connection = $data->getConnection();
$participant = $connection->getParticipant();
$activity = $data->getActivity();
$organiser = $activity->organizer;
$dateTime = now();
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
'connection' => $connection->id_string,
'participant' => $participant->id_string,
'activity' => $activity->id_string,
]);
if (! $participant->hasEnterTime()) {
$participant->update([
'enter_time' => $dateTime,
]);
}
$connection->update([
'is_muted' => $participant->isCoach(),
'opened_at' => $dateTime,
]);
event(new Events\Activities\Connections\Opened($connection));
if ($participant->isCoach()) {
/** @var Connection $roomOwnerConnection */
$roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();
return $this->joinTwilioConference(
$connection,
[
'coach' => $roomOwnerConnection->getTelephonyProviderId(),
]
);
}
return $this->joinTwilioConference(
$connection,
[
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]
);
}
private function determineUserCallerId(Models\User $user): string
{
if ($user->hasCallerId()) {
return $user->getCallerId();
}
// TODO: We should require a caller id to be verified before using the dialer
return $user->getSoftPhoneNumber() ?? $user->phone;
}
private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool
{
$startsWith = '[HTTP 404] Unable to create record: The requested resource';
$endsWith = 'was not found';
return Str::startsWith($exceptionMessage, $startsWith)
&& Str::endsWith($exceptionMessage, $endsWith);
}
/**
* @throws RestException
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOtherParty(Connection $callerConnection): self
{
$session = $callerConnection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$this->logger->debug(__METHOD__, [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {
throw new UnexpectedValueException('Unexpected activity type');
}
$callee = $activity->to;
$calleeId = $callee->getPhoneNumber();
/** @var Connection $calleeConnection */
$calleeConnection = $callee->connections()
->whereIn('type', [
Connection::TYPE_PSTN,
Connection::TYPE_WEBRTC,
])
->firstOrFail();
$caller = $activity->from;
// we need to take the number from the connection
// as the one we store on participant level might be the one we call to the rep via PSTN mode
$callerId = $callerConnection->getPhoneNumber();
if (! $caller->is($callerConnection->getParticipant())) {
throw new LogicException(sprintf(
'Unexpected caller participant. Expected: "%s", Actual: "%s"',
$caller->id_string,
$callerConnection->getParticipant()->id_string
));
}
$twilioConferenceSettings = [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
];
$hasInboundRecording = $activity->isTypeSoftphoneInbound()
&& $roomOwner->checkSoftphoneInboundRecordPreference();
$hasOutboundRecording = $activity->isTypeSoftPhone()
&& $roomOwner->checkSoftphoneOutboundRecordPreference();
$isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;
$isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()
|| $activity->hasRecordingReasonComplianceRestricted();
if ($isRecordingEnabled && ! $isRecordingRestricted) {
$twilioConferenceSettings = array_merge($twilioConferenceSettings, [
'record' => true,
'recordingChannels' => 'dual',
'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),
'recordingStatusCallbackMethod' => 'POST',
'recordingStatusCallbackEvent' => 'in-progress completed absent',
]);
if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {
$twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()
? 'outbound'
: 'inbound';
}
}
try {
if ($activity->hasRecordingReasonCompliancePrompted()) {
$this->logger->info('[Softphone] Request compliance prompt via outbound call', [
'activity_uuid' => $activity->getUuid(),
'session' => $session->id_string,
'conference_sid' => $session->getTelephonyProviderId(),
]);
$callInstance = $twilio->createCall(
$callerId,
$calleeId,
$this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [
'session' => $session->id_string,
'connection' => $calleeConnection->id_string,
]),
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $callInstance->sid,
'parent_connection_id' => $callerConnection->getId(),
]);
} else {
$twilioParticipantConnection = $twilio->createParticipantInstance(
$session->getTelephonyProviderId(),
$callerId,
$calleeId,
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $twilioParticipantConnection->callSid,
]);
}
} catch (RestException $restException) {
if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {
throw $restException;
}
if (! $activity->isInFiniteState()) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
$message = __METHOD__ . ': activity.status -> cancelled';
} else {
$message = sprintf(
__METHOD__ . ': not setting to cancelled, activity.status is %s',
$activity->getStatus()
);
}
$this->logger->info($message, [
'activity' => $activity->id_string,
'exception' => $restException,
]);
}
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
return $this;
}
private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array
{
$session = $connection->getSession();
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => false,
'record' => false,
'waitUrl' => '',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]),
'statusCallbackEvent' => [
'initiated',
'ringing',
'answered',
'completed',
],
'statusCallbackMethod' => 'POST',
'conferenceStatusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
]),
'conferenceStatusCallbackMethod' => 'POST',
'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function applyDefaultTwilioConferenceSettings(Models\Session $session, array $override = []): array
{
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => false,
'endConferenceOnExit' => false,
'waitUrl' => '',
'statusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
// 'speaker',
]),
'statusCallbackMethod' => 'POST',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function joinTwilioConference(Connection $connection, array $options = []): TwiML\VoiceResponse
{
$session = $connection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$this->logger->debug(__METHOD__, [
'connection' => $connection->id_string,
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
]);
$voiceResponse = new TwiML\VoiceResponse();
$voiceResponse->dial()
->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]))
->conference(
$session->getIdString(),
$this->applyDefaultTwilioConferenceSettings($session, $options)
);
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'result' => $voiceResponse,
]);
return $voiceResponse;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
try {
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
$crmService = $crmResolver->prepareCrmService();
} catch (\Throwable $throwable) {
$this->logger->error('[SoftPhoneManager] Failed to resolve CRM service with fallback', [
'exception' => $throwable,
'user_id' => $user->getId(),
'team_id' => $team->getId(),
]);
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' ...
|
71725
|
NULL
|
NULL
|
NULL
|
|
71733
|
2573
|
49
|
2026-05-26T07:06:03.489653+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779163489_m2.jpg...
|
PhpStorm
|
Shelve Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
13 files, folder partially checked
app 10 files, folder partially checked
Component 2 files, folder checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Jobs/Activity/Import 3 files, folder checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
Listeners/Teams 1 file, folder checked
UpdateSalesforceAccount.php, final class checked
Services 2 files, folder partially checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder not checked
.env.local not checked
app 10 files, folder partially checked
Component 2 files, folder checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Jobs/Activity/Import 3 files, folder checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
Listeners/Teams 1 file, folder checked
UpdateSalesforceAccount.php, final class checked
Services 2 files, folder partially checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
Component 2 files, folder checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
CreateActivityFromUploadService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
ActivityController.php, final class checked
Jobs/Activity/Import 3 files, folder checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
Listeners/Teams 1 file, folder checked
UpdateSalesforceAccount.php, final class checked
UpdateSalesforceAccount.php, final class checked
Services 2 files, folder partially checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
TextMessagingService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder not checked
.env.local not checked
8 modified
JY-20963 fix deleted object import
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
3 differences
1aee7aad
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
// Setup services.
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);
$crmService->setUser($roomOwner);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
$activity = $roomOwner->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE_INBOUND,
'provider' => Models\Activity::PROVIDER_TWILIO,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_RINGING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),
'scheduled_start_time' => $now,
]);
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $roomOwner->getRegionId(),
'is_muted' => false,
]);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $callerId,
'name' => $activity->prospect_name,
'enter_time' => $now,
]);
$caller->connections()->create([
'telephony_provider_id' => $callerCallSid,
'session_id' => $session->getId(),
'phone' => $callerId,
'direction' => 'inbound',
'type' => Connection::TYPE_PSTN,
'opened_at' => $now,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'user_id' => $roomOwner->id,
'email' => $roomOwner->email,
'name' => $roomOwner->name,
'phone_number' => $phoneNumber,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'direction' => 'outbound-api',
'phone' => $phoneNumber,
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$this->logger->debug(__METHOD__ . ': done', [
'callSid' => $data->getCallSid(),
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
$activity->refresh();
return $activity;
}
private function getVoiceRecordingReasonCode(Models\Team $team, string $phoneNumber): int
{
$this->logger->debug(__METHOD__, [
'team' => $team->id_string,
'phone' => $phoneNumber,
]);
if (
! $team->hasComplianceModeRecordingPrompt()
&& ! $team->hasComplianceModeRecordingRestrictToOneSide()
&& ! $team->hasComplianceModeRecordingRestrictRecording()
) {
return 0;
}
/** @var Models\VoiceConsentPrefix|null $voiceConsentPolicy */
$voiceConsentPolicy = Models\VoiceConsentPrefix::query()
->whereRaw("? LIKE CONCAT(destination_prefix, '%')", [$phoneNumber])
->where(static function (Eloquent\Builder $builder) use ($team): void {
$builder
->whereNull('team_id')
->orWhere('team_id', $team->getId());
})
->orderByDesc('team_id')
->first();
if (! $voiceConsentPolicy instanceof Models\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {
return 0;
}
if ($team->hasComplianceModeRecordingPrompt()) {
// This number requires consent from the other party before recording the call.
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;
}
if ($team->hasComplianceModeRecordingRestrictToOneSide()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;
}
if ($team->hasComplianceModeRecordingRestrictRecording()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;
}
return 0;
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOrganiser(Models\Activity $activity): void
{
$this->logger->debug(__METHOD__, [
'activity' => $activity->id_string,
]);
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$canBeDialed =
$activity->isTypeSoftPhone()
&& ! $activity->hasStarted()
&& $activity->isPending()
&& $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->exists();
if (! $canBeDialed) {
throw new OutOfBoundsException('Cannot make an outbound call to this activity.');
}
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->firstOrFail();
/** @var Models\Session $session */
$session = $organiserConnection->getSession();
$twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
]);
// What number will the organizer see as caller id
$callerId = $roomOwner->getSoftPhoneNumber();
if ($callerId === null) {
$callee = $activity->participants()->whereNull('user_id')->first();
// This is a bridge call. We make it look like the callee is calling the organizer
$callerId = $callee->phone_number;
}
// What number will the prospect see as caller id
$calleeId = $organiser->getPhoneNumber();
$this->logger->debug(__METHOD__ . ': twilio call', [
'calleeId' => $calleeId,
'callerId' => $callerId,
]);
$twilioParticipant = $twilio->createParticipantInstance(
$session->id_string,
$callerId,
$calleeId,
$twilioConferenceSettings
);
$organiserConnection
->setTelephonyProviderId($twilioParticipant->callSid)
->save();
$session
->setTelephonyProviderId($twilioParticipant->conferenceSid)
->save();
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'session' => $session->id_string,
]);
}
public function dialCoach(Models\Activity $activity, Models\User $user): void
{
throw new \LogicException('Not implemented yet');
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function terminate(VO\ConferenceManager\ActivityTerminate $data): void
{
if (! $data instanceof VO\ConferenceManager\SoftphoneTerminate) {
throw new UnexpectedValueException(sprintf(
'Expecting an instance of "%s", but "%s" given.',
VO\ConferenceManager\SoftphoneTerminate::class,
\get_class($data)
));
}
$this->logger->debug(__METHOD__, [
'activity' => $data->getActivity()->id_string,
'user' => $data->getUser()->id_string,
]);
$activity = $data->getActivity();
$roomOwner = $activity->getUser();
$isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();
if ($isCancelling) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
}
/** @var Models\Session $session */
$session = $activity->sessions->first();
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->whereIn('type', [
Connection::TYPE_WEBRTC,
Connection::TYPE_PSTN,
])
->firstOrFail();
if (! $organiserConnection->hasTelephonyProviderId()) {
throw new LogicException(
'Unable to terminate call as we do not yet know the organiser telephony provider id',
);
}
$twilioClient = $this->twilioClientBuilder->build($activity->getTeam());
$twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());
$session->hasTelephonyProviderId()
&& $twilioClient->terminateConference($session->getTelephonyProviderId());
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'session' => $session->id_string,
]);
}
public function answer(VO\ConferenceManager\ActivityAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundAnswer) {
return $this->answerInboundCall($data);
}
if ($data instanceof VO\ConferenceManager\SoftPhoneOutboundAnswer) {
return $this->answerOutboundCall($data);
}
throw new LogicException('Unable to answer - not supported activity answer instance');
}
private function answerInboundCall(VO\ConferenceManager\SoftPhoneInboundAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$dateTime = now();
$activity = $this->create(
new VO\ConferenceManager\SoftPhoneInboundCreate(
$data->getCallerId(),
$data->getCallSid(),
$data->getUser()
)
);
/** @var Models\Participant $activityGuest */
$activityGuest = $activity->from;
$activityGuest
->setEnterTime($dateTime)
->save();
/** @var Connection $connection */
$connection = $activityGuest->connections()
->where('type', Connection::TYPE_PSTN)
->firstOrFail();
$connection
->setTelephonyProviderId($data->getCallSid())
->setOpenedAt($dateTime)
->save();
event(new Events\Activities\Connections\Opened($connection));
return $this->joinTwilioConference($connection, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]);
}
private function answerOutboundCall(VO\ConferenceManager\SoftPhoneOutboundAnswer $data): TwiML\VoiceResponse
{
$connection = $data->getConnection();
$participant = $connection->getParticipant();
$activity = $data->getActivity();
$organiser = $activity->organizer;
$dateTime = now();
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
'connection' => $connection->id_string,
'participant' => $participant->id_string,
'activity' => $activity->id_string,
]);
if (! $participant->hasEnterTime()) {
$participant->update([
'enter_time' => $dateTime,
]);
}
$connection->update([
'is_muted' => $participant->isCoach(),
'opened_at' => $dateTime,
]);
event(new Events\Activities\Connections\Opened($connection));
if ($participant->isCoach()) {
/** @var Connection $roomOwnerConnection */
$roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();
return $this->joinTwilioConference(
$connection,
[
'coach' => $roomOwnerConnection->getTelephonyProviderId(),
]
);
}
return $this->joinTwilioConference(
$connection,
[
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]
);
}
private function determineUserCallerId(Models\User $user): string
{
if ($user->hasCallerId()) {
return $user->getCallerId();
}
// TODO: We should require a caller id to be verified before using the dialer
return $user->getSoftPhoneNumber() ?? $user->phone;
}
private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool
{
$startsWith = '[HTTP 404] Unable to create record: The requested resource';
$endsWith = 'was not found';
return Str::startsWith($exceptionMessage, $startsWith)
&& Str::endsWith($exceptionMessage, $endsWith);
}
/**
* @throws RestException
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOtherParty(Connection $callerConnection): self
{
$session = $callerConnection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$this->logger->debug(__METHOD__, [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {
throw new UnexpectedValueException('Unexpected activity type');
}
$callee = $activity->to;
$calleeId = $callee->getPhoneNumber();
/** @var Connection $calleeConnection */
$calleeConnection = $callee->connections()
->whereIn('type', [
Connection::TYPE_PSTN,
Connection::TYPE_WEBRTC,
])
->firstOrFail();
$caller = $activity->from;
// we need to take the number from the connection
// as the one we store on participant level might be the one we call to the rep via PSTN mode
$callerId = $callerConnection->getPhoneNumber();
if (! $caller->is($callerConnection->getParticipant())) {
throw new LogicException(sprintf(
'Unexpected caller participant. Expected: "%s", Actual: "%s"',
$caller->id_string,
$callerConnection->getParticipant()->id_string
));
}
$twilioConferenceSettings = [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
];
$hasInboundRecording = $activity->isTypeSoftphoneInbound()
&& $roomOwner->checkSoftphoneInboundRecordPreference();
$hasOutboundRecording = $activity->isTypeSoftPhone()
&& $roomOwner->checkSoftphoneOutboundRecordPreference();
$isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;
$isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()
|| $activity->hasRecordingReasonComplianceRestricted();
if ($isRecordingEnabled && ! $isRecordingRestricted) {
$twilioConferenceSettings = array_merge($twilioConferenceSettings, [
'record' => true,
'recordingChannels' => 'dual',
'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),
'recordingStatusCallbackMethod' => 'POST',
'recordingStatusCallbackEvent' => 'in-progress completed absent',
]);
if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {
$twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()
? 'outbound'
: 'inbound';
}
}
try {
if ($activity->hasRecordingReasonCompliancePrompted()) {
$this->logger->info('[Softphone] Request compliance prompt via outbound call', [
'activity_uuid' => $activity->getUuid(),
'session' => $session->id_string,
'conference_sid' => $session->getTelephonyProviderId(),
]);
$callInstance = $twilio->createCall(
$callerId,
$calleeId,
$this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [
'session' => $session->id_string,
'connection' => $calleeConnection->id_string,
]),
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $callInstance->sid,
'parent_connection_id' => $callerConnection->getId(),
]);
} else {
$twilioParticipantConnection = $twilio->createParticipantInstance(
$session->getTelephonyProviderId(),
$callerId,
$calleeId,
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $twilioParticipantConnection->callSid,
]);
}
} catch (RestException $restException) {
if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {
throw $restException;
}
if (! $activity->isInFiniteState()) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
$message = __METHOD__ . ': activity.status -> cancelled';
} else {
$message = sprintf(
__METHOD__ . ': not setting to cancelled, activity.status is %s',
$activity->getStatus()
);
}
$this->logger->info($message, [
'activity' => $activity->id_string,
'exception' => $restException,
]);
}
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
return $this;
}
private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array
{
$session = $connection->getSession();
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => false,
'record' => false,
'waitUrl' => '',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]),
'statusCallbackEvent' => [
'initiated',
'ringing',
'answered',
'completed',
],
'statusCallbackMethod' => 'POST',
'conferenceStatusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
]),
'conferenceStatusCallbackMethod' => 'POST',
'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function applyDefaultTwilioConferenceSettings(Models\Session $session, array $override = []): array
{
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => false,
'endConferenceOnExit' => false,
'waitUrl' => '',
'statusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
// 'speaker',
]),
'statusCallbackMethod' => 'POST',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function joinTwilioConference(Connection $connection, array $options = []): TwiML\VoiceResponse
{
$session = $connection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$this->logger->debug(__METHOD__, [
'connection' => $connection->id_string,
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
]);
$voiceResponse = new TwiML\VoiceResponse();
$voiceResponse->dial()
->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]))
->conference(
$session->getIdString(),
$this->applyDefaultTwilioConferenceSettings($session, $options)
);
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'result' => $voiceResponse,
]);
return $voiceResponse;
}
}...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.51329786,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.5375665,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.49268618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.5013298,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.13328013,"width":0.032579787,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 10 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component 2 files, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.05418883,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Twilio/Conference/ConferenceManager 1 file, folder checked","depth":7,"bounds":{"left":0.2835771,"top":0.1859537,"width":0.10837766,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SoftPhoneManager.php, final class checked","depth":8,"bounds":{"left":0.28989363,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Uploader/Services 1 file, folder checked","depth":7,"bounds":{"left":0.2835771,"top":0.22106944,"width":0.065159574,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"CreateActivityFromUploadService.php, class checked","depth":8,"bounds":{"left":0.28989363,"top":0.2386273,"width":0.0944149,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.273743,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":7,"bounds":{"left":0.2835771,"top":0.29130086,"width":0.06050532,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Activity/Import 3 files, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.30885875,"width":0.072140954,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HydrateCrmDataByExternalCallIdJob.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.3264166,"width":0.100398935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmData.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.055851065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateCrmFieldDataJob.php, class checked","depth":7,"bounds":{"left":0.2835771,"top":0.36153233,"width":0.07579787,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Listeners/Teams 1 file, folder checked","depth":6,"bounds":{"left":0.27726063,"top":0.3790902,"width":0.061502658,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateSalesforceAccount.php, final class checked","depth":7,"bounds":{"left":0.2835771,"top":0.39664805,"width":0.0787899,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":6,"bounds":{"left":0.27726063,"top":0.4142059,"width":0.047872342,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder not checked","depth":7,"bounds":{"left":0.2835771,"top":0.43176377,"width":0.035904255,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Telephony 1 file, folder checked","depth":7,"bounds":{"left":0.2835771,"top":0.44932163,"width":0.04886968,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextMessagingService.php, class checked","depth":8,"bounds":{"left":0.28989363,"top":0.4668795,"width":0.07180851,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.48443735,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.5019952,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.51955307,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 10 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.15083799,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component 2 files, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.05418883,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Twilio/Conference/ConferenceManager 1 file, folder checked","depth":6,"bounds":{"left":0.2835771,"top":0.1859537,"width":0.10837766,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SoftPhoneManager.php, final class checked","depth":7,"bounds":{"left":0.28989363,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Uploader/Services 1 file, folder checked","depth":6,"bounds":{"left":0.2835771,"top":0.22106944,"width":0.065159574,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"CreateActivityFromUploadService.php, class checked","depth":7,"bounds":{"left":0.28989363,"top":0.2386273,"width":0.0944149,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.273743,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":6,"bounds":{"left":0.2835771,"top":0.29130086,"width":0.06050532,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Activity/Import 3 files, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.30885875,"width":0.072140954,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HydrateCrmDataByExternalCallIdJob.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.3264166,"width":0.100398935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmData.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.055851065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateCrmFieldDataJob.php, class checked","depth":6,"bounds":{"left":0.2835771,"top":0.36153233,"width":0.07579787,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Listeners/Teams 1 file, folder checked","depth":5,"bounds":{"left":0.27726063,"top":0.3790902,"width":0.061502658,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateSalesforceAccount.php, final class checked","depth":6,"bounds":{"left":0.2835771,"top":0.39664805,"width":0.0787899,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":5,"bounds":{"left":0.27726063,"top":0.4142059,"width":0.047872342,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder not checked","depth":6,"bounds":{"left":0.2835771,"top":0.43176377,"width":0.035904255,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Telephony 1 file, folder checked","depth":6,"bounds":{"left":0.2835771,"top":0.44932163,"width":0.04886968,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextMessagingService.php, class checked","depth":7,"bounds":{"left":0.28989363,"top":0.4668795,"width":0.07180851,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Component 2 files, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.05418883,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Twilio/Conference/ConferenceManager 1 file, folder checked","depth":5,"bounds":{"left":0.2835771,"top":0.1859537,"width":0.10837766,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SoftPhoneManager.php, final class checked","depth":6,"bounds":{"left":0.28989363,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Uploader/Services 1 file, folder checked","depth":5,"bounds":{"left":0.2835771,"top":0.22106944,"width":0.065159574,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"CreateActivityFromUploadService.php, class checked","depth":6,"bounds":{"left":0.28989363,"top":0.2386273,"width":0.0944149,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Twilio/Conference/ConferenceManager 1 file, folder checked","depth":4,"bounds":{"left":0.2835771,"top":0.1859537,"width":0.10837766,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SoftPhoneManager.php, final class checked","depth":5,"bounds":{"left":0.28989363,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SoftPhoneManager.php, final class checked","depth":4,"bounds":{"left":0.28989363,"top":0.20351157,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Uploader/Services 1 file, folder checked","depth":4,"bounds":{"left":0.2835771,"top":0.22106944,"width":0.065159574,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"CreateActivityFromUploadService.php, class checked","depth":5,"bounds":{"left":0.28989363,"top":0.2386273,"width":0.0944149,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"CreateActivityFromUploadService.php, class checked","depth":4,"bounds":{"left":0.28989363,"top":0.2386273,"width":0.0944149,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":4,"bounds":{"left":0.27726063,"top":0.25618514,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Http/Controllers/API 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.273743,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":5,"bounds":{"left":0.2835771,"top":0.29130086,"width":0.06050532,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ActivityController.php, final class checked","depth":4,"bounds":{"left":0.2835771,"top":0.29130086,"width":0.06050532,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jobs/Activity/Import 3 files, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.30885875,"width":0.072140954,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HydrateCrmDataByExternalCallIdJob.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.3264166,"width":0.100398935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmData.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.055851065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateCrmFieldDataJob.php, class checked","depth":5,"bounds":{"left":0.2835771,"top":0.36153233,"width":0.07579787,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"HydrateCrmDataByExternalCallIdJob.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.3264166,"width":0.100398935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmData.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.34397447,"width":0.055851065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateCrmFieldDataJob.php, class checked","depth":4,"bounds":{"left":0.2835771,"top":0.36153233,"width":0.07579787,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Listeners/Teams 1 file, folder checked","depth":4,"bounds":{"left":0.27726063,"top":0.3790902,"width":0.061502658,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateSalesforceAccount.php, final class checked","depth":5,"bounds":{"left":0.2835771,"top":0.39664805,"width":0.0787899,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"UpdateSalesforceAccount.php, final class checked","depth":4,"bounds":{"left":0.2835771,"top":0.39664805,"width":0.0787899,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":4,"bounds":{"left":0.27726063,"top":0.4142059,"width":0.047872342,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder not checked","depth":5,"bounds":{"left":0.2835771,"top":0.43176377,"width":0.035904255,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Telephony 1 file, folder checked","depth":5,"bounds":{"left":0.2835771,"top":0.44932163,"width":0.04886968,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextMessagingService.php, class checked","depth":6,"bounds":{"left":0.28989363,"top":0.4668795,"width":0.07180851,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder not checked","depth":4,"bounds":{"left":0.2835771,"top":0.43176377,"width":0.035904255,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Telephony 1 file, folder checked","depth":4,"bounds":{"left":0.2835771,"top":0.44932163,"width":0.04886968,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextMessagingService.php, class checked","depth":5,"bounds":{"left":0.28989363,"top":0.4668795,"width":0.07180851,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextMessagingService.php, class checked","depth":4,"bounds":{"left":0.28989363,"top":0.4668795,"width":0.07180851,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":4,"bounds":{"left":0.27094415,"top":0.48443735,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder not checked","depth":4,"bounds":{"left":0.27094415,"top":0.5019952,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"bounds":{"left":0.27094415,"top":0.51955307,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"8 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.32446808,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20963 fix deleted object import","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.3238032,"height":0.087789305},"on_screen":true,"value":"JY-20963 fix deleted object import","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.5711436,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3 differences","depth":1,"bounds":{"left":0.5511968,"top":0.41181165,"width":0.027260639,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"1aee7aad","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.022606382,"height":0.013567438},"on_screen":true,"value":"1aee7aad","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n // Setup services.\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);\n $crmService->setUser($roomOwner);\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.26230052,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\n\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Component\\Twilio\\Conference\\ConferenceManager;\nuse Jiminny\\Component\\Twilio\\Exception\\OutOfBoundsException;\nuse Jiminny\\Component\\Twilio\\Exception\\UnexpectedValueException;\nuse Jiminny\\Component\\Twilio\\Resolver;\nuse Jiminny\\Component\\Twilio\\VO;\nuse Jiminny\\Component\\UrlGenerator\\Webhook;\nuse Jiminny\\Events;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Services\\ActivityService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\ProviderRegistry;\nuse Jiminny\\Services\\Telephony\\TwilioClientBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse Twilio\\Exceptions\\ConfigurationException;\nuse Twilio\\Exceptions\\RestException;\nuse Twilio\\Exceptions\\TwilioException;\nuse Twilio\\TwiML;\n\nfinal readonly class SoftPhoneManager implements ConferenceManager\n{\n public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;\n public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';\n\n public function __construct(\n private ProviderRegistry $crmProviderRegistry,\n private Webhook $urlGenerator,\n private ActivityService $activityService,\n private LoggerInterface $logger,\n private TwilioClientBuilder $twilioClientBuilder,\n ) {\n }\n\n public function create(VO\\ConferenceManager\\ActivityCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n 'instanceof' => get_class($data),\n ]);\n\n $activity = Observer::skipEvents(function () use ($data) {\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundCreate) {\n $activity = $this->createInboundActivity($data);\n } elseif ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundCreate) {\n $activity = $this->createOutboundActivity($data);\n } else {\n throw new LogicException(sprintf(\n 'Unknown softphone implementation: %s',\n get_class($data)\n ));\n }\n\n return $activity;\n });\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'instanceof' => get_class($data),\n 'activity' => $activity->id_string,\n ]);\n\n return $activity;\n }\n\n private function createOutboundActivity(VO\\ConferenceManager\\SoftPhoneOutboundCreate $data): Models\\Activity\n {\n $startTime = microtime(true);\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n $now = now();\n $user = $data->getUser();\n $team = $user->getTeam();\n $device = $data->getDevice();\n $phoneNumber = $data->getPhoneNumber();\n $route = $data->getPhoneRoute();\n\n if (! $this->activityService->teamCanDial($team, $phoneNumber)) {\n $region = getRegionByNumber($phoneNumber, $user->getCountryCode());\n\n throw new LogicException(sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? 'this number'\n ));\n }\n\n $lead = null;\n $account = null;\n $contact = null;\n $stage = null;\n $opportunity = null;\n\n // Setup services.\n $crmService = $this->crmProviderRegistry->get($team->crm->provider);\n\n if (! $user->isCrmRequired()) {\n $crmService->setUser($user->team->owner);\n } else {\n $crmService->setUser($user);\n }\n\n $prospectId = $data->getProspectId();\n\n try {\n if ($prospectId !== null) {\n $crmStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [\n 'prospect_id' => $prospectId,\n 'object_type' => $data->getObjectType(),\n ]);\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(\n $prospectId,\n $data->getObjectType()\n );\n $this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),\n 'found_lead' => $lead !== null,\n 'found_account' => $account !== null,\n 'found_opportunity' => $opportunity !== null,\n 'found_contact' => $contact !== null,\n ]);\n\n // Try to attach a contact if we have an opportunity.\n if ($opportunity !== null && $contact === null) {\n $matchStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');\n [, , , $contact, ] = $crmService->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n $this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),\n 'found_contact' => $contact !== null,\n ]);\n }\n } else {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n [$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(\n $data->getPhoneNumber(),\n $data->getRawPhoneNumber(),\n $user->getId()\n );\n }\n } catch (\\Throwable $throwable) {\n $this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.\n }\n\n if ($data->hasProspectStage()) {\n /** @var Models\\Stage $stage */\n $stage = $team->crm->stages()->uuid($data->getProspectStage());\n }\n\n $dbStart = microtime(true);\n $this->logger->info('[SoftPhoneManager] Starting activity creation in DB');\n $activity = $user->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'device_id' => $device->id ?? null,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'value' => $opportunity ? $opportunity->value : null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_PENDING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),\n 'scheduled_start_time' => $now,\n ]);\n $this->logger->info('[SoftPhoneManager] Activity created in DB', [\n 'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),\n 'activity_id' => $activity->id ?? null,\n ]);\n\n if (! $activity instanceof Models\\Activity) {\n throw new LogicException('Activity model expected');\n }\n\n $regionId = null;\n if ($user->hasRegionId()) {\n $regionId = $user->getRegionId();\n } elseif ($data->hasRegion()) {\n $regionId = $data->getRegion()->getId();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $regionId,\n 'is_muted' => false,\n ]);\n\n $phoneRouteResolver = new Resolver\\PhoneRouteResolver($user, $route);\n $phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();\n $connectionType = $phoneRouteResolver->getConnectionType();\n $callerId = $this->determineUserCallerId($user);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'user_id' => $user->id,\n 'email' => $user->email,\n 'name' => $user->name,\n 'phone_number' => $phoneRouteResolver->mustUsePSTN()\n ? $phoneNumberOrigin\n : $callerId,\n ]);\n\n $caller->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',\n 'phone' => $callerId,\n 'type' => $connectionType,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $phoneNumber,\n 'name' => $activity->prospect_name,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'phone' => $phoneNumber,\n 'direction' => 'outbound-api',\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $activity->refresh();\n\n $this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [\n 'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),\n 'activity_id' => $activity->id_string,\n ]);\n $this->logger->debug(__METHOD__ . ': done', [\n 'data' => $data,\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n return $activity;\n }\n\n private function createInboundActivity(VO\\ConferenceManager\\SoftPhoneInboundCreate $data): Models\\Activity\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $roomOwner = $data->getUser();\n $team = $roomOwner->getTeam();\n $phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();\n $callerId = $data->getCallerId();\n $callerCallSid = $data->getCallSid();\n\n $now = now();\n\n if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {\n $region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());\n\n throw new LogicException(\n sprintf(\n 'Sorry, dialing %s is not available with your subscription.'\n . ' Please contact your manager to enable additional destinations.',\n $region ?? ' this number',\n ),\n );\n }\n\n $lead = $account = $opportunity = $contact = $stage = null;\n\n try {\n $crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);\n $crmService->setUser($roomOwner);\n\n [$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(\n $callerId,\n null,\n $roomOwner->getId()\n );\n } catch (\\Throwable $throwable) {\n $this->logger->error(__METHOD__ . ': crm exception', [\n 'exception' => $throwable,\n ]);\n // maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening\n }\n\n /** @var Models\\Activity $activity */\n $activity = $roomOwner->activities()->create([\n 'type' => Models\\Activity::TYPE_SOFTPHONE_INBOUND,\n 'provider' => Models\\Activity::PROVIDER_TWILIO,\n 'crm_configuration_id' => $team->crm_id,\n 'lead_id' => $lead->id ?? null,\n 'account_id' => $account->id ?? null,\n 'contact_id' => $contact->id ?? null,\n 'opportunity_id' => $opportunity->id ?? null,\n 'stage_id' => $stage->id ?? null,\n 'status' => Models\\Activity::STATUS_RINGING,\n 'recording_state' => Models\\Activity::RECORDING_OFF,\n 'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),\n 'scheduled_start_time' => $now,\n ]);\n\n /** @var Models\\Session $session */\n $session = $activity->sessions()->create([\n 'type' => Models\\Session::TYPE_DIALER,\n 'region_id' => $roomOwner->getRegionId(),\n 'is_muted' => false,\n ]);\n\n /** @var Models\\Participant $caller */\n $caller = $activity->participants()->create([\n 'lead_id' => $activity->lead_id ?? null,\n 'contact_id' => $activity->contact_id ?? null,\n 'phone_number' => $callerId,\n 'name' => $activity->prospect_name,\n 'enter_time' => $now,\n ]);\n\n $caller->connections()->create([\n 'telephony_provider_id' => $callerCallSid,\n 'session_id' => $session->getId(),\n 'phone' => $callerId,\n 'direction' => 'inbound',\n 'type' => Connection::TYPE_PSTN,\n 'opened_at' => $now,\n ]);\n\n /** @var Models\\Participant $callee */\n $callee = $activity->participants()->create([\n 'user_id' => $roomOwner->id,\n 'email' => $roomOwner->email,\n 'name' => $roomOwner->name,\n 'phone_number' => $phoneNumber,\n ]);\n\n $callee->connections()->create([\n 'session_id' => $session->getId(),\n 'direction' => 'outbound-api',\n 'phone' => $phoneNumber,\n 'type' => Connection::TYPE_PSTN,\n ]);\n\n $activity->update([\n 'from_participant_id' => $caller->id,\n 'to_participant_id' => $callee->id,\n ]);\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'callSid' => $data->getCallSid(),\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n 'caller' => $caller->id_string,\n 'callee' => $callee->id_string,\n ]);\n\n $activity->refresh();\n\n return $activity;\n }\n\n private function getVoiceRecordingReasonCode(Models\\Team $team, string $phoneNumber): int\n {\n $this->logger->debug(__METHOD__, [\n 'team' => $team->id_string,\n 'phone' => $phoneNumber,\n ]);\n\n if (\n ! $team->hasComplianceModeRecordingPrompt()\n && ! $team->hasComplianceModeRecordingRestrictToOneSide()\n && ! $team->hasComplianceModeRecordingRestrictRecording()\n ) {\n return 0;\n }\n\n /** @var Models\\VoiceConsentPrefix|null $voiceConsentPolicy */\n $voiceConsentPolicy = Models\\VoiceConsentPrefix::query()\n ->whereRaw(\"? LIKE CONCAT(destination_prefix, '%')\", [$phoneNumber])\n ->where(static function (Eloquent\\Builder $builder) use ($team): void {\n $builder\n ->whereNull('team_id')\n ->orWhere('team_id', $team->getId());\n })\n ->orderByDesc('team_id')\n ->first();\n\n if (! $voiceConsentPolicy instanceof Models\\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {\n return 0;\n }\n\n if ($team->hasComplianceModeRecordingPrompt()) {\n // This number requires consent from the other party before recording the call.\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;\n }\n\n if ($team->hasComplianceModeRecordingRestrictToOneSide()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;\n }\n\n if ($team->hasComplianceModeRecordingRestrictRecording()) {\n return Models\\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;\n }\n\n return 0;\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOrganiser(Models\\Activity $activity): void\n {\n $this->logger->debug(__METHOD__, [\n 'activity' => $activity->id_string,\n ]);\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $canBeDialed =\n $activity->isTypeSoftPhone()\n && ! $activity->hasStarted()\n && $activity->isPending()\n && $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->exists();\n\n if (! $canBeDialed) {\n throw new OutOfBoundsException('Cannot make an outbound call to this activity.');\n }\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->where('type', '=', Connection::TYPE_PSTN)\n ->whereNull('opened_at')\n ->firstOrFail();\n\n /** @var Models\\Session $session */\n $session = $organiserConnection->getSession();\n\n $twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ]);\n\n // What number will the organizer see as caller id\n $callerId = $roomOwner->getSoftPhoneNumber();\n\n if ($callerId === null) {\n $callee = $activity->participants()->whereNull('user_id')->first();\n // This is a bridge call. We make it look like the callee is calling the organizer\n $callerId = $callee->phone_number;\n }\n\n // What number will the prospect see as caller id\n $calleeId = $organiser->getPhoneNumber();\n\n $this->logger->debug(__METHOD__ . ': twilio call', [\n 'calleeId' => $calleeId,\n 'callerId' => $callerId,\n ]);\n\n $twilioParticipant = $twilio->createParticipantInstance(\n $session->id_string,\n $callerId,\n $calleeId,\n $twilioConferenceSettings\n );\n\n $organiserConnection\n ->setTelephonyProviderId($twilioParticipant->callSid)\n ->save();\n\n $session\n ->setTelephonyProviderId($twilioParticipant->conferenceSid)\n ->save();\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function dialCoach(Models\\Activity $activity, Models\\User $user): void\n {\n throw new \\LogicException('Not implemented yet');\n }\n\n /**\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function terminate(VO\\ConferenceManager\\ActivityTerminate $data): void\n {\n if (! $data instanceof VO\\ConferenceManager\\SoftphoneTerminate) {\n throw new UnexpectedValueException(sprintf(\n 'Expecting an instance of \"%s\", but \"%s\" given.',\n VO\\ConferenceManager\\SoftphoneTerminate::class,\n \\get_class($data)\n ));\n }\n\n $this->logger->debug(__METHOD__, [\n 'activity' => $data->getActivity()->id_string,\n 'user' => $data->getUser()->id_string,\n ]);\n\n $activity = $data->getActivity();\n $roomOwner = $activity->getUser();\n\n $isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();\n\n if ($isCancelling) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n }\n\n /** @var Models\\Session $session */\n $session = $activity->sessions->first();\n\n /** @var Models\\Participant $organiser */\n $organiser = $activity->organizer;\n\n /** @var Connection $organiserConnection */\n $organiserConnection = $organiser->connections()\n ->whereIn('type', [\n Connection::TYPE_WEBRTC,\n Connection::TYPE_PSTN,\n ])\n ->firstOrFail();\n\n if (! $organiserConnection->hasTelephonyProviderId()) {\n throw new LogicException(\n 'Unable to terminate call as we do not yet know the organiser telephony provider id',\n );\n }\n\n $twilioClient = $this->twilioClientBuilder->build($activity->getTeam());\n\n $twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());\n\n $session->hasTelephonyProviderId()\n && $twilioClient->terminateConference($session->getTelephonyProviderId());\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'session' => $session->id_string,\n ]);\n }\n\n public function answer(VO\\ConferenceManager\\ActivityAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'data' => $data,\n ]);\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneInboundAnswer) {\n return $this->answerInboundCall($data);\n }\n\n if ($data instanceof VO\\ConferenceManager\\SoftPhoneOutboundAnswer) {\n return $this->answerOutboundCall($data);\n }\n\n throw new LogicException('Unable to answer - not supported activity answer instance');\n }\n\n private function answerInboundCall(VO\\ConferenceManager\\SoftPhoneInboundAnswer $data): TwiML\\VoiceResponse\n {\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n ]);\n\n $dateTime = now();\n\n $activity = $this->create(\n new VO\\ConferenceManager\\SoftPhoneInboundCreate(\n $data->getCallerId(),\n $data->getCallSid(),\n $data->getUser()\n )\n );\n\n /** @var Models\\Participant $activityGuest */\n $activityGuest = $activity->from;\n\n $activityGuest\n ->setEnterTime($dateTime)\n ->save();\n\n /** @var Connection $connection */\n $connection = $activityGuest->connections()\n ->where('type', Connection::TYPE_PSTN)\n ->firstOrFail();\n\n $connection\n ->setTelephonyProviderId($data->getCallSid())\n ->setOpenedAt($dateTime)\n ->save();\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n return $this->joinTwilioConference($connection, [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]);\n }\n\n private function answerOutboundCall(VO\\ConferenceManager\\SoftPhoneOutboundAnswer $data): TwiML\\VoiceResponse\n {\n $connection = $data->getConnection();\n $participant = $connection->getParticipant();\n $activity = $data->getActivity();\n $organiser = $activity->organizer;\n $dateTime = now();\n\n $this->logger->debug(__METHOD__, [\n 'callSid' => $data->getCallSid(),\n 'connection' => $connection->id_string,\n 'participant' => $participant->id_string,\n 'activity' => $activity->id_string,\n ]);\n\n if (! $participant->hasEnterTime()) {\n $participant->update([\n 'enter_time' => $dateTime,\n ]);\n }\n\n $connection->update([\n 'is_muted' => $participant->isCoach(),\n 'opened_at' => $dateTime,\n ]);\n\n event(new Events\\Activities\\Connections\\Opened($connection));\n\n if ($participant->isCoach()) {\n /** @var Connection $roomOwnerConnection */\n $roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'coach' => $roomOwnerConnection->getTelephonyProviderId(),\n ]\n );\n }\n\n return $this->joinTwilioConference(\n $connection,\n [\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n 'muted' => false,\n ]\n );\n }\n\n private function determineUserCallerId(Models\\User $user): string\n {\n if ($user->hasCallerId()) {\n return $user->getCallerId();\n }\n\n // TODO: We should require a caller id to be verified before using the dialer\n return $user->getSoftPhoneNumber() ?? $user->phone;\n }\n\n private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool\n {\n $startsWith = '[HTTP 404] Unable to create record: The requested resource';\n $endsWith = 'was not found';\n\n return Str::startsWith($exceptionMessage, $startsWith)\n && Str::endsWith($exceptionMessage, $endsWith);\n }\n\n /**\n * @throws RestException\n * @throws ConfigurationException\n * @throws TwilioException\n */\n public function dialOtherParty(Connection $callerConnection): self\n {\n $session = $callerConnection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n $twilio = $this->twilioClientBuilder->build($activity->getTeam());\n\n $this->logger->debug(__METHOD__, [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {\n throw new UnexpectedValueException('Unexpected activity type');\n }\n\n $callee = $activity->to;\n $calleeId = $callee->getPhoneNumber();\n\n /** @var Connection $calleeConnection */\n $calleeConnection = $callee->connections()\n ->whereIn('type', [\n Connection::TYPE_PSTN,\n Connection::TYPE_WEBRTC,\n ])\n ->firstOrFail();\n\n $caller = $activity->from;\n\n // we need to take the number from the connection\n // as the one we store on participant level might be the one we call to the rep via PSTN mode\n $callerId = $callerConnection->getPhoneNumber();\n\n if (! $caller->is($callerConnection->getParticipant())) {\n throw new LogicException(sprintf(\n 'Unexpected caller participant. Expected: \"%s\", Actual: \"%s\"',\n $caller->id_string,\n $callerConnection->getParticipant()->id_string\n ));\n }\n\n $twilioConferenceSettings = [\n 'muted' => false,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => true,\n ];\n\n $hasInboundRecording = $activity->isTypeSoftphoneInbound()\n && $roomOwner->checkSoftphoneInboundRecordPreference();\n\n $hasOutboundRecording = $activity->isTypeSoftPhone()\n && $roomOwner->checkSoftphoneOutboundRecordPreference();\n\n $isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;\n\n $isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()\n || $activity->hasRecordingReasonComplianceRestricted();\n\n if ($isRecordingEnabled && ! $isRecordingRestricted) {\n $twilioConferenceSettings = array_merge($twilioConferenceSettings, [\n 'record' => true,\n 'recordingChannels' => 'dual',\n 'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),\n 'recordingStatusCallbackMethod' => 'POST',\n 'recordingStatusCallbackEvent' => 'in-progress completed absent',\n ]);\n\n if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n $twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()\n ? 'outbound'\n : 'inbound';\n }\n }\n\n try {\n if ($activity->hasRecordingReasonCompliancePrompted()) {\n $this->logger->info('[Softphone] Request compliance prompt via outbound call', [\n 'activity_uuid' => $activity->getUuid(),\n 'session' => $session->id_string,\n 'conference_sid' => $session->getTelephonyProviderId(),\n ]);\n\n $callInstance = $twilio->createCall(\n $callerId,\n $calleeId,\n $this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [\n 'session' => $session->id_string,\n 'connection' => $calleeConnection->id_string,\n ]),\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $callInstance->sid,\n 'parent_connection_id' => $callerConnection->getId(),\n ]);\n } else {\n $twilioParticipantConnection = $twilio->createParticipantInstance(\n $session->getTelephonyProviderId(),\n $callerId,\n $calleeId,\n $this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)\n );\n\n $calleeConnection->update([\n 'telephony_provider_id' => $twilioParticipantConnection->callSid,\n ]);\n }\n } catch (RestException $restException) {\n if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {\n throw $restException;\n }\n\n if (! $activity->isInFiniteState()) {\n $activity\n ->setStatus(Models\\Activity::STATUS_CANCELLED)\n ->save();\n\n $message = __METHOD__ . ': activity.status -> cancelled';\n } else {\n $message = sprintf(\n __METHOD__ . ': not setting to cancelled, activity.status is %s',\n $activity->getStatus()\n );\n }\n\n $this->logger->info($message, [\n 'activity' => $activity->id_string,\n 'exception' => $restException,\n ]);\n }\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n 'connection' => $callerConnection->id_string,\n ]);\n\n return $this;\n }\n\n private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array\n {\n $session = $connection->getSession();\n\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => true,\n 'endConferenceOnExit' => false,\n 'record' => false,\n 'waitUrl' => '',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]),\n 'statusCallbackEvent' => [\n 'initiated',\n 'ringing',\n 'answered',\n 'completed',\n ],\n 'statusCallbackMethod' => 'POST',\n 'conferenceStatusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n ]),\n 'conferenceStatusCallbackMethod' => 'POST',\n 'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function applyDefaultTwilioConferenceSettings(Models\\Session $session, array $override = []): array\n {\n $default = [\n 'beep' => false,\n 'muted' => true,\n 'startConferenceOnEnter' => false,\n 'endConferenceOnExit' => false,\n 'waitUrl' => '',\n 'statusCallbackEvent' => implode(' ', [\n 'start',\n 'end',\n 'join',\n 'leave',\n 'mute',\n 'hold',\n// 'speaker',\n ]),\n 'statusCallbackMethod' => 'POST',\n 'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [\n 'session' => $session->getIdString(),\n ]),\n ];\n\n if ($session->hasRegion()) {\n $default['region'] = $session->getRegion()->getName();\n }\n\n return array_merge($default, $override);\n }\n\n private function joinTwilioConference(Connection $connection, array $options = []): TwiML\\VoiceResponse\n {\n $session = $connection->getSession();\n $activity = $session->getActivity();\n $roomOwner = $activity->getUser();\n\n $this->logger->debug(__METHOD__, [\n 'connection' => $connection->id_string,\n 'session' => $session->id_string,\n 'activity' => $activity->id_string,\n 'user' => $roomOwner->id_string,\n ]);\n\n $voiceResponse = new TwiML\\VoiceResponse();\n\n $voiceResponse->dial()\n ->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [\n 'connection' => $connection->id_string,\n ]))\n ->conference(\n $session->getIdString(),\n $this->applyDefaultTwilioConferenceSettings($session, $options)\n );\n\n $this->logger->debug(__METHOD__ . ': done', [\n 'session' => $session->id_string,\n 'result' => $voiceResponse,\n ]);\n\n return $voiceResponse;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6747064852800206703
|
8672411643061121689
|
visual_change
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
13 files, folder partially checked
app 10 files, folder partially checked
Component 2 files, folder checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Jobs/Activity/Import 3 files, folder checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
Listeners/Teams 1 file, folder checked
UpdateSalesforceAccount.php, final class checked
Services 2 files, folder partially checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder not checked
.env.local not checked
app 10 files, folder partially checked
Component 2 files, folder checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
Jobs/Activity/Import 3 files, folder checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
Listeners/Teams 1 file, folder checked
UpdateSalesforceAccount.php, final class checked
Services 2 files, folder partially checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
Component 2 files, folder checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
Twilio/Conference/ConferenceManager 1 file, folder checked
SoftPhoneManager.php, final class checked
SoftPhoneManager.php, final class checked
Uploader/Services 1 file, folder checked
CreateActivityFromUploadService.php, class checked
CreateActivityFromUploadService.php, class checked
Console/Commands 1 file, folder not checked
Http/Controllers/API 1 file, folder checked
ActivityController.php, final class checked
ActivityController.php, final class checked
Jobs/Activity/Import 3 files, folder checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
HydrateCrmDataByExternalCallIdJob.php, class checked
MatchCrmData.php, class checked
UpdateCrmFieldDataJob.php, class checked
Listeners/Teams 1 file, folder checked
UpdateSalesforceAccount.php, final class checked
UpdateSalesforceAccount.php, final class checked
Services 2 files, folder partially checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
Mail 1 file, folder not checked
Telephony 1 file, folder checked
TextMessagingService.php, class checked
TextMessagingService.php, class checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder not checked
.env.local not checked
8 modified
JY-20963 fix deleted object import
Commit Message
Commit Message History
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
3 differences
1aee7aad
<?php
declare(strict_types=1);
namespace Jiminny\Component\Twilio\Conference\ConferenceManager;
use Illuminate\Database\Eloquent;
use Illuminate\Support\Str;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Component\Twilio\Conference\ConferenceManager;
use Jiminny\Component\Twilio\Exception\OutOfBoundsException;
use Jiminny\Component\Twilio\Exception\UnexpectedValueException;
use Jiminny\Component\Twilio\Resolver;
use Jiminny\Component\Twilio\VO;
use Jiminny\Component\UrlGenerator\Webhook;
use Jiminny\Events;
use Jiminny\Exceptions\LogicException;
use Jiminny\Models;
use Jiminny\Models\Participant\Connection;
use Jiminny\Services\ActivityService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\ProviderRegistry;
use Jiminny\Services\Telephony\TwilioClientBuilder;
use Psr\Log\LoggerInterface;
use Twilio\Exceptions\ConfigurationException;
use Twilio\Exceptions\RestException;
use Twilio\Exceptions\TwilioException;
use Twilio\TwiML;
final readonly class SoftPhoneManager implements ConferenceManager
{
public const int COMPLIANCE_PROMPT_TIMEOUT_SECONDS = 20;
public const string COMPLIANCE_PROMPT_ACCEPT_DIGIT = '1';
public function __construct(
private ProviderRegistry $crmProviderRegistry,
private Webhook $urlGenerator,
private ActivityService $activityService,
private LoggerInterface $logger,
private TwilioClientBuilder $twilioClientBuilder,
) {
}
public function create(VO\ConferenceManager\ActivityCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'data' => $data,
'instanceof' => get_class($data),
]);
$activity = Observer::skipEvents(function () use ($data) {
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundCreate) {
$activity = $this->createInboundActivity($data);
} elseif ($data instanceof VO\ConferenceManager\SoftPhoneOutboundCreate) {
$activity = $this->createOutboundActivity($data);
} else {
throw new LogicException(sprintf(
'Unknown softphone implementation: %s',
get_class($data)
));
}
return $activity;
});
$this->logger->debug(__METHOD__ . ': done', [
'instanceof' => get_class($data),
'activity' => $activity->id_string,
]);
return $activity;
}
private function createOutboundActivity(VO\ConferenceManager\SoftPhoneOutboundCreate $data): Models\Activity
{
$startTime = microtime(true);
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
$now = now();
$user = $data->getUser();
$team = $user->getTeam();
$device = $data->getDevice();
$phoneNumber = $data->getPhoneNumber();
$route = $data->getPhoneRoute();
if (! $this->activityService->teamCanDial($team, $phoneNumber)) {
$region = getRegionByNumber($phoneNumber, $user->getCountryCode());
throw new LogicException(sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? 'this number'
));
}
$lead = null;
$account = null;
$contact = null;
$stage = null;
$opportunity = null;
// Setup services.
$crmService = $this->crmProviderRegistry->get($team->crm->provider);
if (! $user->isCrmRequired()) {
$crmService->setUser($user->team->owner);
} else {
$crmService->setUser($user);
}
$prospectId = $data->getProspectId();
try {
if ($prospectId !== null) {
$crmStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM parseRecords', [
'prospect_id' => $prospectId,
'object_type' => $data->getObjectType(),
]);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->parseRecords(
$prospectId,
$data->getObjectType()
);
$this->logger->info('[SoftPhoneManager] CRM parseRecords completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'parse_duration_ms' => round((microtime(true) - $crmStart) * 1000, 2),
'found_lead' => $lead !== null,
'found_account' => $account !== null,
'found_opportunity' => $opportunity !== null,
'found_contact' => $contact !== null,
]);
// Try to attach a contact if we have an opportunity.
if ($opportunity !== null && $contact === null) {
$matchStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting CRM matchByPhone');
[, , , $contact, ] = $crmService->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
$this->logger->info('[SoftPhoneManager] CRM matchByPhone completed', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'match_duration_ms' => round((microtime(true) - $matchStart) * 1000, 2),
'found_contact' => $contact !== null,
]);
}
} else {
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
[$lead, $account, $opportunity, $contact, $stage] = $decorator->matchByPhone(
$data->getPhoneNumber(),
$data->getRawPhoneNumber(),
$user->getId()
);
}
} catch (\Throwable $throwable) {
$this->logger->error('[' . $crmService->getDisplayName() . '] Error looking up customer', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an outbound call from happening.
}
if ($data->hasProspectStage()) {
/** @var Models\Stage $stage */
$stage = $team->crm->stages()->uuid($data->getProspectStage());
}
$dbStart = microtime(true);
$this->logger->info('[SoftPhoneManager] Starting activity creation in DB');
$activity = $user->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE,
'provider' => Models\Activity::PROVIDER_TWILIO,
'device_id' => $device->id ?? null,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'value' => $opportunity ? $opportunity->value : null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_PENDING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $phoneNumber),
'scheduled_start_time' => $now,
]);
$this->logger->info('[SoftPhoneManager] Activity created in DB', [
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'db_duration_ms' => round((microtime(true) - $dbStart) * 1000, 2),
'activity_id' => $activity->id ?? null,
]);
if (! $activity instanceof Models\Activity) {
throw new LogicException('Activity model expected');
}
$regionId = null;
if ($user->hasRegionId()) {
$regionId = $user->getRegionId();
} elseif ($data->hasRegion()) {
$regionId = $data->getRegion()->getId();
}
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $regionId,
'is_muted' => false,
]);
$phoneRouteResolver = new Resolver\PhoneRouteResolver($user, $route);
$phoneNumberOrigin = $phoneRouteResolver->getPhoneNumberOrigin();
$connectionType = $phoneRouteResolver->getConnectionType();
$callerId = $this->determineUserCallerId($user);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'phone_number' => $phoneRouteResolver->mustUsePSTN()
? $phoneNumberOrigin
: $callerId,
]);
$caller->connections()->create([
'session_id' => $session->getId(),
'direction' => $phoneRouteResolver->mustUsePSTN() ? 'outbound-api' : 'outbound',
'phone' => $callerId,
'type' => $connectionType,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $phoneNumber,
'name' => $activity->prospect_name,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'phone' => $phoneNumber,
'direction' => 'outbound-api',
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$activity->refresh();
$this->logger->info('[SoftPhoneManager] createOutboundActivity completed', [
'total_duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'activity_id' => $activity->id_string,
]);
$this->logger->debug(__METHOD__ . ': done', [
'data' => $data,
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
return $activity;
}
private function createInboundActivity(VO\ConferenceManager\SoftPhoneInboundCreate $data): Models\Activity
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$roomOwner = $data->getUser();
$team = $roomOwner->getTeam();
$phoneNumber = $roomOwner->getSoftPhoneInboundDestinationNumber();
$callerId = $data->getCallerId();
$callerCallSid = $data->getCallSid();
$now = now();
if (! $this->activityService->teamCanDial($roomOwner->getTeam(), $roomOwner->softphone_inbound_destination)) {
$region = getRegionByNumber($roomOwner->softphone_inbound_destination, $roomOwner->getCountryCode());
throw new LogicException(
sprintf(
'Sorry, dialing %s is not available with your subscription.'
. ' Please contact your manager to enable additional destinations.',
$region ?? ' this number',
),
);
}
$lead = $account = $opportunity = $contact = $stage = null;
try {
$crmService = $this->crmProviderRegistry->get($roomOwner->getTeam()->crm->provider);
$crmService->setUser($roomOwner);
[$lead, $account, $opportunity, $contact, $stage] = $crmService->matchByPhone(
$callerId,
null,
$roomOwner->getId()
);
} catch (\Throwable $throwable) {
$this->logger->error(__METHOD__ . ': crm exception', [
'exception' => $throwable,
]);
// maybe their crm is disconnected, but this shouldn't prevent an inbound call from happening
}
/** @var Models\Activity $activity */
$activity = $roomOwner->activities()->create([
'type' => Models\Activity::TYPE_SOFTPHONE_INBOUND,
'provider' => Models\Activity::PROVIDER_TWILIO,
'crm_configuration_id' => $team->crm_id,
'lead_id' => $lead->id ?? null,
'account_id' => $account->id ?? null,
'contact_id' => $contact->id ?? null,
'opportunity_id' => $opportunity->id ?? null,
'stage_id' => $stage->id ?? null,
'status' => Models\Activity::STATUS_RINGING,
'recording_state' => Models\Activity::RECORDING_OFF,
'recording_reason_code' => $this->getVoiceRecordingReasonCode($team, $callerId),
'scheduled_start_time' => $now,
]);
/** @var Models\Session $session */
$session = $activity->sessions()->create([
'type' => Models\Session::TYPE_DIALER,
'region_id' => $roomOwner->getRegionId(),
'is_muted' => false,
]);
/** @var Models\Participant $caller */
$caller = $activity->participants()->create([
'lead_id' => $activity->lead_id ?? null,
'contact_id' => $activity->contact_id ?? null,
'phone_number' => $callerId,
'name' => $activity->prospect_name,
'enter_time' => $now,
]);
$caller->connections()->create([
'telephony_provider_id' => $callerCallSid,
'session_id' => $session->getId(),
'phone' => $callerId,
'direction' => 'inbound',
'type' => Connection::TYPE_PSTN,
'opened_at' => $now,
]);
/** @var Models\Participant $callee */
$callee = $activity->participants()->create([
'user_id' => $roomOwner->id,
'email' => $roomOwner->email,
'name' => $roomOwner->name,
'phone_number' => $phoneNumber,
]);
$callee->connections()->create([
'session_id' => $session->getId(),
'direction' => 'outbound-api',
'phone' => $phoneNumber,
'type' => Connection::TYPE_PSTN,
]);
$activity->update([
'from_participant_id' => $caller->id,
'to_participant_id' => $callee->id,
]);
$this->logger->debug(__METHOD__ . ': done', [
'callSid' => $data->getCallSid(),
'activity' => $activity->id_string,
'session' => $session->id_string,
'caller' => $caller->id_string,
'callee' => $callee->id_string,
]);
$activity->refresh();
return $activity;
}
private function getVoiceRecordingReasonCode(Models\Team $team, string $phoneNumber): int
{
$this->logger->debug(__METHOD__, [
'team' => $team->id_string,
'phone' => $phoneNumber,
]);
if (
! $team->hasComplianceModeRecordingPrompt()
&& ! $team->hasComplianceModeRecordingRestrictToOneSide()
&& ! $team->hasComplianceModeRecordingRestrictRecording()
) {
return 0;
}
/** @var Models\VoiceConsentPrefix|null $voiceConsentPolicy */
$voiceConsentPolicy = Models\VoiceConsentPrefix::query()
->whereRaw("? LIKE CONCAT(destination_prefix, '%')", [$phoneNumber])
->where(static function (Eloquent\Builder $builder) use ($team): void {
$builder
->whereNull('team_id')
->orWhere('team_id', $team->getId());
})
->orderByDesc('team_id')
->first();
if (! $voiceConsentPolicy instanceof Models\VoiceConsentPrefix || $voiceConsentPolicy->isAllowed()) {
return 0;
}
if ($team->hasComplianceModeRecordingPrompt()) {
// This number requires consent from the other party before recording the call.
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT;
}
if ($team->hasComplianceModeRecordingRestrictToOneSide()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE;
}
if ($team->hasComplianceModeRecordingRestrictRecording()) {
return Models\Activity::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT;
}
return 0;
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOrganiser(Models\Activity $activity): void
{
$this->logger->debug(__METHOD__, [
'activity' => $activity->id_string,
]);
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$canBeDialed =
$activity->isTypeSoftPhone()
&& ! $activity->hasStarted()
&& $activity->isPending()
&& $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->exists();
if (! $canBeDialed) {
throw new OutOfBoundsException('Cannot make an outbound call to this activity.');
}
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->where('type', '=', Connection::TYPE_PSTN)
->whereNull('opened_at')
->firstOrFail();
/** @var Models\Session $session */
$session = $organiserConnection->getSession();
$twilioConferenceSettings = $this->applyDefaultTwilioParticipantSettings($organiserConnection, [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
]);
// What number will the organizer see as caller id
$callerId = $roomOwner->getSoftPhoneNumber();
if ($callerId === null) {
$callee = $activity->participants()->whereNull('user_id')->first();
// This is a bridge call. We make it look like the callee is calling the organizer
$callerId = $callee->phone_number;
}
// What number will the prospect see as caller id
$calleeId = $organiser->getPhoneNumber();
$this->logger->debug(__METHOD__ . ': twilio call', [
'calleeId' => $calleeId,
'callerId' => $callerId,
]);
$twilioParticipant = $twilio->createParticipantInstance(
$session->id_string,
$callerId,
$calleeId,
$twilioConferenceSettings
);
$organiserConnection
->setTelephonyProviderId($twilioParticipant->callSid)
->save();
$session
->setTelephonyProviderId($twilioParticipant->conferenceSid)
->save();
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'session' => $session->id_string,
]);
}
public function dialCoach(Models\Activity $activity, Models\User $user): void
{
throw new \LogicException('Not implemented yet');
}
/**
* @throws ConfigurationException
* @throws TwilioException
*/
public function terminate(VO\ConferenceManager\ActivityTerminate $data): void
{
if (! $data instanceof VO\ConferenceManager\SoftphoneTerminate) {
throw new UnexpectedValueException(sprintf(
'Expecting an instance of "%s", but "%s" given.',
VO\ConferenceManager\SoftphoneTerminate::class,
\get_class($data)
));
}
$this->logger->debug(__METHOD__, [
'activity' => $data->getActivity()->id_string,
'user' => $data->getUser()->id_string,
]);
$activity = $data->getActivity();
$roomOwner = $activity->getUser();
$isCancelling = ! $activity->isInFiniteState() && ! $activity->isInProgress();
if ($isCancelling) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
}
/** @var Models\Session $session */
$session = $activity->sessions->first();
/** @var Models\Participant $organiser */
$organiser = $activity->organizer;
/** @var Connection $organiserConnection */
$organiserConnection = $organiser->connections()
->whereIn('type', [
Connection::TYPE_WEBRTC,
Connection::TYPE_PSTN,
])
->firstOrFail();
if (! $organiserConnection->hasTelephonyProviderId()) {
throw new LogicException(
'Unable to terminate call as we do not yet know the organiser telephony provider id',
);
}
$twilioClient = $this->twilioClientBuilder->build($activity->getTeam());
$twilioClient->terminateVoiceCall($organiserConnection->getTelephonyProviderId());
$session->hasTelephonyProviderId()
&& $twilioClient->terminateConference($session->getTelephonyProviderId());
$this->logger->debug(__METHOD__ . ': done', [
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'session' => $session->id_string,
]);
}
public function answer(VO\ConferenceManager\ActivityAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'data' => $data,
]);
if ($data instanceof VO\ConferenceManager\SoftPhoneInboundAnswer) {
return $this->answerInboundCall($data);
}
if ($data instanceof VO\ConferenceManager\SoftPhoneOutboundAnswer) {
return $this->answerOutboundCall($data);
}
throw new LogicException('Unable to answer - not supported activity answer instance');
}
private function answerInboundCall(VO\ConferenceManager\SoftPhoneInboundAnswer $data): TwiML\VoiceResponse
{
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
]);
$dateTime = now();
$activity = $this->create(
new VO\ConferenceManager\SoftPhoneInboundCreate(
$data->getCallerId(),
$data->getCallSid(),
$data->getUser()
)
);
/** @var Models\Participant $activityGuest */
$activityGuest = $activity->from;
$activityGuest
->setEnterTime($dateTime)
->save();
/** @var Connection $connection */
$connection = $activityGuest->connections()
->where('type', Connection::TYPE_PSTN)
->firstOrFail();
$connection
->setTelephonyProviderId($data->getCallSid())
->setOpenedAt($dateTime)
->save();
event(new Events\Activities\Connections\Opened($connection));
return $this->joinTwilioConference($connection, [
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]);
}
private function answerOutboundCall(VO\ConferenceManager\SoftPhoneOutboundAnswer $data): TwiML\VoiceResponse
{
$connection = $data->getConnection();
$participant = $connection->getParticipant();
$activity = $data->getActivity();
$organiser = $activity->organizer;
$dateTime = now();
$this->logger->debug(__METHOD__, [
'callSid' => $data->getCallSid(),
'connection' => $connection->id_string,
'participant' => $participant->id_string,
'activity' => $activity->id_string,
]);
if (! $participant->hasEnterTime()) {
$participant->update([
'enter_time' => $dateTime,
]);
}
$connection->update([
'is_muted' => $participant->isCoach(),
'opened_at' => $dateTime,
]);
event(new Events\Activities\Connections\Opened($connection));
if ($participant->isCoach()) {
/** @var Connection $roomOwnerConnection */
$roomOwnerConnection = $organiser->connections()->openCall()->firstOrFail();
return $this->joinTwilioConference(
$connection,
[
'coach' => $roomOwnerConnection->getTelephonyProviderId(),
]
);
}
return $this->joinTwilioConference(
$connection,
[
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
'muted' => false,
]
);
}
private function determineUserCallerId(Models\User $user): string
{
if ($user->hasCallerId()) {
return $user->getCallerId();
}
// TODO: We should require a caller id to be verified before using the dialer
return $user->getSoftPhoneNumber() ?? $user->phone;
}
private function isRestExceptionAboutTwilioConferenceNotFound(string $exceptionMessage): bool
{
$startsWith = '[HTTP 404] Unable to create record: The requested resource';
$endsWith = 'was not found';
return Str::startsWith($exceptionMessage, $startsWith)
&& Str::endsWith($exceptionMessage, $endsWith);
}
/**
* @throws RestException
* @throws ConfigurationException
* @throws TwilioException
*/
public function dialOtherParty(Connection $callerConnection): self
{
$session = $callerConnection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$twilio = $this->twilioClientBuilder->build($activity->getTeam());
$this->logger->debug(__METHOD__, [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
if (! $activity->isTypeSoftPhone() && ! $activity->isTypeSoftphoneInbound()) {
throw new UnexpectedValueException('Unexpected activity type');
}
$callee = $activity->to;
$calleeId = $callee->getPhoneNumber();
/** @var Connection $calleeConnection */
$calleeConnection = $callee->connections()
->whereIn('type', [
Connection::TYPE_PSTN,
Connection::TYPE_WEBRTC,
])
->firstOrFail();
$caller = $activity->from;
// we need to take the number from the connection
// as the one we store on participant level might be the one we call to the rep via PSTN mode
$callerId = $callerConnection->getPhoneNumber();
if (! $caller->is($callerConnection->getParticipant())) {
throw new LogicException(sprintf(
'Unexpected caller participant. Expected: "%s", Actual: "%s"',
$caller->id_string,
$callerConnection->getParticipant()->id_string
));
}
$twilioConferenceSettings = [
'muted' => false,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => true,
];
$hasInboundRecording = $activity->isTypeSoftphoneInbound()
&& $roomOwner->checkSoftphoneInboundRecordPreference();
$hasOutboundRecording = $activity->isTypeSoftPhone()
&& $roomOwner->checkSoftphoneOutboundRecordPreference();
$isRecordingEnabled = $hasInboundRecording || $hasOutboundRecording;
$isRecordingRestricted = $activity->hasRecordingReasonCompliancePrompted()
|| $activity->hasRecordingReasonComplianceRestricted();
if ($isRecordingEnabled && ! $isRecordingRestricted) {
$twilioConferenceSettings = array_merge($twilioConferenceSettings, [
'record' => true,
'recordingChannels' => 'dual',
'recordingStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.recording.event'),
'recordingStatusCallbackMethod' => 'POST',
'recordingStatusCallbackEvent' => 'in-progress completed absent',
]);
if ($activity->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {
$twilioConferenceSettings['recordingTrack'] = $activity->isTypeSoftPhone()
? 'outbound'
: 'inbound';
}
}
try {
if ($activity->hasRecordingReasonCompliancePrompted()) {
$this->logger->info('[Softphone] Request compliance prompt via outbound call', [
'activity_uuid' => $activity->getUuid(),
'session' => $session->id_string,
'conference_sid' => $session->getTelephonyProviderId(),
]);
$callInstance = $twilio->createCall(
$callerId,
$calleeId,
$this->urlGenerator->route('jiminny.webhook.softphone.compliance.prompt', [
'session' => $session->id_string,
'connection' => $calleeConnection->id_string,
]),
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $callInstance->sid,
'parent_connection_id' => $callerConnection->getId(),
]);
} else {
$twilioParticipantConnection = $twilio->createParticipantInstance(
$session->getTelephonyProviderId(),
$callerId,
$calleeId,
$this->applyDefaultTwilioParticipantSettings($calleeConnection, $twilioConferenceSettings)
);
$calleeConnection->update([
'telephony_provider_id' => $twilioParticipantConnection->callSid,
]);
}
} catch (RestException $restException) {
if (! $this->isRestExceptionAboutTwilioConferenceNotFound($restException->getMessage())) {
throw $restException;
}
if (! $activity->isInFiniteState()) {
$activity
->setStatus(Models\Activity::STATUS_CANCELLED)
->save();
$message = __METHOD__ . ': activity.status -> cancelled';
} else {
$message = sprintf(
__METHOD__ . ': not setting to cancelled, activity.status is %s',
$activity->getStatus()
);
}
$this->logger->info($message, [
'activity' => $activity->id_string,
'exception' => $restException,
]);
}
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
'connection' => $callerConnection->id_string,
]);
return $this;
}
private function applyDefaultTwilioParticipantSettings(Connection $connection, array $override = []): array
{
$session = $connection->getSession();
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => true,
'endConferenceOnExit' => false,
'record' => false,
'waitUrl' => '',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]),
'statusCallbackEvent' => [
'initiated',
'ringing',
'answered',
'completed',
],
'statusCallbackMethod' => 'POST',
'conferenceStatusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
]),
'conferenceStatusCallbackMethod' => 'POST',
'conferenceStatusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function applyDefaultTwilioConferenceSettings(Models\Session $session, array $override = []): array
{
$default = [
'beep' => false,
'muted' => true,
'startConferenceOnEnter' => false,
'endConferenceOnExit' => false,
'waitUrl' => '',
'statusCallbackEvent' => implode(' ', [
'start',
'end',
'join',
'leave',
'mute',
'hold',
// 'speaker',
]),
'statusCallbackMethod' => 'POST',
'statusCallback' => $this->urlGenerator->route('jiminny.webhook.softphone.conference.event', [
'session' => $session->getIdString(),
]),
];
if ($session->hasRegion()) {
$default['region'] = $session->getRegion()->getName();
}
return array_merge($default, $override);
}
private function joinTwilioConference(Connection $connection, array $options = []): TwiML\VoiceResponse
{
$session = $connection->getSession();
$activity = $session->getActivity();
$roomOwner = $activity->getUser();
$this->logger->debug(__METHOD__, [
'connection' => $connection->id_string,
'session' => $session->id_string,
'activity' => $activity->id_string,
'user' => $roomOwner->id_string,
]);
$voiceResponse = new TwiML\VoiceResponse();
$voiceResponse->dial()
->setAction($this->urlGenerator->route('jiminny.webhook.softphone.participant.event', [
'connection' => $connection->id_string,
]))
->conference(
$session->getIdString(),
$this->applyDefaultTwilioConferenceSettings($session, $options)
);
$this->logger->debug(__METHOD__ . ': done', [
'session' => $session->id_string,
'result' => $voiceResponse,
]);
return $voiceResponse;
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71732
|
2573
|
48
|
2026-05-26T07:05:48.148174+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779148148_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.44980052,"top":0.07581804,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"bounds":{"left":0.45910904,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.47041222,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.47772607,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.48636967,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.4950133,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.50598407,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.51462764,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.52327126,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.53424203,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.54521275,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.5718085,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5827792,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"bounds":{"left":0.6761968,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68849736,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.6978058,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"bounds":{"left":0.7094415,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1264749411961195938
|
2218635329548990031
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
71731
|
NULL
|
NULL
|
NULL
|
|
71731
|
2573
|
47
|
2026-05-26T07:05:46.585611+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779146585_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.44980052,"top":0.07581804,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"bounds":{"left":0.45910904,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.47041222,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.47772607,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3283227310384301408
|
-5281383152968365685
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71730
|
2573
|
46
|
2026-05-26T07:05:45.885285+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779145885_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.44980052,"top":0.07581804,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"bounds":{"left":0.45910904,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.47041222,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.47772607,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.48636967,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.4950133,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.50598407,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.51462764,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.52327126,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.53424203,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.54521275,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.5718085,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5827792,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"bounds":{"left":0.6761968,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68849736,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.6978058,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"bounds":{"left":0.7094415,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4251140263196447925
|
2218635329548990023
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;...
|
71729
|
NULL
|
NULL
|
NULL
|
|
71729
|
2573
|
45
|
2026-05-26T07:05:44.606500+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779144606_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.44980052,"top":0.07581804,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"bounds":{"left":0.45910904,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.47041222,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.47772607,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.48636967,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.4950133,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.50598407,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.51462764,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.52327126,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.53424203,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.54521275,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.5718085,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5827792,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"bounds":{"left":0.6761968,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68849736,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.6978058,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"bounds":{"left":0.7094415,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7967649825566240085
|
-6434304657571017973
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71728
|
2573
|
44
|
2026-05-26T07:05:42.860943+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779142860_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.44980052,"top":0.07581804,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"10","depth":4,"bounds":{"left":0.45910904,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.47041222,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.47772607,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded;\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => $messageIds,\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to' => $header->value,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.48636967,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.4950133,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.50598407,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.51462764,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.52327126,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.53424203,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.54521275,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.5718085,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.5827792,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.70611703,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"bounds":{"left":0.6761968,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.68849736,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.6978058,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"bounds":{"left":0.7094415,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1264749411961195938
|
2218635329548990031
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
10
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded;
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = (new EmailTextRelay($messageId, $relayedText))->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => $messageIds,
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to' => $header->value,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
45
1
41
66
Previous Highlighted Error
Next Highlighted Error
SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993
SELECT * FROM users WHERE id = 25061;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 994;
SELECT * FROM crm_profiles WHERE user_id = 25061;
select * from crm_configurations where id = 834;
SELECT * FROM teams WHERE id = 882;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;
SELECT * FROM contacts where crm_configuration_id = 834;
SELECT * FROM opportunities WHERE team_id = 933
# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');
AND id IN (8482561,18352941,19042734,19232139,19445140,19472541);
SELECT * FROM opportunity_contacts
WHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 485; #
SELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
select crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id
where crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')
# and l.converted_at IS NOT NULL
;
# [PASSWORD_DOTS]
SELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')
and opportunity_id IS NULL
order by id desc;
SELECT * FROM teams WHERE id = 604; # 598
SELECT * FROM activities WHERE id = 74410828; # [EMAIL]
SELECT * FROM accounts WHERE id = 20068382;
SELECT * FROM accounts WHERE id = 35186038;
SELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 559 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;
select * from sidekick_settings where team_id = 781;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100
SELECT * FROM crm_layouts WHERE crm_configuration_id = 711;
SELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL
and is_internal = 0 and status = 'completed'
order by id desc;
SELECT * FROM crm_layout_entities
WHERE crm_layout_id IN (2352, 2353);
;
SELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 556 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;
SELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;
select * from contacts
where crm_configuration_id = 530
and crm_provider_id = 872252;
select * from activities where crm_configuration_id = 530
and user_id = 14343 and type like '%softphone%'
and created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya
SELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);
SELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t
JOIN crm_configurations c ON t.id = c.team_id
WHERE t.status = 'active';
SELECT * FROM teams where id = 1091;
SELECT * FROM crm_configurations where team_id = 1091;
SELECT * FROM activity_providers where team_id = 1091;
SELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT * FROM teams WHERE name LIKE '%Leadventure%';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1091 and sa.provider = 'salesforce';
SELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812
SELECT * FROM teams where id = 862;
SELECT * FROM crm_configurations where team_id = 862;
SELECT * FROM activity_providers where team_id = 862;
SELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')
and provider NOT IN ('hubspot', 'aircall')
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by id desc;
SELECT t.id, crm.id, crm.provider, ap.* FROM teams t
join crm_configurations crm on t.id = crm.team_id
join activity_providers ap on t.id = ap.team_id
where t.status = 'active' and ap.is_enabled = 1
and crm.provider = 'hubspot'
and ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',
'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');
SELECT * FROM teams where id = 1068;
SELECT * FROM crm_configurations where team_id = 1068;
SELECT * FROM activity_providers where team_id = 1068;
SELECT * FROM activities a
where crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')
and a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'
)
# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'
order by a.id desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1068 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 933 and sa.provider = 'hubspot';
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262
SELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 882 and sa.provider = 'hubspot';
select * from crm_layouts where crm_configuration_id = 834;
select * from crm_layout_entities where crm_layout_id = 2780;
select * from crm_fields where id IN (321153,321192,321193,321194);
SELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1057 and sa.provider = 'hubspot';
SELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8
SELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20
SELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last
SELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10
SELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;
SELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #
SELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;
select * from users where team_id = 51; # 7783
SELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130
select * from activity_searches where user_id = 7783;
select * from activity_search_filters where activity_search_id IN (32291, 32292);
SELECT asf.activity_search_id, asf.id, asf.value
FROM activity_search_filters asf
WHERE asf.filter = 'group_id'
AND asf.value IN (
SELECT CONCAT(
HEX(SUBSTR(uuid, 5, 4)), '-',
HEX(SUBSTR(uuid, 3, 2)), '-',
HEX(SUBSTR(uuid, 1, 2)), '-',
HEX(SUBSTR(uuid, 9, 2)), '-',
HEX(SUBSTR(uuid, 11))
)
FROM groups
WHERE deleted_at IS NOT NULL
);
SELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th
# [PASSWORD_DOTS]
SELECT * FROM crm_configurations where provider = 'hubspot';
SELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133
SELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;
SELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null
# [PASSWORD_DOTS]
select * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';
select
cp.*
# DISTINCT t.id
# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields
FROM crm_profiles cp
JOIN crm_configurations crm on crm.id = cp.crm_configuration_id
JOIN users u on u.id = cp.user_id
JOIN teams t ON t.id = crm.team_id
WHERE crm.provider = 'salesforce' and t.status = 'active'
and cp.archived_at IS NULL and u.deleted_at IS NULL
and t.id NOT IN (1093)
and t.id = 2
and cp.contact_fields IS NULL;
# and c.crm_provider_id = '003Uu00000ojD4NIAU';
SELECT * FROM users WHERE id = 26484;
SELECT * FROM crm_profiles WHERE user_id = 26484;
SELECT * FROM social_accounts WHERE sociable_id = 26484;
SELECT * FROM crm_configurations where provider = 'salesforce';
select * from users where id IN (10022, 10403);
select * from users where team_id IN (526);
select * from teams where id IN (526, 532);
select * from crm_configurations where id IN (500, 516);
select * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);
select * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 526 and sa.provider = 'salesforce';
select * from team_settings where team_id IN (526, 532);
select * from users where id IN (22824);
select * from crm_profiles where crm_configuration_id IN (1026);
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1093 and sa.provider = 'salesforce';
select * from teams where id = 1099;
select * from users where id = 29643
select * from activity_processing_states;
SELECT * FROM teams where name LIKE '%Fare%'; # 233
SELECT * FROM opportunities where crm_configuration_id = 215
# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'
;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1088 and sa.provider = 'hubspot';
SELECT * FROM teams order by updated_at DESC
SELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account
select * from crm_configurations where provider = 'pipedrive';
select * from teams where id = 957;
select * from crm_configurations where id = 957;
SELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743
SELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;
select * from users where team_id = 1; # 26726 - Gabriela Dureva
SELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific
select * from activities where user_id = 26726 order by id desc;
select * from contacts where crm_configuration_id = 1
and email IN ('[EMAIL]', '[EMAIL]'); # 2094416, 2093620
SELECT * FROM contacts WHERE id = 6284931;
SELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id
WHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;
select * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);
select * from crm_configurations where id = 1;
43801692-1aeb-32ce-acba-5b80a479701a
44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b
405975c0-b3d0-7aaa-821f-09d59cae6dd1
4caf848d-4bed-2299-b248-7788d41f9fca
49bedc3f-f196-eef3-89c3-dea6a3b4aa63
43420989-a09d-b8f8-9806-c8bbf7a02aac
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
SELECT * FROM activities WHERE id = 75461988;
SELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;
select * from contacts where id = 17900517;
select * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id
where crm.provider != 'salesforce';
select * from users where id = 21047;
SELECT * FROM crm_configurations WHERE id = 892;
SELECT * FROM teams WHERE id = 942;
select * from opportunities where team_id = 942 order by updated_at desc;
select * from contacts where team_id = 942 order by updated_at desc;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 942 and sa.provider = 'hubspot';
SELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430
SELECT * FROM crm_configurations WHERE id = 1;
SELECT * FROM teams WHERE crm_id = 1;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1 and sa.provider = 'salesforce';
select id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1
SELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430
select * from teams where id = 852;
select * from groups where id = 2286;
select * from sidekick_settings where team_id = 852;
select * from default_activity_types where team_id = 852;
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1 AND u.deleted_at IS NULL
AND u.crm_required = 1
AND u.team_id = 1
ORDER BY u.team_id;
SELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (
18481
);
SELECT cc.provider, cc.id, p.id, u.*
FROM users u
LEFT JOIN crm_profiles p ON u.id = p.user_id
INNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'
INNER JOIN crm_configurations cc ON t.crm_id = cc.id
WHERE u.status = 1
AND u.deleted_at IS NULL
AND u.crm_required = 1
# AND u.team_id = 1
AND p.id IS NULL -- Move this condition to WHERE clause
ORDER BY u.team_id;
SELECT * FROM opportunities WHERE id = 20002609;
select * from teams where id = 1122; # Velatir, 29953 - [EMAIL]
select * from crm_configurations where id = 1060;
select * from crm_layouts where crm_configuration_id = 1060;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1122 and sa.provider = 'hubspot';
select * from opportunities where team_id = 1122 order by updated_at desc;
select * from crm_field_data where object_type = 'contact';
SELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 248 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS
SELECT * FROM users where id = 24115;
SELECT * FROM accounts where id = 4002896;
SELECT * FROM teams WHERE name LIKE '%adswerve%';
SELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN ("0069N000003GIQ9QAO","0061r000019yGP9AAM","0066900001S2KWlAAN","0066900001TDpj2AAD","0066900001b8uEwAAI","0069N000001rQi0QAE","006QF00000KD40mYAD","006QF00000LzpRJYAZ","0069N000002uomtQAA","0069N000002xlMLQAY","0066900001NV6ubAAD","0061r00001HJp45AAD","006QF00000uTlUoYAK","006QF00000v0bZqYAI");
SELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203
SELECT u.id, u.email, ac.name, a.* FROM activities a
JOIN users u ON a.user_id = u.id
JOIN accounts ac ON a.account_id = ac.id
WHERE
uuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or
uuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or
uuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;
select * from users where id = 5825;
SELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;
select * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;
19594, 862
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 862 and sa.provider = 'salesforce';
select * from automated_reports where id = 36;
select ar.frequency, r.*, ar.* from automated_report_results r
join automated_reports ar on r.report_id = ar.id
where ar.frequency != 'one_off';
select s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;
select * from nudges n where n.activity_search_id
select * from teams where created_at > '2026-03-09';
SELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;
select * from users where team_id = 1 and name like '%Lukas%'; # 7160
SELECT * FROM teams WHERE id = 575;
select * from opportunities where team_id = 575;
SELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,
select * from opportunities where team_id = 1126;
SELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,
select * from opportunities where team_id = 1125;
select * from contacts c
where c.team_id = 882;
SELECT * FROM activities WHERE id = 76822967;
SELECT * FROM crm_profiles WHERE user_id = 15440;
SELECT * FROM crm_profiles WHERE crm_configuration_id = 555;
SELECT * FROM crm_configurations WHERE id = 555;
SELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 581 and sa.provider = 'salesforce';
SELECT * FROM automated_report_results order by id desc;
select * from features;
select * from team_features where feature_id = 40;
select * from teams where id = 556;
select * from automated_reports;
where id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , ["pdf","podcast"]
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from automated_report_results order by id desc;
SELECT * FROM automated_report_results WHERE id = 1919;
select * from automated_report_results WHERE report_id = 54;
select * from opportunities where id = 7594349;
SELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - [EMAIL]
select * from playbooks where team_id = 711; # event 226147
SELECT * FROM playbook_categories WHERE playbook_id = 5515;
SELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';
SELECT * FROM crm_fields WHERE id = 226147;
SELECT * FROM crm_field_values WHERE crm_field_id = 226147;
SELECT * FROM crm_configurations WHERE id = 692;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 711 and sa.provider = 'salesforce';
SELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;
select * from leads;
select * from calendars;
SELECT
t.id AS team_id,
t.name,
LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-05-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = .user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1117 and sa.provider = 'hubspot';
SELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES
SELECT * FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO
SELECT * FROM crm_configurations WHERE id = 1053;
SELECT * FROM teams WHERE id = 1117;
select * from users where id = 30249;
select * from playbooks where id = 5473;
select * from playbook_categories where id = 43783;
select * from playbook_categories where playbook_id = 5473;
select * from crm_fields where id = 659242;
select * from crm_field_values where crm_field_id = 659242;
SELECT * FROM crm_field_data fd
# JOIN crm_fields f ON fd.crm_field_id = f.id
# JOIN activities a ON fd.activity_id = a.id
WHERE activity_id = 79933459
# AND f.crm_provider_id = 'hs_activity_type';
select * from text_relays where created_at > '2026-05-01';
select * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;
select * from users where team_id = 1;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
71727
|
NULL
|
NULL
|
NULL
|
|
71727
|
2573
|
43
|
2026-05-26T07:05:40.971555+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779140971_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomKetdcioOOl-wincowFV faVsco.|s ~.gitignore.p rapstomKetdcioOOl-wincowFV faVsco.|s ~.gitignore.php-cs-fixer.cachecloss Texckeldyservachppiono-cs-tixer.cisconopuoere tunceion synces. arroySexpectedAlias = config( key. "jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all'=)sexoecredhost= contralkex. "11ainny.goo0lgtexthosthwp.onostorm.meta.ongE.phounit.resuit.cacheE.prettierignoreE.windsurfrulesSservice = sthis-xoerSerycelscar boxmessadehistory "Thisoscertistorliseruscomessade ids nwp1denelper-models.omM.CLAUDE.mdcomoostrsonLnar ntot messsoe*[TextRelayService) Starting sync', ["nallbox" => Smailbox,'expected_allas => SexpectedAlias,aynected hostl es Seynecherhost)comoosarockdependency-checkericordewsonEide.txforeach (SnessageHistory as Shistories)Smessages = Shistories-›messagesAddedin ection.son.deMEINSTALum.MEINTERNAL WSSHOOK S3tUP maforeach (Snessages as Smessage)Snessageld = Smessage-›message->id:liminny storaceMallicenses.malif ( Sthis->isForCurnentEnvironment(Sservice, Smailbox, Snessageld, SexpectedAlias, SexpectedHost)) «MMakereconenucrnackage.lock.isonE phpstan.neon.distphpstan-baseline.neor< phpunit.xmlPTAM OLOUEN.MOSANMS maSrelavedText = TextRelay::ahere( column: 'emailprovider_id'. Snessageld)->firstOif (SrelavedText aax null) "Srel avectext & Text Relav:"crectelesonar-project.propertiesRtesto<> Untitied Diagram.xmlis vetur.confialMWERHOOK GI TEOING IMDI GUIENTATION MAenatorowider" a> Tex ReLav:" PROVMUER OSWEEenaorovder1d a> Snessagelidrstatus" a> Textrelav."STATlSPROPESSIs>ih External Librariesv Scratches and Consolesohe nen Fattteyttell aul raccaceld Snel avedteyr sonduene.dConstants+-NlFllF FNATIS)!LochlehnooSheitConsolewwwwtCantrallor mhnanntitnlcantralloreo@/crostoAotivitGromlIninedGonino.nhn. enn/@amnanont/lininador/Gorvinoe→ (a)Do not ignoreHiabliaht woedeXBA?8 1aee7aad .env.local=(C) HydrateCrmDataByExternalCaliIdJob.php app/Jobs/Activity/import@ JiminnyDebugCommand.php app/Console/CommandsSENURRY HEADER OUSTIONLASPEphe logging.php configl@ MatchCrmData.php apo/Jobs/Activity/import— пв соммєсттомелисл!( SoftPhoneManager.php app/Component/Twilio/Conference/ConferenceManaol© TextMessagingService.php app/Services/Telephony08_HOST:127.0.0.1nelonorerta© TextRelayService.php app/Services/Mail( TextRelayServiceTest.pho tests/Unit/Services/Mailne InATARAsErmonsNA TIGCOMANS-NAAT© UpdateCrmFieldDataJob.php app/Jobs/Activity/impor8 UndateSalesforceAccount.oho.ano/Listeners/TeameDB PASSWORD=[PASSWORD] Filce 9 fledB CUNNECTLUN UNLUZONLOв інаст пнтлоьлсв Алднол затоллої.Tocte noceodt 20 la minitoe andTextRelayServiceTest0O%L7oP lUC coMdy TUUO.4U+0.customuolaraveio"SF- uiminnytblocalhost& HS_Jocal (jiminny@localhostA1A10 A VA console (EUA console [STAGINGEBE715716715$726721723=724725=720729=73CONCAT(U.10, CASE WHEN U.1d = t.ouner_1d THEN • (ONneN)• ELSE •E EXDI AS USeR TdU.chostsa.*t.ouner_id FROM social_accounts stJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.team_icWHERE u.tean_id = 1117 and sa.provider = 'hubspot':SELECT * FROM actávities WHEREuuidto bin(*8024fffb-2df7-4017-91f4-d9f896050248') = uutSELECT * FROM actávities WHERE uuid to bin(*[CREDIT_CARD]-927f-4f4da2a8185c') = uutSELECT * FROMcrn configurations WHERE id = 1053SELECT * FROM teans WHERE id = 1117:Useiwneitsoleyselect * fron plavbooks where 1d = 5473select * fron plavbook carecortes where 10 = 457855plavoook carecortes where playoook1d = 54055where 10= 659242select * fron con Sield values where crn Sield 1d = 659242,SELECT * FROM crn_field_data foJouk con flelds +owto.conteldidet# JOIN activities a ON fd.activity_id = a.idHERE aCTMITy 0=19333459WAMD € con orouiden fa = the actiuity tune!.select * fron text_relays where created_at > "2826-85-81":eallaet *Fros dertvitle noerd neen aliu tshe Raneal and eosaterar 17:0121277select * fron users where tean_1d = 1;umhonerhehyorPehoce All tens are now pussing consistenuy, Loume create a summahy of niat wos occom plisncoTwtoweroeuccessuivaodtcmdrxtoweceenwnodon.commtahaneiuathetcauraoatorthaaohooenoll0oc.nochhoohe/tAYtRATwCarscatndmmac.nliscues Fixed1. Incorrect Gmail Class Names: Fixed GooglelService\GnailVMistoryMessage to GooglelService\Gna1l\HistoryMessageAsdrd and uodated method calls from sessage property to cetMessaoelo method2. Complex Model Mocking Issues: Simplified the test approach by using partial mocks instead of complex alias/overloadmockina that was causing contucts with existing classes3. Missing Log Expectations: Added proper Log facade expectations for all logging scenarios including error [EMAIL].ndexorc100ns1wirechoetniurtsTest Cowcrnac Addind,Naw Tact Methode•testSyncW{thEnhancedLogging() - Tests start and completion logs with proper contexttsuncwthRenuseo.essaoel00s0-estsuncowwihtetsmast00000o• testGetHistoryLogsError0nException() - Tests error logging in getHistory methodtestisForcurrentenvironmentLogsRetusecMessagel =Tests retusca messggc loggingEnhanend exist na tosts to verity warn na and error loacindotn scenanos CovercoSync start logs with mailbox, expected alias, and expected, hostSune comoletion loas with ma box, masenges procassed, and mascaoeRefused message logs with message_id and original. toException error loas with proner contextAck anything (XOL)Ad dikoroncadCurrent versionSECUREY HEANER CUS TON OSPEDB CONNECTION=mysqNe HnG SnanndrneunGGanniindiAR PADT-ZZA,DB DATABASES iminnyDB USERNAMEsimnyadminDB PASSWORD=[PASSWORD] ADMIN USERNAME=imnyadnin#DB ADMIN PASSWORD=[PASSWORD]...
|
NULL
|
6838104542103114193
|
NULL
|
click
|
ocr
|
NULL
|
rapstomKetdcioOOl-wincowFV faVsco.|s ~.gitignore.p rapstomKetdcioOOl-wincowFV faVsco.|s ~.gitignore.php-cs-fixer.cachecloss Texckeldyservachppiono-cs-tixer.cisconopuoere tunceion synces. arroySexpectedAlias = config( key. "jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all'=)sexoecredhost= contralkex. "11ainny.goo0lgtexthosthwp.onostorm.meta.ongE.phounit.resuit.cacheE.prettierignoreE.windsurfrulesSservice = sthis-xoerSerycelscar boxmessadehistory "Thisoscertistorliseruscomessade ids nwp1denelper-models.omM.CLAUDE.mdcomoostrsonLnar ntot messsoe*[TextRelayService) Starting sync', ["nallbox" => Smailbox,'expected_allas => SexpectedAlias,aynected hostl es Seynecherhost)comoosarockdependency-checkericordewsonEide.txforeach (SnessageHistory as Shistories)Smessages = Shistories-›messagesAddedin ection.son.deMEINSTALum.MEINTERNAL WSSHOOK S3tUP maforeach (Snessages as Smessage)Snessageld = Smessage-›message->id:liminny storaceMallicenses.malif ( Sthis->isForCurnentEnvironment(Sservice, Smailbox, Snessageld, SexpectedAlias, SexpectedHost)) «MMakereconenucrnackage.lock.isonE phpstan.neon.distphpstan-baseline.neor< phpunit.xmlPTAM OLOUEN.MOSANMS maSrelavedText = TextRelay::ahere( column: 'emailprovider_id'. Snessageld)->firstOif (SrelavedText aax null) "Srel avectext & Text Relav:"crectelesonar-project.propertiesRtesto<> Untitied Diagram.xmlis vetur.confialMWERHOOK GI TEOING IMDI GUIENTATION MAenatorowider" a> Tex ReLav:" PROVMUER OSWEEenaorovder1d a> Snessagelidrstatus" a> Textrelav."STATlSPROPESSIs>ih External Librariesv Scratches and Consolesohe nen Fattteyttell aul raccaceld Snel avedteyr sonduene.dConstants+-NlFllF FNATIS)!LochlehnooSheitConsolewwwwtCantrallor mhnanntitnlcantralloreo@/crostoAotivitGromlIninedGonino.nhn. enn/@amnanont/lininador/Gorvinoe→ (a)Do not ignoreHiabliaht woedeXBA?8 1aee7aad .env.local=(C) HydrateCrmDataByExternalCaliIdJob.php app/Jobs/Activity/import@ JiminnyDebugCommand.php app/Console/CommandsSENURRY HEADER OUSTIONLASPEphe logging.php configl@ MatchCrmData.php apo/Jobs/Activity/import— пв соммєсттомелисл!( SoftPhoneManager.php app/Component/Twilio/Conference/ConferenceManaol© TextMessagingService.php app/Services/Telephony08_HOST:127.0.0.1nelonorerta© TextRelayService.php app/Services/Mail( TextRelayServiceTest.pho tests/Unit/Services/Mailne InATARAsErmonsNA TIGCOMANS-NAAT© UpdateCrmFieldDataJob.php app/Jobs/Activity/impor8 UndateSalesforceAccount.oho.ano/Listeners/TeameDB PASSWORD=[PASSWORD] Filce 9 fledB CUNNECTLUN UNLUZONLOв інаст пнтлоьлсв Алднол затоллої.Tocte noceodt 20 la minitoe andTextRelayServiceTest0O%L7oP lUC coMdy TUUO.4U+0.customuolaraveio"SF- uiminnytblocalhost& HS_Jocal (jiminny@localhostA1A10 A VA console (EUA console [STAGINGEBE715716715$726721723=724725=720729=73CONCAT(U.10, CASE WHEN U.1d = t.ouner_1d THEN • (ONneN)• ELSE •E EXDI AS USeR TdU.chostsa.*t.ouner_id FROM social_accounts stJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.team_icWHERE u.tean_id = 1117 and sa.provider = 'hubspot':SELECT * FROM actávities WHEREuuidto bin(*8024fffb-2df7-4017-91f4-d9f896050248') = uutSELECT * FROM actávities WHERE uuid to bin(*[CREDIT_CARD]-927f-4f4da2a8185c') = uutSELECT * FROMcrn configurations WHERE id = 1053SELECT * FROM teans WHERE id = 1117:Useiwneitsoleyselect * fron plavbooks where 1d = 5473select * fron plavbook carecortes where 10 = 457855plavoook carecortes where playoook1d = 54055where 10= 659242select * fron con Sield values where crn Sield 1d = 659242,SELECT * FROM crn_field_data foJouk con flelds +owto.conteldidet# JOIN activities a ON fd.activity_id = a.idHERE aCTMITy 0=19333459WAMD € con orouiden fa = the actiuity tune!.select * fron text_relays where created_at > "2826-85-81":eallaet *Fros dertvitle noerd neen aliu tshe Raneal and eosaterar 17:0121277select * fron users where tean_1d = 1;umhonerhehyorPehoce All tens are now pussing consistenuy, Loume create a summahy of niat wos occom plisncoTwtoweroeuccessuivaodtcmdrxtoweceenwnodon.commtahaneiuathetcauraoatorthaaohooenoll0oc.nochhoohe/tAYtRATwCarscatndmmac.nliscues Fixed1. Incorrect Gmail Class Names: Fixed GooglelService\GnailVMistoryMessage to GooglelService\Gna1l\HistoryMessageAsdrd and uodated method calls from sessage property to cetMessaoelo method2. Complex Model Mocking Issues: Simplified the test approach by using partial mocks instead of complex alias/overloadmockina that was causing contucts with existing classes3. Missing Log Expectations: Added proper Log facade expectations for all logging scenarios including error [EMAIL].ndexorc100ns1wirechoetniurtsTest Cowcrnac Addind,Naw Tact Methode•testSyncW{thEnhancedLogging() - Tests start and completion logs with proper contexttsuncwthRenuseo.essaoel00s0-estsuncowwihtetsmast00000o• testGetHistoryLogsError0nException() - Tests error logging in getHistory methodtestisForcurrentenvironmentLogsRetusecMessagel =Tests retusca messggc loggingEnhanend exist na tosts to verity warn na and error loacindotn scenanos CovercoSync start logs with mailbox, expected alias, and expected, hostSune comoletion loas with ma box, masenges procassed, and mascaoeRefused message logs with message_id and original. toException error loas with proner contextAck anything (XOL)Ad dikoroncadCurrent versionSECUREY HEANER CUS TON OSPEDB CONNECTION=mysqNe HnG SnanndrneunGGanniindiAR PADT-ZZA,DB DATABASES iminnyDB USERNAMEsimnyadminDB PASSWORD=[PASSWORD] ADMIN USERNAME=imnyadnin#DB ADMIN PASSWORD=[PASSWORD]...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71726
|
2573
|
42
|
2026-05-26T07:05:36.696957+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779136696_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomFV faVsco.|s ~Proinet v.gitignore.php-cs-fi rapstomFV faVsco.|s ~Proinet v.gitignore.php-cs-fixer.cacheppiono-cs-tixer.cisconowp.onostorm.meta.ongE.phounit.resuit.cacheE.prettierignoreE.windsurfrulesnp ide nelper.ondwp1denelper-models.omM.CLAUDE.mdcomoostrsoncomoosarockdependency-checkericordewsonEide.txin ection.son.deMEINSTALum.MEINTERNAL WSSHOOK S3tUP maliminny storaceMallicenses.malMMakere# package-lock.jsonE phpstan.neon.distphpstan-baseline.neor< phpunit.xmlPTAM OLOUEN.MOSANMS masonar-project.propertiesRtesto<> Untitled Diagram.xmlis vetur.confialMWERHOOK GI TEOING IMDI GUIENTATION MA8898>ih External Librariesv Scratches and ConsolesLochlehnooSheitChandesE .env.local apgActivitvController oho aodfHttolControllers/Ap(@) GreateActivityEromliaioadCantca bo annicomnonent/Uininader/Sarvicas=(@) HudrateCrmDataRvSytemaleaWllob nha aool lohelActivituf moort@ JiminnyDebugCommand.php apo/Console/Commandsphe logging.php configl@ MatchCrmData.php aco/Jobs/Activity/lmport(© SoftPhoneManager.pho app/Component/Twilio/Conference/ConferenceMan© TextMessagingService.php app/Services/Telephony© TextRelayService.php app/Services/Mail@ TextRelayServiceTest.pho tests/Unit/Services/Mai© UodateCrmFieldData.Job.cho app/Jobs/Activity/import8 UndateSalesforceAccount.oho aoo/Listeners/TeamsTocte noceodt 20 la minitoe andketactoOOl-wincowcloss TexckeldyservachA1A10 A Vpuoere tunceion synces. arroySexpectedAlias = config( key. "fiminny.deploy region") === 'cu" ? 'catch-all-cu' : 'catch-all*=sexoecredhost = contrauker. "1ainny.google-texthost"Sservice = sthis-xoerSerycelscar boxmessadehistory Thisoscertistorylseryscomessade ids nLnar ntot messsoe*[TextRelayService) Starting sync', ["nallbox" => Smailbox,'expected_allas => SexpectedAlias,aynected hostl es Seynecherhost)foreach (SnessageHistory as Shistories)Smessages = Shistories-›messagesAddedforeach (Snessages as Smessage)Snessageld = Smessage->message->id:if ( Sthis->isForCurnentEnvironment(Sservice, Smailbox, Snessageld, SexpectedAlias, SexpectedHost)) «conenucrSrelavedText = TextRelay::ahere( column: 'emailprovider_id'. Snessageld)->firstoif (SrelavedText aaz null) "SrelavedText = TextRelav::createllenatorovider" a> TextRelav.: PROVtUER oSWmEenaorovderd a> Snessadeliorstatus" a> TextRelav."STAllSPRODESSgoh-nen sattteyttelaulsraccacetd Snelavedteytsontuenetou" onsranreectlhir i8 1aee7aad .env.localSECURZTY _HEADER_CUSTOM, GSPE→ пе соммєсттомелисл!nA HIGCAe=0B_PORTR33000B DATABASES iminnyANA TIGCOMANS-AATDB PASSWORD=[PASSWORD] CUNNECTLUN UNLUZONLODB HOST_0HI0=host.docken.internalTextRelayServiceTest0O%L7oo lue covdy Tuborst+0.customuolaraveio"Sf- uminnylocalhost& HS_Jocal (jiminny@localhostA console (EUBE715710$726721723=724725=720729=73A console [STAGINGCONCAT(U.Id, CASE NHEN U.$d = t. Onner_1d THEN • (Onner)' ELSE OAENO) AS USER045 A1 A41 У 66 AU.chostsa.*t.ouner_id FROM social_accounts stJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.team_icWHERE u.tean_id = 1117 and sa.provider = 'hubspot':SELECT * FROM actávities WHEREuuidto bin(*8024fffb-2df7-4017-91f4-d9f896050248') = uutSELECT * FROM actávities WHERE uuid to bin(*[CREDIT_CARD]-927f-4f4da2a8185c') = uustSELECT * FROMcrn configurations WHERE id = 1053SELECT * FROM teans WHERE id = 1117:Useiwneitsoleyplaybooks where id= 5473select * fron pl avbook carecortes where 10 = 457855Tron pl avbook carecortes where olavbook.1d = 5475%where 108 655242select * fron crn. field_values where crn field id = 659242,SELECT * FROM crn_fleld_data FoJouk con flelds +owto.conteldde4.d# JOIN activities a ON fd.activity_id = a.idMHERE aCTMITy 0= 1933459IAMD € con ocouiden fa = the actiuity tune!.select * fron text_relays where created_at > "2826-85-81":eallaet *Fros dertvitle noerd neen aliu tshe Raneal and eosaterar 17:0121277select * fron users where tean_id = 1;umhonerhehyorPehoce All tens are now pussing consistenuy, Loume create a summahy of niat wos occom plisncotwtowerosuccessuiv.ndoedtimorxtweceenwnodon.commtahaneiuathetcauraoatorthaaohooenoll0oc.nochhoohe/tAYtRATwCarscatndmmac.nliscues Fixed1. Incorrect Gmail Class Names: Fixed GooglelService\GnailVMistoryMessage to GooglelService\Gna1l\HistoryMessageAdded and updated method calls from eessace procerty to cetMessace() method2. Complex Model Mocking Issues: Simplified the test approach by using partial mocks instead of complex alias/overloadmockina that was causing contuets with existing classes3. Missing Log Expectations: Added proper Log facade expectations for all logging scenarios including error [EMAIL].ndexorc100ns1wirechoetniurtsTest Cowcrnac Addind,Naw Tact MethodetestSyncWithEnhancedLogging() - Tests start and completion logs with proper contexttsuncwthRenuseo.essaoel00s0-estsuncowwihtetsmast00000o• testGetHistoryLogsError0nException() - Tests error logging in getHistory methodtestisForcurrentenvironmentLogsRetusecMessagel =Tests retusca messggc loggingEnhanend exist na tosts to verity warn na and error loacindLotn scensnos coverceSync start logs with mailbox, expected alias, and expected hostSune comoletion loas with ma box, masenges procassed, and mascaoeRefused message logs with message_id and original. toException error loas with proner contextAck anything (XOL)Ad dikoroncadCurrent versionSECUREY HEANER CUS TON OSPEDB CONNECTION=mysqNe HnG SnanndraneunGGnannnhAR PADT-ZZA,DB DATABASES iminnyDB USERNAMEsimnyadminDB PASSWORD=[PASSWORD] ADMIN USERNAME=imnyadnin#DB ADMIN PASSWORD=[PASSWORD] Tosme 720:15uiTee...
|
NULL
|
-3550327497412399342
|
NULL
|
visual_change
|
ocr
|
NULL
|
rapstomFV faVsco.|s ~Proinet v.gitignore.php-cs-fi rapstomFV faVsco.|s ~Proinet v.gitignore.php-cs-fixer.cacheppiono-cs-tixer.cisconowp.onostorm.meta.ongE.phounit.resuit.cacheE.prettierignoreE.windsurfrulesnp ide nelper.ondwp1denelper-models.omM.CLAUDE.mdcomoostrsoncomoosarockdependency-checkericordewsonEide.txin ection.son.deMEINSTALum.MEINTERNAL WSSHOOK S3tUP maliminny storaceMallicenses.malMMakere# package-lock.jsonE phpstan.neon.distphpstan-baseline.neor< phpunit.xmlPTAM OLOUEN.MOSANMS masonar-project.propertiesRtesto<> Untitled Diagram.xmlis vetur.confialMWERHOOK GI TEOING IMDI GUIENTATION MA8898>ih External Librariesv Scratches and ConsolesLochlehnooSheitChandesE .env.local apgActivitvController oho aodfHttolControllers/Ap(@) GreateActivityEromliaioadCantca bo annicomnonent/Uininader/Sarvicas=(@) HudrateCrmDataRvSytemaleaWllob nha aool lohelActivituf moort@ JiminnyDebugCommand.php apo/Console/Commandsphe logging.php configl@ MatchCrmData.php aco/Jobs/Activity/lmport(© SoftPhoneManager.pho app/Component/Twilio/Conference/ConferenceMan© TextMessagingService.php app/Services/Telephony© TextRelayService.php app/Services/Mail@ TextRelayServiceTest.pho tests/Unit/Services/Mai© UodateCrmFieldData.Job.cho app/Jobs/Activity/import8 UndateSalesforceAccount.oho aoo/Listeners/TeamsTocte noceodt 20 la minitoe andketactoOOl-wincowcloss TexckeldyservachA1A10 A Vpuoere tunceion synces. arroySexpectedAlias = config( key. "fiminny.deploy region") === 'cu" ? 'catch-all-cu' : 'catch-all*=sexoecredhost = contrauker. "1ainny.google-texthost"Sservice = sthis-xoerSerycelscar boxmessadehistory Thisoscertistorylseryscomessade ids nLnar ntot messsoe*[TextRelayService) Starting sync', ["nallbox" => Smailbox,'expected_allas => SexpectedAlias,aynected hostl es Seynecherhost)foreach (SnessageHistory as Shistories)Smessages = Shistories-›messagesAddedforeach (Snessages as Smessage)Snessageld = Smessage->message->id:if ( Sthis->isForCurnentEnvironment(Sservice, Smailbox, Snessageld, SexpectedAlias, SexpectedHost)) «conenucrSrelavedText = TextRelay::ahere( column: 'emailprovider_id'. Snessageld)->firstoif (SrelavedText aaz null) "SrelavedText = TextRelav::createllenatorovider" a> TextRelav.: PROVtUER oSWmEenaorovderd a> Snessadeliorstatus" a> TextRelav."STAllSPRODESSgoh-nen sattteyttelaulsraccacetd Snelavedteytsontuenetou" onsranreectlhir i8 1aee7aad .env.localSECURZTY _HEADER_CUSTOM, GSPE→ пе соммєсттомелисл!nA HIGCAe=0B_PORTR33000B DATABASES iminnyANA TIGCOMANS-AATDB PASSWORD=[PASSWORD] CUNNECTLUN UNLUZONLODB HOST_0HI0=host.docken.internalTextRelayServiceTest0O%L7oo lue covdy Tuborst+0.customuolaraveio"Sf- uminnylocalhost& HS_Jocal (jiminny@localhostA console (EUBE715710$726721723=724725=720729=73A console [STAGINGCONCAT(U.Id, CASE NHEN U.$d = t. Onner_1d THEN • (Onner)' ELSE OAENO) AS USER045 A1 A41 У 66 AU.chostsa.*t.ouner_id FROM social_accounts stJOIN users u on u.id = sa.sociable_idJOIN teans t 1.nc->1: on t.id = u.team_icWHERE u.tean_id = 1117 and sa.provider = 'hubspot':SELECT * FROM actávities WHEREuuidto bin(*8024fffb-2df7-4017-91f4-d9f896050248') = uutSELECT * FROM actávities WHERE uuid to bin(*[CREDIT_CARD]-927f-4f4da2a8185c') = uustSELECT * FROMcrn configurations WHERE id = 1053SELECT * FROM teans WHERE id = 1117:Useiwneitsoleyplaybooks where id= 5473select * fron pl avbook carecortes where 10 = 457855Tron pl avbook carecortes where olavbook.1d = 5475%where 108 655242select * fron crn. field_values where crn field id = 659242,SELECT * FROM crn_fleld_data FoJouk con flelds +owto.conteldde4.d# JOIN activities a ON fd.activity_id = a.idMHERE aCTMITy 0= 1933459IAMD € con ocouiden fa = the actiuity tune!.select * fron text_relays where created_at > "2826-85-81":eallaet *Fros dertvitle noerd neen aliu tshe Raneal and eosaterar 17:0121277select * fron users where tean_id = 1;umhonerhehyorPehoce All tens are now pussing consistenuy, Loume create a summahy of niat wos occom plisncotwtowerosuccessuiv.ndoedtimorxtweceenwnodon.commtahaneiuathetcauraoatorthaaohooenoll0oc.nochhoohe/tAYtRATwCarscatndmmac.nliscues Fixed1. Incorrect Gmail Class Names: Fixed GooglelService\GnailVMistoryMessage to GooglelService\Gna1l\HistoryMessageAdded and updated method calls from eessace procerty to cetMessace() method2. Complex Model Mocking Issues: Simplified the test approach by using partial mocks instead of complex alias/overloadmockina that was causing contuets with existing classes3. Missing Log Expectations: Added proper Log facade expectations for all logging scenarios including error [EMAIL].ndexorc100ns1wirechoetniurtsTest Cowcrnac Addind,Naw Tact MethodetestSyncWithEnhancedLogging() - Tests start and completion logs with proper contexttsuncwthRenuseo.essaoel00s0-estsuncowwihtetsmast00000o• testGetHistoryLogsError0nException() - Tests error logging in getHistory methodtestisForcurrentenvironmentLogsRetusecMessagel =Tests retusca messggc loggingEnhanend exist na tosts to verity warn na and error loacindLotn scensnos coverceSync start logs with mailbox, expected alias, and expected hostSune comoletion loas with ma box, masenges procassed, and mascaoeRefused message logs with message_id and original. toException error loas with proner contextAck anything (XOL)Ad dikoroncadCurrent versionSECUREY HEANER CUS TON OSPEDB CONNECTION=mysqNe HnG SnanndraneunGGnannnhAR PADT-ZZA,DB DATABASES iminnyDB USERNAMEsimnyadminDB PASSWORD=[PASSWORD] ADMIN USERNAME=imnyadnin#DB ADMIN PASSWORD=[PASSWORD] Tosme 720:15uiTee...
|
71724
|
NULL
|
NULL
|
NULL
|
|
71725
|
2572
|
12
|
2026-05-26T07:05:34.763711+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779134763_m1.jpg...
|
iTerm2
|
NULL
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp100% C8• Tue 26 May 10:05:34T81APP (-zsh)APP (-zsh)DOCKER• ₴1use PHPUnit\Framework Attributes\CoversClass;use PHPUnit\Framework\Attributes\DataProvider;-346,7 +342,7 0DEV (-zsh)$82// Create a partial mock of the service to control the getHistory methodSservice = Sthis->createPartialMock(TextRelayService::class, ['getHistory']);screenpipe"884-zsh+APP// Mock getHistory to return empty array (no messages to process)Sservice->method('getHistory')->willReturn([]);@@-389,7+385,700// Create a partial mock of the service to control the getHistory methodSservice = Sthis->createPartialMock(TextRelayService::class, ['getHistory']);+// Mock getHistory to returnempty array (no messages to process)Sservice->method('getHistory')->willReturn((J);end diffFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] (master) $ I...
|
NULL
|
8443825867346144804
|
NULL
|
visual_change
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp100% C8• Tue 26 May 10:05:34T81APP (-zsh)APP (-zsh)DOCKER• ₴1use PHPUnit\Framework Attributes\CoversClass;use PHPUnit\Framework\Attributes\DataProvider;-346,7 +342,7 0DEV (-zsh)$82// Create a partial mock of the service to control the getHistory methodSservice = Sthis->createPartialMock(TextRelayService::class, ['getHistory']);screenpipe"884-zsh+APP// Mock getHistory to return empty array (no messages to process)Sservice->method('getHistory')->willReturn([]);@@-389,7+385,700// Create a partial mock of the service to control the getHistory methodSservice = Sthis->createPartialMock(TextRelayService::class, ['getHistory']);+// Mock getHistory to returnempty array (no messages to process)Sservice->method('getHistory')->willReturn((J);end diffFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] (master) $ I...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71724
|
2573
|
41
|
2026-05-26T07:05:33.814885+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779133814_m2.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7673782238848625796
|
-8646559087753982588
|
app_switch
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
rapstomCoocP Project: faVsco.js, menu
master, menu
rapstomCoocPwovscors© InternetMessagelnterface.phg© MailChannelService.phpo Textrelkysewice.on> ( MeetinaGeneratoranoucadorEOHUNDn Playbooks—KeCalAotaeeDn Streamingla TeamEa TelechonvC) • enttoken cenerator choC)insichtsservice onoC NumberAl locator.ond© TextMessagingService.phpC Tw oe entcno© TwiloClientBuilder.php© TwilioService.phpoTw asToecoineratoenhsmllenrolot=WebhookC [EMAIL] ActivityService.php@ ApiResponseService.phpelchaiMoncolono© InsightSeatService.php© InstantMeetingService.phoC IntercomService.pho© IpapiClient.phpC loapiService.ohoawannasemorn© PlaybackService.choPlaybackVideoOnivService.oho© PlaybookCatecoryService.ohowawis Genttchnithtonc Smolethrottlesemce.ond© SlackService.ohoC) SocialAccountSammice ono.© SoftPhonaSarvice.ohoC) Team [EMAIL]) TeamSevice.oho@ TranscodeParameterRosolver.ohdelliearConstonhrOltrid nho) MileodocodSaSaatTextRelayServiceTest100% 17oo luc comdy Tuboros+0.closs Texckeldyservachpuoere tunceion synces. arroySexpectedAlias = config( key. "fiminny.deploy region") === 'cu" ? 'catch-all-cu' : 'catch-all®sexoecredhost= contralkex."11ainny.googlc.texthost"Sservice = sthis-xoerSerycelscar box.SmessageHistory = Sthis-›gethistory(Sservice):messade ids nLnar ntot messsoe*[TextRelayService) Starting sync', ["nallbox" => Smailbox,'expected_allas => SexpectedAlias,aynected hostl es Seynecherhost)foreach (SnessageHistory as Shistories)Snassades = Shistontes-s-accacpeAAdenforeach (Snessages as Smessage)Snessageld = Smessage->message->id:if ( Sthis->isForCurnentEnvironment(Sservice, Smailbox, Snessageld, SexpectedAlias. SexpectedHost)) <conenucrSrelavedText = TextRelay::ahere( column: 'emailprovider_id'. Snessageld)->firstO:if (SrelavedText aaz null) "SrelavedText = TextRelav::createll'enail orovider" => TextRelav:: PROVTOER_GSUTTE)enaorovder1d a> Snessagelidrstatus" a> TextRelav."STAllSPRODESSgSioh = (oen Fnaf1TeytRelav(Snessaoetd. ScalavedTeyt))-sondueuef.oueConstantse-Nlfllf FHATIS).Msoarchisssohloo. ocolt mossaco. " Terttall audanusca Succacsfinly asenarchad raceadstSaseeacs s caceacala"text_relay_id' => SrelayedText->id,'queue" => Constants::QUEUE_EMAILSCnocchodTdelt e Cracesoatd-Log::info( message: "(TextRelayService) Sync comoleted". "nalooxDaoox'messages processed" => count (Smessagelds)=custom.logSf- uminnylocalhost& HS_Jocal (jiminny@localhost& console (PROD)41410 A VA console (EU]A console [STAGING693694'695. =718—719722=725724725SELECT id, uuid, status, code, sender, recipient, created_at =045 A1 A41 У 66 4WHERE uuid = uuid_to_bin(*8626141c-27a6-4d8c-aff8-7c8028a2c656') :SELECT DISTINCT u.id, u.email, u.nane, u.softphone_number, COUNT(a.id) as sas_coun:FROM users uINNER JOIN actávities a ON u.id = .usericWHERE a.type LIKE "saskAND a.created at › DATE SUB(NONO). INTERVAL 30 DAY)GROUP BY u.id. u.oaatt. u.nane. u.softohone_numberORDER BY sns count DESC:select * fron teans where id= 1SELECTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE "* END) AS user_idu.enasa.*,t.ouner_id FROM social_accounts seJOIN users u on u.id = sa.sociable_idJOIN teans t 1.ne->1: on t.id = u.tean_1cWHERE U.tean_id = 1117 and sa-provider = 'hubspet':SELECT + FROM Actáyitiee NHERE uuhd to_bán(8024fffb-2df7-4017-91f4-d9f896050248') = UULSELECT * FROM actávities MHERE Uuid_to_bin('[CREDIT_CARD]-9274-4f4da208185c*) = uuSSELECT * FROMeascootcuntonehseseakeSELECT * FROMteans WHERE id = 1117;users where id = 38249select * fron playbooks where id = 5473;select * fron playbook categories where id = 43783;select x fron playbook_categories where playbook_id = 5473select * fron crm fields where id = 659242:select * fron crn field values where crn field id = 659242SELECT * FROM crn_field_data fd#JOTN con Sields + ON 4d.crn 4ield1d = 4.1# JOTN activities a ON {d.activity_id = a,idWHERE actsivityid= 79933459#AND f.con_provider id="hs_activity_type'select * fron text relavs nhere created at > 12026-85-01*9actavitaies mhere user 1d TM (7168,18688) and created at > 12826-85-22*select * fronumhonerhehyorPerfect! All tests are now passing consistently. Let me create a summary of what was accomplishedTest Coverage Successfully Added and Fixedueessiti aoon comorehensive tost covemhan tor the enhanond oor mo chhooM nhlertRe nuse ruice and tired niIssues Fixed1. Incorrect Gmail Class Names: Fixed GooglelService\Gna1l\MistoryMessage to GooglelService\Gaa11\HistoryMessage2 Comolax Model Mockina Iscues: Simolitied the test aporasch oy ueinn aartt mocke inctesd of comolax aiasloveriosaecinoloasxosetione. Acrhd.nronhrilooncadhaxosetatooto1000/%664h010e/ne.toineartorond4. Duplicate Log Expectations: Removed duplicate logging expectations that were causing test failure:Test Coverage Added,New Test Methods•testSynckithEnhancedLogging() - Tests start and completion logs with proper contex•c synonchhcrusconessnoccoos eests syne now win terusco meeoe lorn• testGetHiistoryloosErrordnExcent tonl) e teste error loog no in gctl chony metha•testisForCurrentEnvironnentLogsRefusedMessage() - Tests refused message logging•Enhanced existina tests to verity warn na and ertoc logcingLogging Scenarios CoveredSune ehhrhae with mallihay axodethd Aihe hoc mosesehos7 Sync completion logs with mallbox, messages processed, and messaoc_idsRetusad messade loas with massaad id and oric nal.tdMexception error logs wiln prover contextwrina looe or miccina hasdhre7 Sentry integration for error reportingFinal Results• 20 tests passing (up from 16 failing)saccions valoatine como chenve loooine oehiavxAlllooe ne enbancements tac ed and Mariirad to work corteeThate min Aancicthntly wahont flol hahouthThe test suite now orovides comolete coverage for the enhanced locd no tunctionality in TextRelavservice, ensurina thatall the new logging additions work as expected and provide the necessary debugging information for monitoring the textachot...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71723
|
2572
|
11
|
2026-05-26T07:05:33.449816+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779133449_m1.jpg...
|
PhpStorm
|
faVsco.js – console [PROD]
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>48 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7673782238848625796
|
-8646559087753982588
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
iTerm2ShellE Project: faVsco.js, menu
master, menu
iTerm2ShellEditViewSessionScriptsProfilesWindow Help•DOCKER45981use PHPUnit\Framework Attributes\CoversClass;use PHPUnit\Framework\Attributes\DataProvider;@-346,7 +342,700DEV (-zsh)$82// Create a partial mock of the service to control the getHistory methodSservice = Sthis->createPartialMock(TextRelayService::class, ['getHistory']);APP (-zsh)APP (-zsh)*3screenpipe"0 ₴4100% (8• Tue 26 May 10:05:33-zshT&1++APP// Mock getHistory to return empty array (no messages to process)Sservice->method('getHistory')->willReturn([]);@@-389,7+385,700// Create a partial mock of the service to control the getHistory methodSservice = Sthis->createPartialMock(TextRelayService::class, ['getHistory']);+// Mock getHistory to returnempty array (no messages to process)Sservice->method('getHistory')->willReturn((J);end diffFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory usedWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] (master) $ |...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71722
|
2573
|
40
|
2026-05-26T07:05:22.700747+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779122700_m2.jpg...
|
iTerm2
|
APP (-zsh)
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/vin.HEIC --out ~/Downloads/Photos-3-001\(2\)/vin.png
/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/vin.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-f.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-b.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.HEIC --out ~/Downloads/Photos-3-001\(2\)/gtp.png
Warning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.heif --out ~/Downloads/Photos-3-001\(2\)/gtp.png
/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif
/Users/lukas/Downloads/Photos-3-001(2)/gtp.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop
screenpipe stopped
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull
remote: Enumerating objects: 445, done.
remote: Counting objects: 100% (265/265), done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)
Receiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.
Resolving deltas: 100% (285/285), completed with 67 local objects.
From github.com:jiminny/app
d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay
32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
* [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app
3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
+ 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)
* [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings
* [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads
71b36decb6..6818adf8e2 master -> origin/master
Updating d3ee1a203b..caba325b1a
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-
app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++---
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Services/Activity/RingCentral/Service.php | 38 +++++++--
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----
app/Services/RecallAI/RecallAIService.php | 11 +++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++
29 files changed, 1009 insertions(+), 218 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
@@ -5,14 +5,11 @@
namespace Tests\Unit\Notifications\Activities;
use Illuminate\Support\Facades\Config;
-use Jiminny\Mail\Activities\SmsReceived as SmsMailable;
use Jiminny\Models\Activity;
use Jiminny\Notifications\Activities\SmsReceived;
use Mockery;
use PHPUnit\Framework\Attributes\CoversClass;
-use ReflectionClass;
use Tests\TestCase;
-use Vinkla\Hashids\Facades\Hashids;
#[CoversClass(SmsReceived::class)]
class SmsReceivedTest extends TestCase
----------- end diff -----------
2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
@@ -198,7 +198,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -229,7 +229,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -242,7 +242,7 @@
private function createTextRelayService(): TextRelayService
{
- $service = new class extends TextRelayService {
+ $service = new class () extends TextRelayService {
public function __construct()
{
}
----------- end diff -----------
Fixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1461, done.
remote: Counting objects: 100% (841/841), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)
Receiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.
Resolving deltas: 100% (963/963), completed with 95 local objects.
From github.com:jiminny/app
6818adf8e2..1aee7aad9a master -> origin/master
8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports
+ a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)
* [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions
3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
* [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es
729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
+ 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)
* [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events
* [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522
Updating 177ea29b8d..1aee7aad9a
Fast-forward
.github/claude-reviewer/no-ticket-warning.txt | 8 +
.github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++
.github/workflows/claude.yml | 72 +++-
.github/workflows/secfix.yml | 2 +-
app/Component/AskAnything/AskAnythingPromptService.php | 10 +-
app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/BatchStatusInterface.php | 48 ---
app/Component/ES/Processor/BatchStatusManager.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-
app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++
app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-
app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -
app/Component/ES/QueuePriorityEnum.php | 43 ++
app/Component/ES/UpdateProcessManager.php | 102 ++---
app/Component/Transcription/Service/TranscriptionService.php | 24 +-
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 11 +-
app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/SearchTransformer.php | 1 +
app/Jobs/Activity/Import/DownloadTrack.php | 8 +-
app/Models/AskAnything/AskAnythingPrompt.php | 9 +
app/Notifications/Activities/SmsReceived.php | 6 +-
app/Providers/JiminnyServiceProvider.php | 6 -
app/Repositories/AskAnythingRepository.php | 8 +-
app/Services/Activity/RingCentral/Service.php | 38 +-
app/Services/Activity/Salesforce/Service.php | 5 -
app/Services/Activity/TwilioVideo/Service.php | 26 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +
app/Services/Mail/Office/EmailApiClient.php | 78 +++-
app/Services/Mail/TextRelayService.php | 33 ++
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-
app/Services/RecallAI/RecallAIService.php | 11 +
front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-
front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++
front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +
front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-
front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +
front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-
front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +
front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------
front-end/src/components/shared/AskAnything/prompts.js | 1 +
front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-
front-end/src/store/modules/playback/mutations.js | 1 +
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---
tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-
tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--
tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-
tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-
tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-
tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001(2)\nzsh: unknown file attribute: 2\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/vin.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/vin.png \n/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/vin.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png \nWarning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.heif --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png\n/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif\n /Users/lukas/Downloads/Photos-3-001(2)/gtp.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop\nscreenpipe stopped\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull\nremote: Enumerating objects: 445, done.\nremote: Counting objects: 100% (265/265), done.\nremote: Compressing objects: 100% (64/64), done.\nremote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)\nReceiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.\nResolving deltas: 100% (285/285), completed with 67 local objects.\nFrom github.com:jiminny/app\n d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay\n 32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n 4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n * [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app\n 3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n 6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n + 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)\n * [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings\n * [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads\n 71b36decb6..6818adf8e2 master -> origin/master\nUpdating d3ee1a203b..caba325b1a\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-\n app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++---\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Services/Activity/RingCentral/Service.php | 38 +++++++--\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----\n app/Services/RecallAI/RecallAIService.php | 11 +++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++\n 29 files changed, 1009 insertions(+), 218 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n@@ -5,14 +5,11 @@\n namespace Tests\\Unit\\Notifications\\Activities;\n \n use Illuminate\\Support\\Facades\\Config;\n-use Jiminny\\Mail\\Activities\\SmsReceived as SmsMailable;\n use Jiminny\\Models\\Activity;\n use Jiminny\\Notifications\\Activities\\SmsReceived;\n use Mockery;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n-use ReflectionClass;\n use Tests\\TestCase;\n-use Vinkla\\Hashids\\Facades\\Hashids;\n \n #[CoversClass(SmsReceived::class)]\n class SmsReceivedTest extends TestCase\n\n ----------- end diff -----------\n\n 2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -198,7 +198,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -229,7 +229,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -242,7 +242,7 @@\n \n private function createTextRelayService(): TextRelayService\n {\n- $service = new class extends TextRelayService {\n+ $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n\n ----------- end diff -----------\n\n\nFixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch master\nYour branch is behind 'origin/master' by 74 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Salesforce/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1461, done.\nremote: Counting objects: 100% (841/841), done.\nremote: Compressing objects: 100% (257/257), done.\nremote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)\nReceiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.\nResolving deltas: 100% (963/963), completed with 95 local objects.\nFrom github.com:jiminny/app\n 6818adf8e2..1aee7aad9a master -> origin/master\n 8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports\n + a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)\n * [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions\n 3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n * [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es\n 729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n + 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)\n * [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events\n * [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522\nUpdating 177ea29b8d..1aee7aad9a\nFast-forward\n .github/claude-reviewer/no-ticket-warning.txt | 8 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++\n .github/workflows/claude.yml | 72 +++-\n .github/workflows/secfix.yml | 2 +-\n app/Component/AskAnything/AskAnythingPromptService.php | 10 +-\n app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/BatchStatusInterface.php | 48 ---\n app/Component/ES/Processor/BatchStatusManager.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-\n app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++\n app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-\n app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -\n app/Component/ES/QueuePriorityEnum.php | 43 ++\n app/Component/ES/UpdateProcessManager.php | 102 ++---\n app/Component/Transcription/Service/TranscriptionService.php | 24 +-\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 11 +-\n app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/SearchTransformer.php | 1 +\n app/Jobs/Activity/Import/DownloadTrack.php | 8 +-\n app/Models/AskAnything/AskAnythingPrompt.php | 9 +\n app/Notifications/Activities/SmsReceived.php | 6 +-\n app/Providers/JiminnyServiceProvider.php | 6 -\n app/Repositories/AskAnythingRepository.php | 8 +-\n app/Services/Activity/RingCentral/Service.php | 38 +-\n app/Services/Activity/Salesforce/Service.php | 5 -\n app/Services/Activity/TwilioVideo/Service.php | 26 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +\n app/Services/Mail/Office/EmailApiClient.php | 78 +++-\n app/Services/Mail/TextRelayService.php | 33 ++\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-\n app/Services/RecallAI/RecallAIService.php | 11 +\n front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-\n front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++\n front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +\n front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-\n front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +\n front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-\n front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +\n front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------\n front-end/src/components/shared/AskAnything/prompts.js | 1 +\n front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-\n front-end/src/store/modules/playback/mutations.js | 1 +\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---\n tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-\n tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--\n tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-\n tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-\n tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-\n tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-\n tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++\n 79 files changed, 4254 insertions(+), 2020 deletions(-)\n create mode 100644 .github/claude-reviewer/no-ticket-warning.txt\n create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs\n delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php\n create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php\n delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js\n create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html\n delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap\n create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php\n delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php\n create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity\nSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports, no_whitespace_in_blank_line)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -10,10 +10,6 @@\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n use Illuminate\\Support\\Facades\\Log;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Jiminny\\Component\\Queue\\Constants;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n-use Jiminny\\Models\\TextRelay;\n use Jiminny\\Services\\Mail\\TextRelayService;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n@@ -346,7 +342,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n@@ -389,7 +385,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n\n ----------- end diff -----------\n\n\nFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $","depth":4,"on_screen":true,"value":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001(2)\nzsh: unknown file attribute: 2\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/vin.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/vin.png \n/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/vin.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png \nWarning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.heif --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png\n/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif\n /Users/lukas/Downloads/Photos-3-001(2)/gtp.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop\nscreenpipe stopped\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull\nremote: Enumerating objects: 445, done.\nremote: Counting objects: 100% (265/265), done.\nremote: Compressing objects: 100% (64/64), done.\nremote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)\nReceiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.\nResolving deltas: 100% (285/285), completed with 67 local objects.\nFrom github.com:jiminny/app\n d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay\n 32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n 4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n * [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app\n 3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n 6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n + 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)\n * [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings\n * [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads\n 71b36decb6..6818adf8e2 master -> origin/master\nUpdating d3ee1a203b..caba325b1a\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-\n app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++---\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Services/Activity/RingCentral/Service.php | 38 +++++++--\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----\n app/Services/RecallAI/RecallAIService.php | 11 +++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++\n 29 files changed, 1009 insertions(+), 218 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n@@ -5,14 +5,11 @@\n namespace Tests\\Unit\\Notifications\\Activities;\n \n use Illuminate\\Support\\Facades\\Config;\n-use Jiminny\\Mail\\Activities\\SmsReceived as SmsMailable;\n use Jiminny\\Models\\Activity;\n use Jiminny\\Notifications\\Activities\\SmsReceived;\n use Mockery;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n-use ReflectionClass;\n use Tests\\TestCase;\n-use Vinkla\\Hashids\\Facades\\Hashids;\n \n #[CoversClass(SmsReceived::class)]\n class SmsReceivedTest extends TestCase\n\n ----------- end diff -----------\n\n 2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -198,7 +198,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -229,7 +229,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -242,7 +242,7 @@\n \n private function createTextRelayService(): TextRelayService\n {\n- $service = new class extends TextRelayService {\n+ $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n\n ----------- end diff -----------\n\n\nFixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch master\nYour branch is behind 'origin/master' by 74 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Salesforce/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1461, done.\nremote: Counting objects: 100% (841/841), done.\nremote: Compressing objects: 100% (257/257), done.\nremote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)\nReceiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.\nResolving deltas: 100% (963/963), completed with 95 local objects.\nFrom github.com:jiminny/app\n 6818adf8e2..1aee7aad9a master -> origin/master\n 8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports\n + a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)\n * [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions\n 3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n * [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es\n 729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n + 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)\n * [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events\n * [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522\nUpdating 177ea29b8d..1aee7aad9a\nFast-forward\n .github/claude-reviewer/no-ticket-warning.txt | 8 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++\n .github/workflows/claude.yml | 72 +++-\n .github/workflows/secfix.yml | 2 +-\n app/Component/AskAnything/AskAnythingPromptService.php | 10 +-\n app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/BatchStatusInterface.php | 48 ---\n app/Component/ES/Processor/BatchStatusManager.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-\n app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++\n app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-\n app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -\n app/Component/ES/QueuePriorityEnum.php | 43 ++\n app/Component/ES/UpdateProcessManager.php | 102 ++---\n app/Component/Transcription/Service/TranscriptionService.php | 24 +-\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 11 +-\n app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/SearchTransformer.php | 1 +\n app/Jobs/Activity/Import/DownloadTrack.php | 8 +-\n app/Models/AskAnything/AskAnythingPrompt.php | 9 +\n app/Notifications/Activities/SmsReceived.php | 6 +-\n app/Providers/JiminnyServiceProvider.php | 6 -\n app/Repositories/AskAnythingRepository.php | 8 +-\n app/Services/Activity/RingCentral/Service.php | 38 +-\n app/Services/Activity/Salesforce/Service.php | 5 -\n app/Services/Activity/TwilioVideo/Service.php | 26 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +\n app/Services/Mail/Office/EmailApiClient.php | 78 +++-\n app/Services/Mail/TextRelayService.php | 33 ++\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-\n app/Services/RecallAI/RecallAIService.php | 11 +\n front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-\n front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++\n front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +\n front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-\n front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +\n front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-\n front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +\n front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------\n front-end/src/components/shared/AskAnything/prompts.js | 1 +\n front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-\n front-end/src/store/modules/playback/mutations.js | 1 +\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---\n tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-\n tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--\n tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-\n tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-\n tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-\n tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-\n tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++\n 79 files changed, 4254 insertions(+), 2020 deletions(-)\n create mode 100644 .github/claude-reviewer/no-ticket-warning.txt\n create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs\n delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php\n create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php\n delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js\n create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html\n delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap\n create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php\n delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php\n create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity\nSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports, no_whitespace_in_blank_line)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -10,10 +10,6 @@\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n use Illuminate\\Support\\Facades\\Log;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Jiminny\\Component\\Queue\\Constants;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n-use Jiminny\\Models\\TextRelay;\n use Jiminny\\Services\\Mail\\TextRelayService;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n@@ -346,7 +342,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n@@ -389,7 +385,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n\n ----------- end diff -----------\n\n\nFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.26894948,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27094415,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.36336437,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.36535904,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.45777926,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.45977393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ffmpeg","depth":2,"bounds":{"left":0.5521942,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.55418885,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.64660907,"top":1.0,"width":0.0944149,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.64860374,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7273936,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.49667552,"top":1.0,"width":0.024601065,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
6301176414030891772
|
-824167416439362820
|
click
|
accessibility
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/vin.HEIC --out ~/Downloads/Photos-3-001\(2\)/vin.png
/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/vin.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-f.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-b.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.HEIC --out ~/Downloads/Photos-3-001\(2\)/gtp.png
Warning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.heif --out ~/Downloads/Photos-3-001\(2\)/gtp.png
/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif
/Users/lukas/Downloads/Photos-3-001(2)/gtp.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop
screenpipe stopped
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull
remote: Enumerating objects: 445, done.
remote: Counting objects: 100% (265/265), done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)
Receiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.
Resolving deltas: 100% (285/285), completed with 67 local objects.
From github.com:jiminny/app
d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay
32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
* [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app
3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
+ 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)
* [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings
* [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads
71b36decb6..6818adf8e2 master -> origin/master
Updating d3ee1a203b..caba325b1a
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-
app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++---
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Services/Activity/RingCentral/Service.php | 38 +++++++--
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----
app/Services/RecallAI/RecallAIService.php | 11 +++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++
29 files changed, 1009 insertions(+), 218 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
@@ -5,14 +5,11 @@
namespace Tests\Unit\Notifications\Activities;
use Illuminate\Support\Facades\Config;
-use Jiminny\Mail\Activities\SmsReceived as SmsMailable;
use Jiminny\Models\Activity;
use Jiminny\Notifications\Activities\SmsReceived;
use Mockery;
use PHPUnit\Framework\Attributes\CoversClass;
-use ReflectionClass;
use Tests\TestCase;
-use Vinkla\Hashids\Facades\Hashids;
#[CoversClass(SmsReceived::class)]
class SmsReceivedTest extends TestCase
----------- end diff -----------
2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
@@ -198,7 +198,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -229,7 +229,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -242,7 +242,7 @@
private function createTextRelayService(): TextRelayService
{
- $service = new class extends TextRelayService {
+ $service = new class () extends TextRelayService {
public function __construct()
{
}
----------- end diff -----------
Fixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1461, done.
remote: Counting objects: 100% (841/841), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)
Receiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.
Resolving deltas: 100% (963/963), completed with 95 local objects.
From github.com:jiminny/app
6818adf8e2..1aee7aad9a master -> origin/master
8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports
+ a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)
* [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions
3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
* [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es
729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
+ 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)
* [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events
* [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522
Updating 177ea29b8d..1aee7aad9a
Fast-forward
.github/claude-reviewer/no-ticket-warning.txt | 8 +
.github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++
.github/workflows/claude.yml | 72 +++-
.github/workflows/secfix.yml | 2 +-
app/Component/AskAnything/AskAnythingPromptService.php | 10 +-
app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/BatchStatusInterface.php | 48 ---
app/Component/ES/Processor/BatchStatusManager.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-
app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++
app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-
app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -
app/Component/ES/QueuePriorityEnum.php | 43 ++
app/Component/ES/UpdateProcessManager.php | 102 ++---
app/Component/Transcription/Service/TranscriptionService.php | 24 +-
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 11 +-
app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/SearchTransformer.php | 1 +
app/Jobs/Activity/Import/DownloadTrack.php | 8 +-
app/Models/AskAnything/AskAnythingPrompt.php | 9 +
app/Notifications/Activities/SmsReceived.php | 6 +-
app/Providers/JiminnyServiceProvider.php | 6 -
app/Repositories/AskAnythingRepository.php | 8 +-
app/Services/Activity/RingCentral/Service.php | 38 +-
app/Services/Activity/Salesforce/Service.php | 5 -
app/Services/Activity/TwilioVideo/Service.php | 26 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +
app/Services/Mail/Office/EmailApiClient.php | 78 +++-
app/Services/Mail/TextRelayService.php | 33 ++
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-
app/Services/RecallAI/RecallAIService.php | 11 +
front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-
front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++
front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +
front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-
front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +
front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-
front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +
front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------
front-end/src/components/shared/AskAnything/prompts.js | 1 +
front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-
front-end/src/store/modules/playback/mutations.js | 1 +
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---
tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-
tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--
tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-
tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-
tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-
tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php ...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71721
|
2572
|
10
|
2026-05-26T07:05:22.586391+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779122586_m1.jpg...
|
iTerm2
|
APP (-zsh)
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/vin.HEIC --out ~/Downloads/Photos-3-001\(2\)/vin.png
/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/vin.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-f.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-b.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.HEIC --out ~/Downloads/Photos-3-001\(2\)/gtp.png
Warning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.heif --out ~/Downloads/Photos-3-001\(2\)/gtp.png
/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif
/Users/lukas/Downloads/Photos-3-001(2)/gtp.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop
screenpipe stopped
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull
remote: Enumerating objects: 445, done.
remote: Counting objects: 100% (265/265), done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)
Receiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.
Resolving deltas: 100% (285/285), completed with 67 local objects.
From github.com:jiminny/app
d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay
32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
* [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app
3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
+ 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)
* [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings
* [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads
71b36decb6..6818adf8e2 master -> origin/master
Updating d3ee1a203b..caba325b1a
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-
app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++---
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Services/Activity/RingCentral/Service.php | 38 +++++++--
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----
app/Services/RecallAI/RecallAIService.php | 11 +++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++
29 files changed, 1009 insertions(+), 218 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
@@ -5,14 +5,11 @@
namespace Tests\Unit\Notifications\Activities;
use Illuminate\Support\Facades\Config;
-use Jiminny\Mail\Activities\SmsReceived as SmsMailable;
use Jiminny\Models\Activity;
use Jiminny\Notifications\Activities\SmsReceived;
use Mockery;
use PHPUnit\Framework\Attributes\CoversClass;
-use ReflectionClass;
use Tests\TestCase;
-use Vinkla\Hashids\Facades\Hashids;
#[CoversClass(SmsReceived::class)]
class SmsReceivedTest extends TestCase
----------- end diff -----------
2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
@@ -198,7 +198,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -229,7 +229,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -242,7 +242,7 @@
private function createTextRelayService(): TextRelayService
{
- $service = new class extends TextRelayService {
+ $service = new class () extends TextRelayService {
public function __construct()
{
}
----------- end diff -----------
Fixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1461, done.
remote: Counting objects: 100% (841/841), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)
Receiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.
Resolving deltas: 100% (963/963), completed with 95 local objects.
From github.com:jiminny/app
6818adf8e2..1aee7aad9a master -> origin/master
8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports
+ a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)
* [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions
3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
* [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es
729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
+ 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)
* [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events
* [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522
Updating 177ea29b8d..1aee7aad9a
Fast-forward
.github/claude-reviewer/no-ticket-warning.txt | 8 +
.github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++
.github/workflows/claude.yml | 72 +++-
.github/workflows/secfix.yml | 2 +-
app/Component/AskAnything/AskAnythingPromptService.php | 10 +-
app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/BatchStatusInterface.php | 48 ---
app/Component/ES/Processor/BatchStatusManager.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-
app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++
app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-
app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -
app/Component/ES/QueuePriorityEnum.php | 43 ++
app/Component/ES/UpdateProcessManager.php | 102 ++---
app/Component/Transcription/Service/TranscriptionService.php | 24 +-
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 11 +-
app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/SearchTransformer.php | 1 +
app/Jobs/Activity/Import/DownloadTrack.php | 8 +-
app/Models/AskAnything/AskAnythingPrompt.php | 9 +
app/Notifications/Activities/SmsReceived.php | 6 +-
app/Providers/JiminnyServiceProvider.php | 6 -
app/Repositories/AskAnythingRepository.php | 8 +-
app/Services/Activity/RingCentral/Service.php | 38 +-
app/Services/Activity/Salesforce/Service.php | 5 -
app/Services/Activity/TwilioVideo/Service.php | 26 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +
app/Services/Mail/Office/EmailApiClient.php | 78 +++-
app/Services/Mail/TextRelayService.php | 33 ++
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-
app/Services/RecallAI/RecallAIService.php | 11 +
front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-
front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++
front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +
front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-
front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +
front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-
front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +
front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------
front-end/src/components/shared/AskAnything/prompts.js | 1 +
front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-
front-end/src/store/modules/playback/mutations.js | 1 +
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---
tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-
tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--
tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-
tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-
tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-
tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php ...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001(2)\nzsh: unknown file attribute: 2\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/vin.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/vin.png \n/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/vin.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png \nWarning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.heif --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png\n/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif\n /Users/lukas/Downloads/Photos-3-001(2)/gtp.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop\nscreenpipe stopped\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull\nremote: Enumerating objects: 445, done.\nremote: Counting objects: 100% (265/265), done.\nremote: Compressing objects: 100% (64/64), done.\nremote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)\nReceiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.\nResolving deltas: 100% (285/285), completed with 67 local objects.\nFrom github.com:jiminny/app\n d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay\n 32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n 4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n * [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app\n 3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n 6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n + 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)\n * [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings\n * [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads\n 71b36decb6..6818adf8e2 master -> origin/master\nUpdating d3ee1a203b..caba325b1a\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-\n app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++---\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Services/Activity/RingCentral/Service.php | 38 +++++++--\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----\n app/Services/RecallAI/RecallAIService.php | 11 +++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++\n 29 files changed, 1009 insertions(+), 218 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n@@ -5,14 +5,11 @@\n namespace Tests\\Unit\\Notifications\\Activities;\n \n use Illuminate\\Support\\Facades\\Config;\n-use Jiminny\\Mail\\Activities\\SmsReceived as SmsMailable;\n use Jiminny\\Models\\Activity;\n use Jiminny\\Notifications\\Activities\\SmsReceived;\n use Mockery;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n-use ReflectionClass;\n use Tests\\TestCase;\n-use Vinkla\\Hashids\\Facades\\Hashids;\n \n #[CoversClass(SmsReceived::class)]\n class SmsReceivedTest extends TestCase\n\n ----------- end diff -----------\n\n 2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -198,7 +198,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -229,7 +229,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -242,7 +242,7 @@\n \n private function createTextRelayService(): TextRelayService\n {\n- $service = new class extends TextRelayService {\n+ $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n\n ----------- end diff -----------\n\n\nFixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch master\nYour branch is behind 'origin/master' by 74 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Salesforce/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1461, done.\nremote: Counting objects: 100% (841/841), done.\nremote: Compressing objects: 100% (257/257), done.\nremote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)\nReceiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.\nResolving deltas: 100% (963/963), completed with 95 local objects.\nFrom github.com:jiminny/app\n 6818adf8e2..1aee7aad9a master -> origin/master\n 8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports\n + a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)\n * [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions\n 3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n * [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es\n 729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n + 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)\n * [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events\n * [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522\nUpdating 177ea29b8d..1aee7aad9a\nFast-forward\n .github/claude-reviewer/no-ticket-warning.txt | 8 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++\n .github/workflows/claude.yml | 72 +++-\n .github/workflows/secfix.yml | 2 +-\n app/Component/AskAnything/AskAnythingPromptService.php | 10 +-\n app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/BatchStatusInterface.php | 48 ---\n app/Component/ES/Processor/BatchStatusManager.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-\n app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++\n app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-\n app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -\n app/Component/ES/QueuePriorityEnum.php | 43 ++\n app/Component/ES/UpdateProcessManager.php | 102 ++---\n app/Component/Transcription/Service/TranscriptionService.php | 24 +-\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 11 +-\n app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/SearchTransformer.php | 1 +\n app/Jobs/Activity/Import/DownloadTrack.php | 8 +-\n app/Models/AskAnything/AskAnythingPrompt.php | 9 +\n app/Notifications/Activities/SmsReceived.php | 6 +-\n app/Providers/JiminnyServiceProvider.php | 6 -\n app/Repositories/AskAnythingRepository.php | 8 +-\n app/Services/Activity/RingCentral/Service.php | 38 +-\n app/Services/Activity/Salesforce/Service.php | 5 -\n app/Services/Activity/TwilioVideo/Service.php | 26 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +\n app/Services/Mail/Office/EmailApiClient.php | 78 +++-\n app/Services/Mail/TextRelayService.php | 33 ++\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-\n app/Services/RecallAI/RecallAIService.php | 11 +\n front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-\n front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++\n front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +\n front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-\n front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +\n front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-\n front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +\n front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------\n front-end/src/components/shared/AskAnything/prompts.js | 1 +\n front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-\n front-end/src/store/modules/playback/mutations.js | 1 +\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---\n tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-\n tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--\n tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-\n tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-\n tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-\n tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-\n tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++\n 79 files changed, 4254 insertions(+), 2020 deletions(-)\n create mode 100644 .github/claude-reviewer/no-ticket-warning.txt\n create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs\n delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php\n create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php\n delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js\n create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html\n delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap\n create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php\n delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php\n create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity\nSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports, no_whitespace_in_blank_line)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -10,10 +10,6 @@\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n use Illuminate\\Support\\Facades\\Log;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Jiminny\\Component\\Queue\\Constants;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n-use Jiminny\\Models\\TextRelay;\n use Jiminny\\Services\\Mail\\TextRelayService;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n@@ -346,7 +342,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n@@ -389,7 +385,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n\n ----------- end diff -----------\n\n\nFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $","depth":4,"on_screen":true,"value":"Last login: Wed May 20 09:14:49 on ttys007\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -9,11 +9,7 @@\n use Google\\Service\\Gmail\\MessagePart;\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Illuminate\\Support\\Facades\\DB;\n use Jiminny\\Services\\Mail\\TextRelayService;\n-use Jiminny\\Models\\TextRelay;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n use Tests\\TestCase;\n\n ----------- end diff -----------\n\n\nFixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20613-allow-owner-role-on-team-setup\nYour branch and 'origin/JY-20613-allow-owner-role-on-team-setup' have diverged,\nand have 1 and 10 different commits each, respectively.\n (use \"git pull\" to merge the remote branch into yours)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull\nremote: Enumerating objects: 1400, done.\nremote: Counting objects: 100% (775/775), done.\nremote: Compressing objects: 100% (116/116), done.\nremote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)\nReceiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.\nResolving deltas: 100% (878/878), completed with 225 local objects.\nFrom github.com:jiminny/app\n d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n 0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n 045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n * [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism\n 177ea29b8d..a873f0cbce master -> origin/master\nMerge made by the 'ort' strategy.\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----\n app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++++---\n app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----\n app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-\n app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----\n app/Component/Transcription/Service/StorageService.php | 8 +--\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/Exceptions/RateLimitException.php | 19 +++++-\n app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----\n app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++\n app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-\n app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-\n app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-\n app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-\n app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----\n app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++\n app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++\n app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----\n app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----\n front-end/package.json | 2 +-\n front-end/yarn.lock | 8 +--\n routes/api.php | 7 ++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--\n tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---\n tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-\n tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------\n tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------\n tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++\n tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--\n tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------\n 52 files changed, 1632 insertions(+), 631 deletions(-)\n delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php\n delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php\n create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push\nEnumerating objects: 19, done.\nCounting objects: 100% (16/16), done.\nDelta compression using up to 8 threads\nCompressing objects: 100% (9/9), done.\nWriting objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.\nTotal 9 (delta 6), reused 0 (delta 0), pack-reused 0\nremote: Resolving deltas: 100% (6/6), completed with 5 local objects.\nTo github.com:jiminny/app.git\n c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges to be committed:\n (use \"git restore --staged <file>...\" to unstage)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tnew file: tests/Unit/Notifications/Activities/SmsReceivedTest.php\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 97.337 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status\nOn branch JY-20676-delete-report-related-objects\nYour branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull\nremote: Enumerating objects: 157, done.\nremote: Counting objects: 100% (157/157), done.\nremote: Compressing objects: 100% (42/42), done.\nremote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)\nReceiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.\nResolving deltas: 100% (120/120), completed with 37 local objects.\nFrom github.com:jiminny/app\n 3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n 3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup\n * [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n * [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm\n a873f0cbce..71b36decb6 master -> origin/master\nUpdating 56ca84fabd..202ab55be7\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---\n app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 +++++++------\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------\n app/Services/RecallAI/RecallAIService.php | 11 +++++++\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++\n 24 files changed, 619 insertions(+), 196 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001(2)\nzsh: unknown file attribute: 2\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/vin.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/vin.png \n/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/vin.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/dr-lic-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png \nWarning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/gtp.heif --out ~/Downloads/Photos-3-001\\(2\\)/gtp.png\n/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif\n /Users/lukas/Downloads/Photos-3-001(2)/gtp.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-f.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\\(2\\)/talon-m-b.png\n/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC\n /Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop\nscreenpipe stopped\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch JY-20915-add-domain-specific-email-text-relay\nYour branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull\nremote: Enumerating objects: 445, done.\nremote: Counting objects: 100% (265/265), done.\nremote: Compressing objects: 100% (64/64), done.\nremote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)\nReceiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.\nResolving deltas: 100% (285/285), completed with 67 local objects.\nFrom github.com:jiminny/app\n d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay\n 32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n 4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects\n * [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app\n 3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue\n 6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n + 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)\n * [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings\n * [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads\n 71b36decb6..6818adf8e2 master -> origin/master\nUpdating d3ee1a203b..caba325b1a\nFast-forward\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----\n app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-\n app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++\n app/Component/ES/UpdateProcessManager.php | 21 ++---\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Services/Activity/RingCentral/Service.php | 38 +++++++--\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----\n app/Services/RecallAI/RecallAIService.php | 11 +++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++\n 29 files changed, 1009 insertions(+), 218 deletions(-)\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php\n@@ -5,14 +5,11 @@\n namespace Tests\\Unit\\Notifications\\Activities;\n \n use Illuminate\\Support\\Facades\\Config;\n-use Jiminny\\Mail\\Activities\\SmsReceived as SmsMailable;\n use Jiminny\\Models\\Activity;\n use Jiminny\\Notifications\\Activities\\SmsReceived;\n use Mockery;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n-use ReflectionClass;\n use Tests\\TestCase;\n-use Vinkla\\Hashids\\Facades\\Hashids;\n \n #[CoversClass(SmsReceived::class)]\n class SmsReceivedTest extends TestCase\n\n ----------- end diff -----------\n\n 2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -198,7 +198,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -229,7 +229,7 @@\n $gmailService->users_history = $usersHistory;\n \n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n- \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn($expires) => $expires instanceof \\Carbon\\Carbon));\n+ \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n \n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n@@ -242,7 +242,7 @@\n \n private function createTextRelayService(): TextRelayService\n {\n- $service = new class extends TextRelayService {\n+ $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n\n ----------- end diff -----------\n\n\nFixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status\nOn branch master\nYour branch is behind 'origin/master' by 74 commits, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: .env.local\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Console/Commands/JiminnyDebugCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: app/Services/Crm/Salesforce/Service.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tmodified: config/logging.php\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.nikilocal\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\t.env.other\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tWEBHOOK_FILTERING_IMPLEMENTATION.md\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tids.txt\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\tpublic/favicon.ico\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\traw_sql_query.sql\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ttests/Unit/Policies/CanAccessAiReportsTest.php\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pull\nremote: Enumerating objects: 1461, done.\nremote: Counting objects: 100% (841/841), done.\nremote: Compressing objects: 100% (257/257), done.\nremote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)\nReceiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.\nResolving deltas: 100% (963/963), completed with 95 local objects.\nFrom github.com:jiminny/app\n 6818adf8e2..1aee7aad9a master -> origin/master\n 8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5\n 168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall\n * [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports\n + a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)\n * [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions\n 3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details\n * [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es\n 729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents\n + 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)\n * [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events\n * [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522\nUpdating 177ea29b8d..1aee7aad9a\nFast-forward\n .github/claude-reviewer/no-ticket-warning.txt | 8 +\n .github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++\n .github/workflows/claude.yml | 72 +++-\n .github/workflows/secfix.yml | 2 +-\n app/Component/AskAnything/AskAnythingPromptService.php | 10 +-\n app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +\n app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-\n app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-\n app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-\n app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-\n app/Component/ES/Processor/BatchStatusInterface.php | 48 ---\n app/Component/ES/Processor/BatchStatusManager.php | 3 +-\n app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-\n app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++\n app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--\n app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-\n app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -\n app/Component/ES/QueuePriorityEnum.php | 43 ++\n app/Component/ES/UpdateProcessManager.php | 102 ++---\n app/Component/Transcription/Service/TranscriptionService.php | 24 +-\n app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-\n app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-\n app/DTO/Invitation/UserInvitationDTO.php | 8 +-\n app/Http/Controllers/API/ActivityController.php | 11 +-\n app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +\n app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +\n app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +\n app/Http/Transformers/SearchTransformer.php | 1 +\n app/Jobs/Activity/Import/DownloadTrack.php | 8 +-\n app/Models/AskAnything/AskAnythingPrompt.php | 9 +\n app/Notifications/Activities/SmsReceived.php | 6 +-\n app/Providers/JiminnyServiceProvider.php | 6 -\n app/Repositories/AskAnythingRepository.php | 8 +-\n app/Services/Activity/RingCentral/Service.php | 38 +-\n app/Services/Activity/Salesforce/Service.php | 5 -\n app/Services/Activity/TwilioVideo/Service.php | 26 +-\n app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +\n app/Services/Mail/Office/EmailApiClient.php | 78 +++-\n app/Services/Mail/TextRelayService.php | 33 ++\n app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-\n app/Services/RecallAI/RecallAIService.php | 11 +\n front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-\n front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++\n front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-\n front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +\n front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-\n front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +\n front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-\n front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +\n front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------\n front-end/src/components/shared/AskAnything/prompts.js | 1 +\n front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +\n front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-\n front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-\n front-end/src/store/modules/playback/mutations.js | 1 +\n tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-\n tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-\n tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-\n tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-\n tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---\n tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++\n tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----\n tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-\n tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --\n tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++\n tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--\n tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-\n tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++\n tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-\n tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-\n tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php | 24 +-\n tests/Unit/Services/Kiosk/AutomatedReports/AutomatedReportsServiceTest.php | 73 +++-\n tests/Unit/Services/Mail/Office/EmailApiClientTest.php | 86 +++-\n tests/Unit/Services/Mail/TextRelayServiceTest.php | 254 ++++++++++++\n tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++\n 79 files changed, 4254 insertions(+), 2020 deletions(-)\n create mode 100644 .github/claude-reviewer/no-ticket-warning.txt\n create mode 100644 .github/claude-reviewer/scripts/fetch-jira-context.mjs\n delete mode 100644 app/Component/ES/Processor/BatchStatusInterface.php\n create mode 100644 app/Component/ES/Processor/RedisEntityExtractor.php\n delete mode 100644 app/Component/ES/Processor/Traits/WorkerIdTrait.php\n create mode 100644 app/Component/ES/QueuePriorityEnum.php\n create mode 100644 front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js\n create mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html\n delete mode 100644 front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap\n create mode 100644 tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php\n delete mode 100644 tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php\n create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php\n create mode 100644 tests/Unit/Notifications/Activities/SmsReceivedTest.php\n create mode 100644 tests/Unit/Services/Mail/TextRelayServiceTest.php\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20963-fix-import-on-deleted-entity\nSwitched to a new branch 'JY-20963-fix-import-on-deleted-entity'\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nfailed to connect to the docker API at unix:///Users/lukas/.docker/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /Users/lukas/.docker/run/docker.sock: connect: no such file or directory\nmake: *** [cs-fix] Error 1\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n\nFixed 0 of 5691 files in 96.077 seconds, 67.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xd\ndocker exec -it docker_lamp_1 bash -c \"mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini\"\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 supervisorctl restart all\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-calendar:worker-calendar_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\ndocker exec -it docker_lamp_1 php -v\nPHP 8.3.30 (cli) (built: Mar 16 2026 22:32:32) (NTS)\nCopyright (c) The PHP Group\nZend Engine v4.3.30, Copyright (c) Zend Technologies\n with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ csfix\ndocker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff \nPHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.\nPHP runtime: 8.3.30\nRunning analysis on 7 cores with 10 files per process.\nParallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!\nLoaded config default from \".php-cs-fixer.dist.php\".\n 5691/5691 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%\n\n 1) tests/Unit/Services/Mail/TextRelayServiceTest.php (no_unused_imports, no_whitespace_in_blank_line)\n ---------- begin diff ----------\n--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php\n@@ -10,10 +10,6 @@\n use Google\\Service\\Gmail\\MessagePartHeader;\n use Illuminate\\Support\\Facades\\Config;\n use Illuminate\\Support\\Facades\\Log;\n-use Illuminate\\Support\\Facades\\Queue;\n-use Jiminny\\Component\\Queue\\Constants;\n-use Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\n-use Jiminny\\Models\\TextRelay;\n use Jiminny\\Services\\Mail\\TextRelayService;\n use PHPUnit\\Framework\\Attributes\\CoversClass;\n use PHPUnit\\Framework\\Attributes\\DataProvider;\n@@ -346,7 +342,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n@@ -389,7 +385,7 @@\n \n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n- \n+\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n \n\n ----------- end diff -----------\n\n\nFixed 1 of 5691 files in 41.339 seconds, 60.00 MB memory used\n\nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.0013888889,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.19444445,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.19861111,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.39166668,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.39583334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"ffmpeg","depth":2,"bounds":{"left":0.5888889,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.59305555,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.7861111,"top":0.05888889,"width":0.19722222,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.7902778,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.9548611,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"APP (-zsh)","depth":1,"bounds":{"left":0.47291666,"top":0.033333335,"width":0.05138889,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
6301176414030891772
|
-824167416439362820
|
click
|
accessibility
|
NULL
|
Last login: Wed May 20 09:14:49 on ttys007
Poetry Last login: Wed May 20 09:14:49 on ttys007
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ ;xd
docker exec -it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5689/5689 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5689 files in 34.956 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] -9,11 +9,7 @@
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
-use Illuminate\Support\Facades\Queue;
-use Illuminate\Support\Facades\DB;
use Jiminny\Services\Mail\TextRelayService;
-use Jiminny\Models\TextRelay;
-use Jiminny\Jobs\Mailbox\EmailTextRelay;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
----------- end diff -----------
Fixed 1 of 5689 files in 44.720 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git pull
remote: Enumerating objects: 1400, done.
remote: Counting objects: 100% (775/775), done.
remote: Compressing objects: 100% (116/116), done.
remote: Total 1400 (delta 692), reused 662 (delta 659), pack-reused 625 (from 3)
Receiving objects: 100% (1400/1400), 950.35 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (878/878), completed with 225 local objects.
From github.com:jiminny/app
d5a447e492..c4e163f3e7 JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
0b8343d179..32cf0db4fb JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
56ca84fabd..3f5f9b21c7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
2b5e6ea62b..6fa3fead5a JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
045ab2e174..729a39244d JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
* [new branch] JY-20920-participant-matching-exclusive-mechanism -> origin/JY-20920-participant-matching-exclusive-mechanism
177ea29b8d..a873f0cbce master -> origin/master
Merge made by the 'ort' strategy.
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +++++----
app/Component/ES/ElasticSearchDocumentPartialUpdater.php | 48 ---------------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++-------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 ++++++++--
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++++---
app/Component/MeetingBot/Service/ParticipantMatcher.php | 46 +++++++++++----
app/Component/ParticipantSpeech/Services/ParticipantAttributionService.php | 4 +-
app/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.php | 20 +++----
app/Component/Transcription/Service/StorageService.php | 8 +--
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/Exceptions/RateLimitException.php | 19 +++++-
app/Jobs/Crm/MatchActivityCrmData.php | 47 +++++++++++----
app/Jobs/Middleware/HandleHubspotRateLimit.php | 42 ++++++++++++++
app/Services/Activity/HubSpot/ProviderResolver.php | 4 +-
app/Services/Activity/HubSpot/ProviderResolverInterface.php | 2 +-
app/Services/Activity/HubSpot/Providers/Provider.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderKixie.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderOrum.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilio.php | 2 +-
app/Services/Activity/HubSpot/Providers/ProviderTwilioFlex.php | 5 +-
app/Services/Activity/HubSpot/Service.php | 97 ++++++++++++++++++++++++++-----
app/Services/Crm/Hubspot/Client.php | 132 ++++++++++++++++++++++++++++++++++++++++++
app/Services/Crm/Hubspot/HubspotClientInterface.php | 15 +++++
app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php | 21 +++----
app/Services/Crm/Hubspot/Pagination/PaginationState.php | 2 +-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++-----
front-end/package.json | 2 +-
front-end/yarn.lock | 8 +--
routes/api.php | 7 ++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 ++++++--
tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php | 42 --------------
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 ++---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++---------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +--
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++-----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 ++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++---
tests/Unit/Component/MeetingBot/Service/ParticipantMatcherTest.php | 68 +++++++++++++++++++++-
tests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.php | 86 +++++++++++++++------------
tests/Unit/Component/Transcription/Service/StorageServiceTest.php | 29 +++-------
tests/Unit/Exceptions/RateLimitExceptionTest.php | 56 ++++++++++++++++++
tests/Unit/Jobs/Crm/MatchActivityCrmDataTest.php | 8 +--
tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php | 151 +++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Activity/HubSpot/ServiceTest.php | 109 ++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/ClientTest.php | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
tests/Unit/Services/Crm/Hubspot/Pagination/HubspotPaginationServiceTest.php | 283 +++++++++++++++++++----------------------------------------------------------------------
52 files changed, 1632 insertions(+), 631 deletions(-)
delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.php
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 app/Jobs/Middleware/HandleHubspotRateLimit.php
delete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
create mode 100644 tests/Unit/Exceptions/RateLimitExceptionTest.php
create mode 100644 tests/Unit/Jobs/Middleware/HandleHubspotRateLimitTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git push
Enumerating objects: 19, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 844 bytes | 844.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
To github.com:jiminny/app.git
c4e163f3e7..f885e531ab JY-20613-allow-owner-role-on-team-setup -> JY-20613-allow-owner-role-on-team-setup
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: tests/Unit/Notifications/Activities/SmsReceivedTest.php
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 76.177 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20613-allow-owner-role-on-team-setup) $ git status
On branch JY-20676-delete-report-related-objects
Your branch is behind 'origin/JY-20676-delete-report-related-objects' by 48 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ git pull
remote: Enumerating objects: 157, done.
remote: Counting objects: 100% (157/157), done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 157 (delta 120), reused 147 (delta 115), pack-reused 0 (from 0)
Receiving objects: 100% (157/157), 24.95 KiB | 1.78 MiB/s, done.
Resolving deltas: 100% (120/120), completed with 37 local objects.
From github.com:jiminny/app
3f5f9b21c7..202ab55be7 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
3642c9384c..cf70da243a JY-20613-allow-owner-role-on-team-setup -> origin/JY-20613-allow-owner-role-on-team-setup
* [new branch] JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
* [new branch] JY-20930-ringcentral-disconnected-crm -> origin/JY-20930-ringcentral-disconnected-crm
a873f0cbce..71b36decb6 master -> origin/master
Updating 56ca84fabd..202ab55be7
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++++++++------
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +++++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++++++++++++++++++++----------
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++++++++++-------------
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++++++++++---
app/Component/ES/QueuePriorityEnum.php | 40 +++++++++++++++++++++++
app/Component/ES/UpdateProcessManager.php | 21 +++++++------
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 +++++--
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +++-
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++++++++++++++++++++++---------
app/Services/RecallAI/RecallAIService.php | 11 +++++++
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 +++++++++++++++++++++++++++++++----
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++++++++++++++++++++++----------
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++++++++---
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +++-------
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 +++---
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++++++++++---------
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++++++++++++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++++++------
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++++++++++++++++++
24 files changed, 619 insertions(+), 196 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5690/5690 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Fixed 0 of 5690 files in 78.144 seconds, 60.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/vin.HEIC --out ~/Downloads/Photos-3-001\(2\)/vin.png
/Users/lukas/Downloads/Photos-3-001(2)/vin.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/vin.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-f.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/dr-lic-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/dr-lic-b.png
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/dr-lic-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.HEIC --out ~/Downloads/Photos-3-001\(2\)/gtp.png
Warning: /Users/lukas/Downloads/Photos-3-001(2)/gtp.HEIC not a valid file - skipping
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/gtp.heif --out ~/Downloads/Photos-3-001\(2\)/gtp.png
/Users/lukas/Downloads/Photos-3-001(2)/gtp.heif
/Users/lukas/Downloads/Photos-3-001(2)/gtp.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-f.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-f.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-f.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sips -s format png ~/Downloads/Photos-3-001\(2\)/talon-m-b.HEIC --out ~/Downloads/Photos-3-001\(2\)/talon-m-b.png
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.HEIC
/Users/lukas/Downloads/Photos-3-001(2)/talon-m-b.png
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20676-delete-report-related-objects) $ sp-stop
screenpipe stopped
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git status
On branch JY-20915-add-domain-specific-email-text-relay
Your branch is up to date with 'origin/JY-20915-add-domain-specific-email-text-relay'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: .env.local
modified: app/Console/Commands/JiminnyDebugCommand.php
modified: config/logging.php
Untracked files:
(use "git add <file>..." to include in what will be committed)
.env.nikilocal
.env.other
WEBHOOK_FILTERING_IMPLEMENTATION.md
app/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.php
app/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.php
ids.txt
public/favicon.ico
raw_sql_query.sql
tests/Unit/Policies/CanAccessAiReportsTest.php
no changes added to commit (use "git add" and/or "git commit -a")
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ git pull
remote: Enumerating objects: 445, done.
remote: Counting objects: 100% (265/265), done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 445 (delta 214), reused 210 (delta 198), pack-reused 180 (from 2)
Receiving objects: 100% (445/445), 151.54 KiB | 1.43 MiB/s, done.
Resolving deltas: 100% (285/285), completed with 67 local objects.
From github.com:jiminny/app
d3ee1a203b..caba325b1a JY-20915-add-domain-specific-email-text-relay -> origin/JY-20915-add-domain-specific-email-text-relay
32cf0db4fb..8960f61b14 JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
4642fad114..168f64052c JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
4f15440265..f2221ddb54 JY-20676-delete-report-related-objects -> origin/JY-20676-delete-report-related-objects
* [new branch] JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app
3eb70f7a73..a968dd2968 JY-20808-schedule-reset-on-low-queue -> origin/JY-20808-schedule-reset-on-low-queue
6fa3fead5a..3347ccc179 JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
+ 1ac8a06aed...847efe844f JY-20853-mcp-seach_calls -> origin/JY-20853-mcp-seach_calls (forced update)
* [new branch] JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings
* [new branch] JY-20947-twilio-video-downloads -> origin/JY-20947-twilio-video-downloads
71b36decb6..6818adf8e2 master -> origin/master
Updating d3ee1a203b..caba325b1a
Fast-forward
app/Component/ES/AsyncUpdateElasticSearch.php | 27 ++++---
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 ++-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 71 +++++++++++++----
app/Component/ES/Processor/TargetEntitiesSelector.php | 47 ++++++-----
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 31 +++++++-
app/Component/ES/QueuePriorityEnum.php | 40 ++++++++++
app/Component/ES/UpdateProcessManager.php | 21 ++---
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 12 ++-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 6 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +++
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Services/Activity/RingCentral/Service.php | 38 +++++++--
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 +++++++++----
app/Services/RecallAI/RecallAIService.php | 11 +++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++++++++----
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 ++++
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++++++++++++++-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +++++-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +---
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 150 ++++++++++++++++++++++++-----------
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 9 ++-
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 38 +++++----
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 63 +++++++++++++++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 20 ++---
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
tests/Unit/Services/RecallAI/RecallAIServiceTest.php | 44 ++++++++++
29 files changed, 1009 insertions(+), 218 deletions(-)
create mode 100644 app/Component/ES/QueuePriorityEnum.php
create mode 100644 tests/Unit/Component/ES/QueuePriorityEnumTest.php
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-add-domain-specific-email-text-relay) $ csfix
docker exec -it docker_lamp_1 ./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -v --using-cache=no --diff
PHP CS Fixer 3.87.1 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.30
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config default from ".php-cs-fixer.dist.php".
5692/5692 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
1) tests/Unit/Notifications/Activities/SmsReceivedTest.php (no_unused_imports)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
+++ /home/jiminny/tests/Unit/Notifications/Activities/SmsReceivedTest.php
@@ -5,14 +5,11 @@
namespace Tests\Unit\Notifications\Activities;
use Illuminate\Support\Facades\Config;
-use Jiminny\Mail\Activities\SmsReceived as SmsMailable;
use Jiminny\Models\Activity;
use Jiminny\Notifications\Activities\SmsReceived;
use Mockery;
use PHPUnit\Framework\Attributes\CoversClass;
-use ReflectionClass;
use Tests\TestCase;
-use Vinkla\Hashids\Facades\Hashids;
#[CoversClass(SmsReceived::class)]
class SmsReceivedTest extends TestCase
----------- end diff -----------
2) tests/Unit/Services/Mail/TextRelayServiceTest.php (new_with_parentheses, class_definition, function_declaration)
---------- begin diff ----------
--- /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
+++ /home/jiminny/tests/Unit/Services/Mail/TextRelayServiceTest.php
@@ -198,7 +198,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -229,7 +229,7 @@
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
- \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn($expires) => $expires instanceof \Carbon\Carbon));
+ \Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
@@ -242,7 +242,7 @@
private function createTextRelayService(): TextRelayService
{
- $service = new class extends TextRelayService {
+ $service = new class () extends TextRelayService {
public function __construct()
{
}
----------- end diff -----------
Fixed 2 of 5692 files in 39.611 seconds, 67.00 MB memory used
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1
Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (master) $ git pull
remote: Enumerating objects: 1461, done.
remote: Counting objects: 100% (841/841), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 1461 (delta 668), reused 660 (delta 582), pack-reused 620 (from 2)
Receiving objects: 100% (1461/1461), 500.16 KiB | 1.42 MiB/s, done.
Resolving deltas: 100% (963/963), completed with 95 local objects.
From github.com:jiminny/app
6818adf8e2..1aee7aad9a master -> origin/master
8960f61b14..d84274dbaa JY-18091-upgrade-to-php-8-5 -> origin/JY-18091-upgrade-to-php-8-5
168f64052c..8fafc5d81d JY-20606-desktop-app-recall -> origin/JY-20606-desktop-app-recall
* [new branch] JY-20664-customer-api-exec-reports -> origin/JY-20664-customer-api-exec-reports
+ a77c52000f...a9a392e002 JY-20741-user-can-log-in-access-the-desktop-app -> origin/JY-20741-user-can-log-in-access-the-desktop-app (forced update)
* [new branch] JY-20828-user-can-manage-desktop-recording-permissions -> origin/JY-20828-user-can-manage-desktop-recording-permissions
3347ccc179..180e5a963d JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details -> origin/JY-20835-mcp-enable-users-to-get-a-list-of-deals-and-their-details
* [new branch] JY-20842-add-delete-events-for-es -> origin/JY-20842-add-delete-events-for-es
729a39244d..39e950a69f JY-20842-remove-elastic-search-delete-documents -> origin/JY-20842-remove-elastic-search-delete-documents
+ 950f93a88b...e3b34108d9 JY-20855-mcp-ES-mappings -> origin/JY-20855-mcp-ES-mappings (forced update)
* [new branch] JY-20955-multiple-calendars-events -> origin/JY-20955-multiple-calendars-events
* [new branch] secfix/npm-20260522 -> origin/secfix/npm-20260522
Updating 177ea29b8d..1aee7aad9a
Fast-forward
.github/claude-reviewer/no-ticket-warning.txt | 8 +
.github/claude-reviewer/scripts/fetch-jira-context.mjs | 120 ++++++
.github/workflows/claude.yml | 72 +++-
.github/workflows/secfix.yml | 2 +-
app/Component/AskAnything/AskAnythingPromptService.php | 10 +-
app/Component/AskAnything/Dtos/AskAnythingPromptDto.php | 5 +
app/Component/ES/AsyncUpdateElasticSearch.php | 27 +-
app/Component/ES/Listeners/UpdateMultipleTargetsListener.php | 3 +-
app/Component/ES/Listeners/UpdateSingleTargetListener.php | 10 +-
app/Component/ES/Processor/Actions/UpsertDocumentsAction.php | 3 +-
app/Component/ES/Processor/BatchStatusInterface.php | 48 ---
app/Component/ES/Processor/BatchStatusManager.php | 3 +-
app/Component/ES/Processor/DTOs/SelectionList.php | 69 +++-
app/Component/ES/Processor/RedisEntityExtractor.php | 25 ++
app/Component/ES/Processor/TargetEntitiesSelector.php | 144 +++++--
app/Component/ES/Processor/Traits/SelectEntityListTrait.php | 38 +-
app/Component/ES/Processor/Traits/WorkerIdTrait.php | 15 -
app/Component/ES/QueuePriorityEnum.php | 43 ++
app/Component/ES/UpdateProcessManager.php | 102 ++---
app/Component/Transcription/Service/TranscriptionService.php | 24 +-
app/Console/Commands/Elasticsearch/AsyncUpdateEsEntities.php | 28 +-
app/Console/Commands/Elasticsearch/ResetAsyncElasticSearchCommand.php | 28 +-
app/DTO/Invitation/UserInvitationDTO.php | 8 +-
app/Http/Controllers/API/ActivityController.php | 11 +-
app/Http/Controllers/API/V2/AskJiminnyReportsController.php | 2 +
app/Http/Requests/Settings/Teams/CreateTeamRequest.php | 12 +
app/Http/Requests/Settings/Teams/EditTeamRequest.php | 1 +
app/Http/Transformers/SearchTransformer.php | 1 +
app/Jobs/Activity/Import/DownloadTrack.php | 8 +-
app/Models/AskAnything/AskAnythingPrompt.php | 9 +
app/Notifications/Activities/SmsReceived.php | 6 +-
app/Providers/JiminnyServiceProvider.php | 6 -
app/Repositories/AskAnythingRepository.php | 8 +-
app/Services/Activity/RingCentral/Service.php | 38 +-
app/Services/Activity/Salesforce/Service.php | 5 -
app/Services/Activity/TwilioVideo/Service.php | 26 +-
app/Services/Kiosk/AutomatedReports/AutomatedReportsService.php | 7 +
app/Services/Mail/Office/EmailApiClient.php | 78 +++-
app/Services/Mail/TextRelayService.php | 33 ++
app/Services/RecallAI/Commands/ScheduleBotCommand.php | 53 ++-
app/Services/RecallAI/RecallAIService.php | 11 +
front-end/src/components/AiReports/Manage/useReportsGrid.js | 5 +-
front-end/src/components/DealInsights/Modals/AskAnythingModal.vue | 30 ++
front-end/src/components/DealInsights/Modals/__tests__/AskAnythingModal.spec.js | 71 ++++
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/EditTeamModal.vue | 51 ++-
front-end/src/components/Settings/Kiosk/modals/EditTeamModal/__tests__/EditTeamModal.spec.js | 14 +
front-end/src/components/playback/Framework/FrameworksTab.vue | 22 +-
front-end/src/components/shared/AskAnything/AskAnythingPromptFormItem.vue | 2 +
front-end/src/components/shared/AskAnything/AskAnythingSettingsDrawer.vue | 23 +-
front-end/src/components/shared/AskAnything/__mocks__/AskAnythingSettingsDrawer.mocks.js | 1 +
front-end/src/components/shared/AskAnything/__tests__/AskAnythingSettingsDrawer.spec.js | 139 ++++---
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.html | 1235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
front-end/src/components/shared/AskAnything/__tests__/__snapshots__/AskAnythingSettingsDrawer.spec.js.snap | 1382 ---------------------------------------------------------------
front-end/src/components/shared/AskAnything/prompts.js | 1 +
front-end/src/components/shared/AskAnything/useAskAnything.js | 1 +
front-end/src/components/shared/SavedSearchModal/__mocks__/saved-searches.js | 1 +
front-end/src/components/shared/SavedSearchModal/__tests__/SavedSearchModal.spec.js | 60 ++-
front-end/src/components/shared/SavedSearchModal/useSavedSearch.js | 55 ++-
front-end/src/store/modules/playback/mutations.js | 1 +
tests/Feature/DTO/UserInvitationDTOTest.php | 65 ++-
tests/Unit/Component/AskAnything/AskAnythingPromptServiceTest.php | 37 +-
tests/Unit/Component/ES/AsyncUpdateElasticSearchTest.php | 24 +-
tests/Unit/Component/ES/Processor/BatchStatusManagerTest.php | 16 +-
tests/Unit/Component/ES/Processor/DTOs/SelectionListTest.php | 144 ++++---
tests/Unit/Component/ES/Processor/RedisEntityExtractorTest.php | 106 +++++
tests/Unit/Component/ES/Processor/TargetEntitiesSelectorTest.php | 438 +++++++++++++++-----
tests/Unit/Component/ES/Processor/Traits/SelectEntityListTraitTest.php | 43 +-
tests/Unit/Component/ES/Processor/Traits/WorkerIdTraitTest.php | 23 --
tests/Unit/Component/ES/QueuePriorityEnumTest.php | 64 +++
tests/Unit/Component/ES/UpdateProcessManagerTest.php | 102 +++--
tests/Unit/Component/Transcription/Service/TranscriptionServiceTest.php | 31 +-
tests/Unit/Notifications/Activities/SmsReceivedTest.php | 50 +++
tests/Unit/Services/Activity/RingCentral/ServiceTest.php | 339 +++++++++++++++-
tests/Unit/Services/Activity/TwilioVideo/ServiceTest.php | 67 +++-
tests/Unit/Services/Activity/TwilioVideo/TwilioVideoMetadataHandlerTest.php ...
|
71719
|
NULL
|
NULL
|
NULL
|
|
71720
|
2573
|
39
|
2026-05-26T07:05:20.677767+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779120677_m2.jpg...
|
Firefox
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira — Work...
|
1
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37?selectedIssue=JY-20891...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Service-Desk - Queues - Platform team - Service space - Jira
New Tab
New Tab
Login - SonarQube Cloud
Login - SonarQube Cloud
What's New in Firefox 151 — Firefox.com
What's New in Firefox 151 — Firefox.com
New Tab
New Tab
[JY-20814] Release unused Twilio numbers on Production - Jira
[JY-20814] Release unused Twilio numbers on Production - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
New Tab
New Tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints
Active sprints
Calendar
Calendar
Reports
Reports
Testing Board
Testing Board
List
List
12 more tabs
More
9+
Add to navigation
As you type to search or apply filters, the board updates with work items to match.
Search on current page
Filter by assignee
Filter assignees by Lukas Kovalik
Filter assignees by Aneliya Angelova
Filter assignees by Nikolay Ivanov
Filter assignees by Nikolay Nikolov
Filter assignees by Nikolay Yankov
Filter assignees by Steliyan Georgiev
Filter assignees by Unassigned
Epic
Epic
Type
Type
Quick filters
Quick filters
Complete sprint
Complete sprint
Sprint details
Sprint details
Group by Queries
Group
: Queries
Sprint insights
Sprint insights
View settings
View settings
More actions
More actions
Ready For DEV
READY FOR DEV
1
In DEV
IN DEV
2
Code Review
CODE REVIEW
1
Blocked
BLOCKED
QA
QA
1
PO Acceptance
PO ACCEPTANCE
Deploy
DEPLOY
1
7
JY-20613 Allow owner's role to be selected when setting up a trial. Use the enter key to load the work item.
Allow owner's role to be selected when setting up a trial
Improvement of our efficiency, Edit Parent
IMPROVEMENT OF OUR EFFICIENCY
Deployed
JY-20613
JY-20613
1.5
merged pull request
JY-20880 [Deadline 25 May] Migrate depricated Gemini 3.1 Flash Lite Preview model. Use the enter key to load the work item.
[Deadline 25 May] Migrate depricated Gemini 3.1 Flash Lite Preview model
Maintenance, Edit Parent
MAINTENANCE
Deployed
JY-20880
JY-20880
1
merged pull request
JY-19951 Setup test coverage for Prophet in Sonar. Use the enter key to load the work item.
Setup test coverage for Prophet in Sonar
MAINTENANCE
Deployed
JY-19951
JY-19951
1
merged pull request
JY-20410 Improve Activity Type suggestions. Use the enter key to load the work item.
Improve Activity Type suggestions
Auto-detected activity type, Edit Parent
AUTO-DETECTED ACTIVITY TYPE
Ready for Deployment
JY-20410
JY-20410
2
Successful deployment to production.
JY-20881 Upgrade Python and libraries - May. Use the enter key to load the work item.
Upgrade Python and libraries - May
Maintenance, Edit Parent
MAINTENANCE
Deployed
JY-20881
JY-20881
1
Successful deployment to production.
JY-20272 [Deadline 17 June] Migrate depricated Gemini models. Use the enter key to load the work item.
[Deadline 17 June] Migrate depricated Gemini models
Maintenance, Edit Parent
MAINTENANCE
Deployed
JY-20272
JY-20272
0...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0018284575,"top":0.0518755,"width":0.038065158,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXRadioButton","text":"Service-Desk - Queues - Platform team - Service space - Jira","depth":4,"bounds":{"left":0.039893616,"top":0.0518755,"width":0.037898935,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.09497207,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.10614525,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Login - SonarQube Cloud","depth":4,"bounds":{"left":0.0,"top":0.12769353,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Login - SonarQube Cloud","depth":5,"bounds":{"left":0.013297873,"top":0.13886672,"width":0.044049203,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"What's New in Firefox 151 — Firefox.com","depth":4,"bounds":{"left":0.0,"top":0.16041501,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"What's New in Firefox 151 — Firefox.com","depth":5,"bounds":{"left":0.013297873,"top":0.17158818,"width":0.07014628,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.19313647,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.20430966,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20814] Release unused Twilio numbers on Production - Jira","depth":4,"bounds":{"left":0.0,"top":0.22585794,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20814] Release unused Twilio numbers on Production - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.23703113,"width":0.11087101,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":4,"bounds":{"left":0.0,"top":0.2585794,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":5,"bounds":{"left":0.013297873,"top":0.2697526,"width":0.4644282,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.29130086,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.30247405,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.3256185,"width":0.07413564,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to:","depth":4,"bounds":{"left":0.090259306,"top":0.07861133,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Top Bar","depth":5,"bounds":{"left":0.090259306,"top":0.097765364,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Top Bar","depth":6,"bounds":{"left":0.090259306,"top":0.097765364,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Sidebar","depth":5,"bounds":{"left":0.090259306,"top":0.11691939,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Sidebar","depth":6,"bounds":{"left":0.090259306,"top":0.11691939,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Main Content","depth":5,"bounds":{"left":0.090259306,"top":0.13607343,"width":0.029421542,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Main Content","depth":6,"bounds":{"left":0.090259306,"top":0.13607343,"width":0.029421542,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Space navigation","depth":5,"bounds":{"left":0.090259306,"top":0.15522745,"width":0.037898935,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Space navigation","depth":6,"bounds":{"left":0.090259306,"top":0.15522745,"width":0.037898935,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Collapse sidebar [","depth":4,"bounds":{"left":0.08361037,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Collapse sidebar [","depth":6,"bounds":{"left":0.0887633,"top":0.06344773,"width":0.039727394,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Switch sites or apps","depth":5,"bounds":{"left":0.095578454,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Switch sites or apps","depth":7,"bounds":{"left":0.10073138,"top":0.06344773,"width":0.044215426,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Go to your Jira homepage","depth":4,"bounds":{"left":0.10887633,"top":0.057861134,"width":0.029421542,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXComboBox","text":"Search, press enter to navigate to advanced search with your text query","depth":6,"bounds":{"left":0.20694813,"top":0.06264964,"width":0.13829787,"height":0.015961692},"on_screen":true,"help_text":"","placeholder":"Search","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Create","depth":5,"bounds":{"left":0.35355717,"top":0.057861134,"width":0.030086435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create","depth":7,"bounds":{"left":0.3648604,"top":0.06384677,"width":0.014793883,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Rovo Ask Rovo","depth":7,"bounds":{"left":0.41206783,"top":0.057861134,"width":0.036070477,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Rovo","depth":9,"bounds":{"left":0.42337102,"top":0.06384677,"width":0.020777926,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Notifications","depth":7,"bounds":{"left":0.44946808,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Notifications","depth":9,"bounds":{"left":0.45462102,"top":0.06344773,"width":0.027759308,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Help","depth":7,"bounds":{"left":0.46143618,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Help","depth":9,"bounds":{"left":0.4665891,"top":0.06344773,"width":0.010139627,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Settings","depth":7,"bounds":{"left":0.47340426,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Settings","depth":9,"bounds":{"left":0.47855717,"top":0.06344773,"width":0.017952127,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"lukas.kovalik@jiminny.com","depth":7,"bounds":{"left":0.48537233,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"lukas.kovalik@jiminny.com","depth":9,"bounds":{"left":0.49052528,"top":0.06344773,"width":0.05867686,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"For you","depth":7,"bounds":{"left":0.08361037,"top":0.09976058,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"For you","depth":10,"bounds":{"left":0.09424867,"top":0.10574621,"width":0.01662234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Recent","depth":7,"bounds":{"left":0.08361037,"top":0.12529927,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Recent","depth":10,"bounds":{"left":0.09424867,"top":0.13128492,"width":0.015458777,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Starred","depth":7,"bounds":{"left":0.08361037,"top":0.15083799,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Starred","depth":10,"bounds":{"left":0.09424867,"top":0.15682362,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Apps","depth":7,"bounds":{"left":0.08361037,"top":0.1763767,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Apps","depth":10,"bounds":{"left":0.09424867,"top":0.18236233,"width":0.011635638,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Apps","depth":8,"bounds":{"left":0.15309176,"top":0.17956904,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Apps","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Spaces","depth":7,"bounds":{"left":0.08361037,"top":0.2019154,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXStaticText","text":"Spaces","depth":10,"bounds":{"left":0.09424867,"top":0.20790103,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Create space","depth":8,"bounds":{"left":0.13646941,"top":0.20510775,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create space","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for spaces","depth":8,"bounds":{"left":0.14577793,"top":0.20510775,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for spaces","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Recent","depth":11,"bounds":{"left":0.08959442,"top":0.23423783,"width":0.013464096,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Jiminny (New)","depth":12,"bounds":{"left":0.08759973,"top":0.2529928,"width":0.0674867,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny (New)","depth":15,"bounds":{"left":0.09823803,"top":0.25897846,"width":0.032081116,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Jiminny (New)","depth":13,"bounds":{"left":0.08892952,"top":0.25618514,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXMenuButton","text":"Create board","depth":13,"bounds":{"left":0.13646941,"top":0.25618514,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Create board","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Jiminny (New)","depth":13,"bounds":{"left":0.14577793,"top":0.25618514,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Jiminny (New)","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Platform Team","depth":14,"bounds":{"left":0.09158909,"top":0.27853152,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Team","depth":17,"bounds":{"left":0.1022274,"top":0.28451717,"width":0.032247342,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"bounds":{"left":0.15309176,"top":0.28172386,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Capture Team","depth":14,"bounds":{"left":0.09158909,"top":0.30407023,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Capture Team","depth":17,"bounds":{"left":0.1022274,"top":0.31005585,"width":0.03125,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"bounds":{"left":0.15309176,"top":0.30726257,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Enterprise Stability Issues 🤕","depth":14,"bounds":{"left":0.09158909,"top":0.32960895,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Enterprise Stability Issues 🤕","depth":17,"bounds":{"left":0.1022274,"top":0.33559456,"width":0.050531916,"height":0.030726258},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"bounds":{"left":0.15309176,"top":0.33280128,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Processing Team","depth":14,"bounds":{"left":0.09158909,"top":0.35514766,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Processing Team","depth":17,"bounds":{"left":0.1022274,"top":0.36113328,"width":0.038231384,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"bounds":{"left":0.15309176,"top":0.35834,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SE Kanban","depth":14,"bounds":{"left":0.09158909,"top":0.38068634,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SE Kanban","depth":17,"bounds":{"left":0.1022274,"top":0.386672,"width":0.024102394,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"bounds":{"left":0.15309176,"top":0.38387868,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Service-Desk","depth":12,"bounds":{"left":0.08759973,"top":0.40622506,"width":0.0674867,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Service-Desk","depth":15,"bounds":{"left":0.09823803,"top":0.4122107,"width":0.03025266,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Service-Desk","depth":13,"bounds":{"left":0.15442154,"top":0.4094174,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Service-Desk","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"More spaces","depth":12,"bounds":{"left":0.08759973,"top":0.43176377,"width":0.0674867,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More spaces","depth":15,"bounds":{"left":0.09823803,"top":0.43774942,"width":0.028756648,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Filters","depth":7,"bounds":{"left":0.08361037,"top":0.45730248,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Filters","depth":10,"bounds":{"left":0.09424867,"top":0.4632881,"width":0.013796543,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Filters","depth":8,"bounds":{"left":0.15309176,"top":0.46049482,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Filters","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dashboards","depth":7,"bounds":{"left":0.08361037,"top":0.4828412,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Dashboards","depth":10,"bounds":{"left":0.09424867,"top":0.4888268,"width":0.026761968,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Create dashboard","depth":8,"bounds":{"left":0.15508644,"top":0.48603353,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create dashboard","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Dashboards","depth":8,"bounds":{"left":0.16240026,"top":0.48603353,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Dashboards","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Operations","depth":7,"bounds":{"left":0.08361037,"top":0.5083799,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Operations","depth":10,"bounds":{"left":0.09424867,"top":0.5143655,"width":0.02443484,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Operations","depth":8,"bounds":{"left":0.15309176,"top":0.51157224,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Operations","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Confluence , (opens new window)","depth":8,"bounds":{"left":0.08361037,"top":0.5434956,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Confluence","depth":12,"bounds":{"left":0.09424867,"top":0.5494813,"width":0.025764627,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", (opens new window)","depth":10,"bounds":{"left":0.08361037,"top":0.55706304,"width":0.04837101,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Teams , (opens new window)","depth":8,"bounds":{"left":0.08361037,"top":0.56903434,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Teams","depth":12,"bounds":{"left":0.09424867,"top":0.57501996,"width":0.014793883,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", (opens new window)","depth":10,"bounds":{"left":0.08361037,"top":0.5826017,"width":0.04837101,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"open menu","depth":9,"bounds":{"left":0.14378324,"top":0.57222664,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"open menu","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Customise sidebar","depth":7,"bounds":{"left":0.08361037,"top":0.60415006,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Customise sidebar","depth":10,"bounds":{"left":0.09424867,"top":0.6101357,"width":0.04155585,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Resize side navigation panel","depth":8,"bounds":{"left":0.2109375,"top":0.0981644,"width":0.062333778,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Spaces","depth":8,"bounds":{"left":0.16738696,"top":0.09976058,"width":0.016289894,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Spaces","depth":10,"bounds":{"left":0.16738696,"top":0.102553874,"width":0.016289894,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":8,"bounds":{"left":0.18683511,"top":0.102553874,"width":0.0016622341,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Jiminny (New)","depth":8,"bounds":{"left":0.19165559,"top":0.09976058,"width":0.03174867,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny (New)","depth":10,"bounds":{"left":0.19165559,"top":0.102553874,"width":0.03174867,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Platform Team","depth":5,"bounds":{"left":0.16738696,"top":0.12210695,"width":0.045877658,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Platform Team","depth":6,"bounds":{"left":0.16738696,"top":0.12210695,"width":0.045877658,"height":0.019553073},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add people","depth":5,"bounds":{"left":0.21525931,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add people","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":5,"bounds":{"left":0.2278923,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Share","depth":5,"bounds":{"left":0.44148937,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Automation","depth":5,"bounds":{"left":0.45478722,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give feedback","depth":5,"bounds":{"left":0.4680851,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Give feedback","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Enter full screen","depth":5,"bounds":{"left":0.48138297,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Enter full screen","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Summary","depth":8,"bounds":{"left":0.16472739,"top":0.14764565,"width":0.035904255,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Summary","depth":10,"bounds":{"left":0.17603059,"top":0.15363128,"width":0.021276595,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Timeline","depth":8,"bounds":{"left":0.20196144,"top":0.14764565,"width":0.03357713,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Timeline","depth":10,"bounds":{"left":0.21326463,"top":0.15363128,"width":0.018949468,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Backlog","depth":8,"bounds":{"left":0.23686835,"top":0.14764565,"width":0.032413565,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Backlog","depth":10,"bounds":{"left":0.24817154,"top":0.15363128,"width":0.017785905,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Active sprints","depth":8,"bounds":{"left":0.2706117,"top":0.14764565,"width":0.045212764,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Active sprints","depth":10,"bounds":{"left":0.2819149,"top":0.15363128,"width":0.030585106,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Calendar","depth":8,"bounds":{"left":0.31715426,"top":0.14764565,"width":0.03474069,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Calendar","depth":10,"bounds":{"left":0.32845744,"top":0.15363128,"width":0.020113032,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Reports","depth":8,"bounds":{"left":0.35322472,"top":0.14764565,"width":0.031914894,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Reports","depth":10,"bounds":{"left":0.3645279,"top":0.15363128,"width":0.017287234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Testing Board","depth":8,"bounds":{"left":0.38646942,"top":0.14764565,"width":0.046708778,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Testing Board","depth":10,"bounds":{"left":0.3977726,"top":0.15363128,"width":0.030751329,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"List","depth":8,"bounds":{"left":0.43450797,"top":0.14764565,"width":0.02244016,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"List","depth":10,"bounds":{"left":0.44581118,"top":0.15363128,"width":0.0078125,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"12 more tabs","depth":6,"bounds":{"left":0.4582779,"top":0.14764565,"width":0.026097074,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":7,"bounds":{"left":0.4616024,"top":0.15363128,"width":0.011469414,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"9+","depth":8,"bounds":{"left":0.4758976,"top":0.15442938,"width":0.0051529254,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Add to navigation","depth":6,"bounds":{"left":0.48570478,"top":0.15083799,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"As you type to search or apply filters, the board updates with work items to match.","depth":6,"bounds":{"left":0.16738696,"top":0.23463687,"width":0.18134974,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXTextField","text":"Search on current page","depth":6,"bounds":{"left":0.17569813,"top":0.22067039,"width":0.050531916,"height":0.026735835},"on_screen":true,"placeholder":"Search board","role_description":"text field","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Filter by assignee","depth":7,"bounds":{"left":0.23121676,"top":0.22226655,"width":0.03873005,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXCheckBox","text":"Filter assignees by Lukas Kovalik","depth":6,"bounds":{"left":0.23254654,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Aneliya Angelova","depth":6,"bounds":{"left":0.24052526,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Nikolay Ivanov","depth":6,"bounds":{"left":0.24850398,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Nikolay Nikolov","depth":6,"bounds":{"left":0.25648272,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Nikolay Yankov","depth":6,"bounds":{"left":0.26446143,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Steliyan Georgiev","depth":6,"bounds":{"left":0.27244017,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Unassigned","depth":6,"bounds":{"left":0.28041887,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Epic","depth":8,"bounds":{"left":0.29238698,"top":0.22106944,"width":0.0234375,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Epic","depth":11,"bounds":{"left":0.29637632,"top":0.22705507,"width":0.009474734,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Type","depth":8,"bounds":{"left":0.31848404,"top":0.22106944,"width":0.025099734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Type","depth":11,"bounds":{"left":0.3224734,"top":0.22705507,"width":0.011136968,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Quick filters","depth":8,"bounds":{"left":0.34624335,"top":0.22106944,"width":0.040724736,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Quick filters","depth":11,"bounds":{"left":0.35023272,"top":0.22705507,"width":0.026761968,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Complete sprint","depth":5,"bounds":{"left":0.35106382,"top":0.18914606,"width":0.04338431,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Complete sprint","depth":7,"bounds":{"left":0.3550532,"top":0.19513169,"width":0.035405584,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Sprint details","depth":5,"bounds":{"left":0.39710772,"top":0.18914606,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Sprint details","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Group by Queries","depth":5,"bounds":{"left":0.41040558,"top":0.18914606,"width":0.041722074,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Group","depth":8,"bounds":{"left":0.41439494,"top":0.19513169,"width":0.013796543,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": Queries","depth":8,"bounds":{"left":0.42819148,"top":0.19513169,"width":0.019946808,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Sprint insights","depth":5,"bounds":{"left":0.45478722,"top":0.18914606,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Sprint insights","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"View settings","depth":5,"bounds":{"left":0.4680851,"top":0.18914606,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"View settings","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions","depth":5,"bounds":{"left":0.48138297,"top":0.18914606,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Ready For DEV","depth":11,"bounds":{"left":0.10571808,"top":0.27214685,"width":0.042220745,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"READY FOR DEV","depth":14,"bounds":{"left":0.10571808,"top":0.2725459,"width":0.03158245,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":17,"bounds":{"left":0.14178856,"top":0.2725459,"width":0.0016622341,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"In DEV","depth":11,"bounds":{"left":0.16173537,"top":0.27214685,"width":0.024601065,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"IN DEV","depth":14,"bounds":{"left":0.16173537,"top":0.2725459,"width":0.013962766,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":17,"bounds":{"left":0.1796875,"top":0.2725459,"width":0.002493351,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Code Review","depth":11,"bounds":{"left":0.21758644,"top":0.27214685,"width":0.0390625,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CODE REVIEW","depth":14,"bounds":{"left":0.21758644,"top":0.2725459,"width":0.028424202,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":17,"bounds":{"left":0.25049868,"top":0.2725459,"width":0.0016622341,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Blocked","depth":11,"bounds":{"left":0.27360374,"top":0.27214685,"width":0.018783245,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"BLOCKED","depth":14,"bounds":{"left":0.27360374,"top":0.2725459,"width":0.018783245,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"QA","depth":11,"bounds":{"left":0.32945478,"top":0.27214685,"width":0.016456118,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"QA","depth":14,"bounds":{"left":0.32945478,"top":0.2725459,"width":0.005817819,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":17,"bounds":{"left":0.33976063,"top":0.2725459,"width":0.0016622341,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"PO Acceptance","depth":11,"bounds":{"left":0.3854721,"top":0.27214685,"width":0.03357713,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PO ACCEPTANCE","depth":14,"bounds":{"left":0.3854721,"top":0.2725459,"width":0.03357713,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Deploy","depth":11,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEPLOY","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"JY-20613 Allow owner's role to be selected when setting up a trial. Use the enter key to load the work item.","depth":11,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Allow owner's role to be selected when setting up a trial","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Improvement of our efficiency, Edit Parent","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"IMPROVEMENT OF OUR EFFICIENCY","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deployed","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20613","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20613","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1.5","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"merged pull request","depth":11,"on_screen":false,"role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20880 [Deadline 25 May] Migrate depricated Gemini 3.1 Flash Lite Preview model. Use the enter key to load the work item.","depth":11,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[Deadline 25 May] Migrate depricated Gemini 3.1 Flash Lite Preview model","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Maintenance, Edit Parent","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"MAINTENANCE","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deployed","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20880","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20880","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"merged pull request","depth":11,"on_screen":false,"role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-19951 Setup test coverage for Prophet in Sonar. Use the enter key to load the work item.","depth":11,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Setup test coverage for Prophet in Sonar","depth":12,"bounds":{"left":0.4429854,"top":0.0,"width":0.028091755,"height":0.061851557},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"MAINTENANCE","depth":13,"bounds":{"left":0.44431517,"top":0.0,"width":0.029089095,"height":0.011173184},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deployed","depth":12,"bounds":{"left":0.4429854,"top":0.0,"width":0.017952127,"height":0.011971269},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-19951","depth":12,"bounds":{"left":0.4496343,"top":0.02952913,"width":0.017453458,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-19951","depth":14,"bounds":{"left":0.4496343,"top":0.029928172,"width":0.017453458,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":12,"bounds":{"left":0.44630983,"top":0.0011971269,"width":0.0014960107,"height":0.011971269},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"merged pull request","depth":11,"bounds":{"left":0.45295876,"top":0.0,"width":0.007978723,"height":0.01915403},"on_screen":false,"role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20410 Improve Activity Type suggestions. Use the enter key to load the work item.","depth":11,"bounds":{"left":0.4403258,"top":0.056664005,"width":0.050531916,"height":0.16001596},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Improve Activity Type suggestions","depth":13,"bounds":{"left":0.4429854,"top":0.06424581,"width":0.028756648,"height":0.045889866},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Auto-detected activity type, Edit Parent","depth":12,"bounds":{"left":0.4429854,"top":0.11731844,"width":0.045212764,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AUTO-DETECTED ACTIVITY TYPE","depth":16,"bounds":{"left":0.44431517,"top":0.118515566,"width":0.063663565,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Ready for Deployment","depth":12,"bounds":{"left":0.4429854,"top":0.13886672,"width":0.042054523,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20410","depth":12,"bounds":{"left":0.4496343,"top":0.19273743,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20410","depth":14,"bounds":{"left":0.4496343,"top":0.19313647,"width":0.018284574,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":12,"bounds":{"left":0.44581118,"top":0.16440542,"width":0.002493351,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Successful deployment to production.","depth":11,"bounds":{"left":0.45295876,"top":0.16081405,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20881 Upgrade Python and libraries - May. Use the enter key to load the work item.","depth":11,"bounds":{"left":0.4403258,"top":0.21987231,"width":0.050531916,"height":0.16001596},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Upgrade Python and libraries - May","depth":13,"bounds":{"left":0.4429854,"top":0.22745411,"width":0.03174867,"height":0.045889866},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Maintenance, Edit Parent","depth":12,"bounds":{"left":0.4429854,"top":0.28052673,"width":0.03174867,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"MAINTENANCE","depth":16,"bounds":{"left":0.44431517,"top":0.28172386,"width":0.029089095,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deployed","depth":12,"bounds":{"left":0.4429854,"top":0.30207503,"width":0.017952127,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20881","depth":12,"bounds":{"left":0.4496343,"top":0.35594574,"width":0.018284574,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20881","depth":14,"bounds":{"left":0.4496343,"top":0.35634476,"width":0.018284574,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":12,"bounds":{"left":0.44630983,"top":0.32761374,"width":0.0014960107,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Successful deployment to production.","depth":11,"bounds":{"left":0.45295876,"top":0.32402235,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20272 [Deadline 17 June] Migrate depricated Gemini models. Use the enter key to load the work item.","depth":11,"bounds":{"left":0.4403258,"top":0.3830806,"width":0.050531916,"height":0.16001596},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[Deadline 17 June] Migrate depricated Gemini models","depth":13,"bounds":{"left":0.4429854,"top":0.3906624,"width":0.032579787,"height":0.061851557},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Maintenance, Edit Parent","depth":12,"bounds":{"left":0.4429854,"top":0.44373503,"width":0.03174867,"height":0.0131683955},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"MAINTENANCE","depth":16,"bounds":{"left":0.44431517,"top":0.44493216,"width":0.029089095,"height":0.011173184},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deployed","depth":12,"bounds":{"left":0.4429854,"top":0.46528333,"width":0.017952127,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20272","depth":12,"bounds":{"left":0.4496343,"top":0.519154,"width":0.018783245,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20272","depth":14,"bounds":{"left":0.4496343,"top":0.51955307,"width":0.018783245,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"0","depth":12,"bounds":{"left":0.44581118,"top":0.49082202,"width":0.002493351,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-2691616374744594534
|
5931636727458419879
|
click
|
accessibility
|
NULL
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Service-Desk - Queues - Platform team - Service space - Jira
New Tab
New Tab
Login - SonarQube Cloud
Login - SonarQube Cloud
What's New in Firefox 151 — Firefox.com
What's New in Firefox 151 — Firefox.com
New Tab
New Tab
[JY-20814] Release unused Twilio numbers on Production - Jira
[JY-20814] Release unused Twilio numbers on Production - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
New Tab
New Tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints
Active sprints
Calendar
Calendar
Reports
Reports
Testing Board
Testing Board
List
List
12 more tabs
More
9+
Add to navigation
As you type to search or apply filters, the board updates with work items to match.
Search on current page
Filter by assignee
Filter assignees by Lukas Kovalik
Filter assignees by Aneliya Angelova
Filter assignees by Nikolay Ivanov
Filter assignees by Nikolay Nikolov
Filter assignees by Nikolay Yankov
Filter assignees by Steliyan Georgiev
Filter assignees by Unassigned
Epic
Epic
Type
Type
Quick filters
Quick filters
Complete sprint
Complete sprint
Sprint details
Sprint details
Group by Queries
Group
: Queries
Sprint insights
Sprint insights
View settings
View settings
More actions
More actions
Ready For DEV
READY FOR DEV
1
In DEV
IN DEV
2
Code Review
CODE REVIEW
1
Blocked
BLOCKED
QA
QA
1
PO Acceptance
PO ACCEPTANCE
Deploy
DEPLOY
1
7
JY-20613 Allow owner's role to be selected when setting up a trial. Use the enter key to load the work item.
Allow owner's role to be selected when setting up a trial
Improvement of our efficiency, Edit Parent
IMPROVEMENT OF OUR EFFICIENCY
Deployed
JY-20613
JY-20613
1.5
merged pull request
JY-20880 [Deadline 25 May] Migrate depricated Gemini 3.1 Flash Lite Preview model. Use the enter key to load the work item.
[Deadline 25 May] Migrate depricated Gemini 3.1 Flash Lite Preview model
Maintenance, Edit Parent
MAINTENANCE
Deployed
JY-20880
JY-20880
1
merged pull request
JY-19951 Setup test coverage for Prophet in Sonar. Use the enter key to load the work item.
Setup test coverage for Prophet in Sonar
MAINTENANCE
Deployed
JY-19951
JY-19951
1
merged pull request
JY-20410 Improve Activity Type suggestions. Use the enter key to load the work item.
Improve Activity Type suggestions
Auto-detected activity type, Edit Parent
AUTO-DETECTED ACTIVITY TYPE
Ready for Deployment
JY-20410
JY-20410
2
Successful deployment to production.
JY-20881 Upgrade Python and libraries - May. Use the enter key to load the work item.
Upgrade Python and libraries - May
Maintenance, Edit Parent
MAINTENANCE
Deployed
JY-20881
JY-20881
1
Successful deployment to production.
JY-20272 [Deadline 17 June] Migrate depricated Gemini models. Use the enter key to load the work item.
[Deadline 17 June] Migrate depricated Gemini models
Maintenance, Edit Parent
MAINTENANCE
Deployed
JY-20272
JY-20272
0...
|
71718
|
NULL
|
NULL
|
NULL
|
|
71719
|
2572
|
9
|
2026-05-26T07:05:20.576362+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779120576_m1.jpg...
|
Firefox
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira — Work...
|
1
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37?selectedIssue=JY-20891...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Service-Desk - Queues - Platform team - Service space - Jira
New Tab
New Tab
Login - SonarQube Cloud
Login - SonarQube Cloud
What's New in Firefox 151 — Firefox.com
What's New in Firefox 151 — Firefox.com
New Tab
New Tab
[JY-20814] Release unused Twilio numbers on Production - Jira
[JY-20814] Release unused Twilio numbers on Production - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
New Tab
New Tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints
Active sprints
Calendar
Calendar
Reports
Reports
Testing Board
Testing Board
List
List
12 more tabs
More
9+
Add to navigation
As you type to search or apply filters, the board updates with work items to match.
Search on current page
Filter by assignee
Filter assignees by Lukas Kovalik
Filter assignees by Aneliya Angelova
Filter assignees by Nikolay Ivanov
Filter assignees by Nikolay Nikolov
Filter assignees by Nikolay Yankov
Filter assignees by Steliyan Georgiev
Filter assignees by Unassigned
Epic
Epic
Type
Type
Quick filters
Quick filters
Complete sprint
Complete sprint
Sprint details
Sprint details
Group by Queries
Group
: Queries
Sprint insights
Sprint insights
View settings
View settings
More actions
More actions
Ready For DEV
READY FOR DEV
1
In DEV
IN DEV
2
Code Review
CODE REVIEW
1
Blocked
BLOCKED
QA
QA
1
PO Acceptance
PO ACCEPTANCE
Deploy
DEPLOY
1
7
JY-20613 Allow owner's role to be selected when setting up a trial. Use the enter key to load the work item.
Allow owner's role to be selected when setting up a trial
Improvement of our efficiency, Edit Parent
IMPROVEMENT OF OUR EFFICIENCY
Deployed
JY-20613
JY-20613
1.5
merged pull request
JY-20880 [Deadline 25 May] Migrate depricated Gemini 3.1 Flash Lite Preview model. Use the enter key to load the work item.
[Deadline 25 May] Migrate depricated Gemini 3.1 Flash Lite Preview model
Maintenance, Edit Parent
MAINTENANCE
Deployed
JY-20880
JY-20880
1
merged pull request
JY-19951 Setup test coverage for Prophet in Sonar. Use the enter key to load the work item.
Setup test coverage for Prophet in Sonar
MAINTENANCE
Deployed
JY-19951
JY-19951
1
merged pull request
JY-20410 Improve Activity Type suggestions. Use the enter key to load the work item.
Improve Activity Type suggestions
Auto-detected activity type, Edit Parent
AUTO-DETECTED ACTIVITY TYPE
Ready for Deployment
JY-20410
JY-20410
2
Successful deployment to production.
JY-20881 Upgrade Python and libraries - May. Use the enter key to load the work item.
Upgrade Python and libraries - May
Maintenance, Edit Parent
MAINTENANCE
Deployed
JY-20881
JY-20881
1
Successful deployment to production.
JY-20272 [Deadline 17 June] Migrate depricated Gemini models. Use the enter key to load the work item....
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXRadioButton","text":"Service-Desk - Queues - Platform team - Service space - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Login - SonarQube Cloud","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Login - SonarQube Cloud","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"What's New in Firefox 151 — Firefox.com","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"What's New in Firefox 151 — Firefox.com","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20814] Release unused Twilio numbers on Production - Jira","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20814] Release unused Twilio numbers on Production - Jira","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to:","depth":4,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Top Bar","depth":5,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Top Bar","depth":6,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Sidebar","depth":5,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Main Content","depth":5,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Main Content","depth":6,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Space navigation","depth":5,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Space navigation","depth":6,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Collapse sidebar [","depth":4,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Collapse sidebar [","depth":6,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Switch sites or apps","depth":5,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Switch sites or apps","depth":7,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Go to your Jira homepage","depth":4,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXComboBox","text":"Search, press enter to navigate to advanced search with your text query","depth":6,"on_screen":true,"help_text":"","placeholder":"Search","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Create","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create","depth":7,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Rovo Ask Rovo","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Rovo","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Notifications","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Notifications","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Help","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Help","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Settings","depth":7,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Settings","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"lukas.kovalik@jiminny.com","depth":7,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"lukas.kovalik@jiminny.com","depth":9,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"For you","depth":7,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"For you","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Recent","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Recent","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Starred","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Starred","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Apps","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Apps","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Apps","depth":8,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Apps","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Spaces","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXStaticText","text":"Spaces","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Create space","depth":8,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create space","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for spaces","depth":8,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for spaces","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Recent","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Jiminny (New)","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny (New)","depth":15,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Jiminny (New)","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXMenuButton","text":"Create board","depth":13,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Create board","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Jiminny (New)","depth":13,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Jiminny (New)","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Platform Team","depth":14,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Team","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Capture Team","depth":14,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Capture Team","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Enterprise Stability Issues 🤕","depth":14,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Enterprise Stability Issues 🤕","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Processing Team","depth":14,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Processing Team","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SE Kanban","depth":14,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SE Kanban","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Service-Desk","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Service-Desk","depth":15,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Service-Desk","depth":13,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Service-Desk","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"More spaces","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More spaces","depth":15,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Filters","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Filters","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Filters","depth":8,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Filters","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dashboards","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Dashboards","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Create dashboard","depth":8,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create dashboard","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Dashboards","depth":8,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Dashboards","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Operations","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Operations","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Operations","depth":8,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Operations","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Confluence , (opens new window)","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Confluence","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", (opens new window)","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Teams , (opens new window)","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Teams","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", (opens new window)","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"open menu","depth":9,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"open menu","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Customise sidebar","depth":7,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Customise sidebar","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Resize side navigation panel","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Spaces","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Spaces","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Jiminny (New)","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny (New)","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Platform Team","depth":5,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Platform Team","depth":6,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add people","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add people","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":5,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Share","depth":5,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Automation","depth":5,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give feedback","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Give feedback","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Enter full screen","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Enter full screen","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Summary","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Summary","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Timeline","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Timeline","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Backlog","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Backlog","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Active sprints","depth":8,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Active sprints","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Calendar","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Calendar","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Reports","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Reports","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Testing Board","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Testing Board","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"List","depth":8,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"List","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"12 more tabs","depth":6,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":7,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"9+","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Add to navigation","depth":6,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"As you type to search or apply filters, the board updates with work items to match.","depth":6,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXTextField","text":"Search on current page","depth":6,"on_screen":true,"placeholder":"Search board","role_description":"text field","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Filter by assignee","depth":7,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXCheckBox","text":"Filter assignees by Lukas Kovalik","depth":6,"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Aneliya Angelova","depth":6,"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Nikolay Ivanov","depth":6,"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Nikolay Nikolov","depth":6,"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Nikolay Yankov","depth":6,"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Steliyan Georgiev","depth":6,"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Unassigned","depth":6,"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Epic","depth":8,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Epic","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Type","depth":8,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Type","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Quick filters","depth":8,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Quick filters","depth":11,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Complete sprint","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Complete sprint","depth":7,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Sprint details","depth":5,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Sprint details","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Group by Queries","depth":5,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Group","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": Queries","depth":8,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Sprint insights","depth":5,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Sprint insights","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"View settings","depth":5,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"View settings","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions","depth":5,"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Ready For DEV","depth":11,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"READY FOR DEV","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"In DEV","depth":11,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"IN DEV","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Code Review","depth":11,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CODE REVIEW","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Blocked","depth":11,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"BLOCKED","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"QA","depth":11,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"QA","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"PO Acceptance","depth":11,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PO ACCEPTANCE","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Deploy","depth":11,"on_screen":false,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"DEPLOY","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"7","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"JY-20613 Allow owner's role to be selected when setting up a trial. Use the enter key to load the work item.","depth":11,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Allow owner's role to be selected when setting up a trial","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Improvement of our efficiency, Edit Parent","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"IMPROVEMENT OF OUR EFFICIENCY","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deployed","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20613","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20613","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1.5","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"merged pull request","depth":11,"on_screen":false,"role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20880 [Deadline 25 May] Migrate depricated Gemini 3.1 Flash Lite Preview model. Use the enter key to load the work item.","depth":11,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[Deadline 25 May] Migrate depricated Gemini 3.1 Flash Lite Preview model","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Maintenance, Edit Parent","depth":12,"on_screen":false,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"MAINTENANCE","depth":16,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deployed","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20880","depth":12,"on_screen":false,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20880","depth":14,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"merged pull request","depth":11,"on_screen":false,"role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-19951 Setup test coverage for Prophet in Sonar. Use the enter key to load the work item.","depth":11,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Setup test coverage for Prophet in Sonar","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"MAINTENANCE","depth":13,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deployed","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-19951","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-19951","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":12,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"merged pull request","depth":11,"on_screen":false,"role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20410 Improve Activity Type suggestions. Use the enter key to load the work item.","depth":11,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Improve Activity Type suggestions","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Auto-detected activity type, Edit Parent","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"AUTO-DETECTED ACTIVITY TYPE","depth":16,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Ready for Deployment","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20410","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20410","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Successful deployment to production.","depth":11,"on_screen":true,"role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20881 Upgrade Python and libraries - May. Use the enter key to load the work item.","depth":11,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Upgrade Python and libraries - May","depth":13,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Maintenance, Edit Parent","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"MAINTENANCE","depth":16,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Deployed","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"JY-20881","depth":12,"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"JY-20881","depth":14,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Successful deployment to production.","depth":11,"on_screen":true,"role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20272 [Deadline 17 June] Migrate depricated Gemini models. Use the enter key to load the work item.","depth":11,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
3048373033285671048
|
5931636710278550695
|
click
|
accessibility
|
NULL
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Service-Desk - Queues - Platform team - Service space - Jira
New Tab
New Tab
Login - SonarQube Cloud
Login - SonarQube Cloud
What's New in Firefox 151 — Firefox.com
What's New in Firefox 151 — Firefox.com
New Tab
New Tab
[JY-20814] Release unused Twilio numbers on Production - Jira
[JY-20814] Release unused Twilio numbers on Production - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
New Tab
New Tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints
Active sprints
Calendar
Calendar
Reports
Reports
Testing Board
Testing Board
List
List
12 more tabs
More
9+
Add to navigation
As you type to search or apply filters, the board updates with work items to match.
Search on current page
Filter by assignee
Filter assignees by Lukas Kovalik
Filter assignees by Aneliya Angelova
Filter assignees by Nikolay Ivanov
Filter assignees by Nikolay Nikolov
Filter assignees by Nikolay Yankov
Filter assignees by Steliyan Georgiev
Filter assignees by Unassigned
Epic
Epic
Type
Type
Quick filters
Quick filters
Complete sprint
Complete sprint
Sprint details
Sprint details
Group by Queries
Group
: Queries
Sprint insights
Sprint insights
View settings
View settings
More actions
More actions
Ready For DEV
READY FOR DEV
1
In DEV
IN DEV
2
Code Review
CODE REVIEW
1
Blocked
BLOCKED
QA
QA
1
PO Acceptance
PO ACCEPTANCE
Deploy
DEPLOY
1
7
JY-20613 Allow owner's role to be selected when setting up a trial. Use the enter key to load the work item.
Allow owner's role to be selected when setting up a trial
Improvement of our efficiency, Edit Parent
IMPROVEMENT OF OUR EFFICIENCY
Deployed
JY-20613
JY-20613
1.5
merged pull request
JY-20880 [Deadline 25 May] Migrate depricated Gemini 3.1 Flash Lite Preview model. Use the enter key to load the work item.
[Deadline 25 May] Migrate depricated Gemini 3.1 Flash Lite Preview model
Maintenance, Edit Parent
MAINTENANCE
Deployed
JY-20880
JY-20880
1
merged pull request
JY-19951 Setup test coverage for Prophet in Sonar. Use the enter key to load the work item.
Setup test coverage for Prophet in Sonar
MAINTENANCE
Deployed
JY-19951
JY-19951
1
merged pull request
JY-20410 Improve Activity Type suggestions. Use the enter key to load the work item.
Improve Activity Type suggestions
Auto-detected activity type, Edit Parent
AUTO-DETECTED ACTIVITY TYPE
Ready for Deployment
JY-20410
JY-20410
2
Successful deployment to production.
JY-20881 Upgrade Python and libraries - May. Use the enter key to load the work item.
Upgrade Python and libraries - May
Maintenance, Edit Parent
MAINTENANCE
Deployed
JY-20881
JY-20881
1
Successful deployment to production.
JY-20272 [Deadline 17 June] Migrate depricated Gemini models. Use the enter key to load the work item....
|
NULL
|
NULL
|
NULL
|
NULL
|
|
71718
|
2573
|
38
|
2026-05-26T07:05:18.833660+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-26/1779 /Users/lukas/.screenpipe/data/data/2026-05-26/1779779118833_m2.jpg...
|
Firefox
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira — Work...
|
1
|
jiminny.atlassian.net/jira/software/c/projects/JY/ jiminny.atlassian.net/jira/software/c/projects/JY/boards/37?selectedIssue=JY-20891...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Service-Desk - Queues - Platform team - Service space - Jira
New Tab
New Tab
Login - SonarQube Cloud
Login - SonarQube Cloud
What's New in Firefox 151 — Firefox.com
What's New in Firefox 151 — Firefox.com
New Tab
New Tab
[JY-20814] Release unused Twilio numbers on Production - Jira
[JY-20814] Release unused Twilio numbers on Production - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
New Tab
New Tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints
Active sprints
Calendar
Calendar
Reports
Reports
Testing Board
Testing Board
List
List
12 more tabs
More
9+
Add to navigation
As you type to search or apply filters, the board updates with work items to match.
Search on current page
Filter by assignee
Filter assignees by Lukas Kovalik
Filter assignees by Aneliya Angelova
Filter assignees by Nikolay Ivanov
Filter assignees by Nikolay Nikolov
Filter assignees by Nikolay Yankov
Filter assignees by Steliyan Georgiev
Filter assignees by Unassigned
Epic
Epic
Type
Type
Quick filters
Quick filters
Complete sprint
Complete sprint
Sprint details
Sprint details
Group by Queries
Group
: Queries
Sprint insights
Sprint insights
View settings
View settings
More actions
More actions
Ready For DEV
READY FOR DEV
1
In DEV
IN DEV
2
Code Review
CODE REVIEW
1
Blocked...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira","depth":4,"bounds":{"left":0.0018284575,"top":0.0518755,"width":0.038065158,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXRadioButton","text":"Service-Desk - Queues - Platform team - Service space - Jira","depth":4,"bounds":{"left":0.039893616,"top":0.0518755,"width":0.037898935,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.09497207,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.10614525,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"Login - SonarQube Cloud","depth":4,"bounds":{"left":0.0,"top":0.12769353,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Login - SonarQube Cloud","depth":5,"bounds":{"left":0.013297873,"top":0.13886672,"width":0.044049203,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"What's New in Firefox 151 — Firefox.com","depth":4,"bounds":{"left":0.0,"top":0.16041501,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"What's New in Firefox 151 — Firefox.com","depth":5,"bounds":{"left":0.013297873,"top":0.17158818,"width":0.07014628,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.19313647,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.20430966,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"[JY-20814] Release unused Twilio numbers on Production - Jira","depth":4,"bounds":{"left":0.0,"top":0.22585794,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"[JY-20814] Release unused Twilio numbers on Production - Jira","depth":5,"bounds":{"left":0.013297873,"top":0.23703113,"width":0.11087101,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":4,"bounds":{"left":0.0,"top":0.2585794,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SevenShores\\Hubspot\\Exceptions\\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT","depth":5,"bounds":{"left":0.013297873,"top":0.2697526,"width":0.4644282,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXRadioButton","text":"New Tab","depth":4,"bounds":{"left":0.0,"top":0.29130086,"width":0.07962101,"height":0.032721467},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"New Tab","depth":5,"bounds":{"left":0.013297873,"top":0.30247405,"width":0.014960106,"height":0.010774142},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.0028257978,"top":0.3256185,"width":0.07413564,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0028257978,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.013796543,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.024933511,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.036070477,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.04720745,"top":0.97007185,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Skip to:","depth":4,"bounds":{"left":0.090259306,"top":0.07861133,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Top Bar","depth":5,"bounds":{"left":0.090259306,"top":0.097765364,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Top Bar","depth":6,"bounds":{"left":0.090259306,"top":0.097765364,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Sidebar","depth":5,"bounds":{"left":0.090259306,"top":0.11691939,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Sidebar","depth":6,"bounds":{"left":0.090259306,"top":0.11691939,"width":0.016954787,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Main Content","depth":5,"bounds":{"left":0.090259306,"top":0.13607343,"width":0.029421542,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Main Content","depth":6,"bounds":{"left":0.090259306,"top":0.13607343,"width":0.029421542,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Space navigation","depth":5,"bounds":{"left":0.090259306,"top":0.15522745,"width":0.037898935,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Space navigation","depth":6,"bounds":{"left":0.090259306,"top":0.15522745,"width":0.037898935,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Collapse sidebar [","depth":4,"bounds":{"left":0.08361037,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Collapse sidebar [","depth":6,"bounds":{"left":0.0887633,"top":0.06344773,"width":0.039727394,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Switch sites or apps","depth":5,"bounds":{"left":0.095578454,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Switch sites or apps","depth":7,"bounds":{"left":0.10073138,"top":0.06344773,"width":0.044215426,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Go to your Jira homepage","depth":4,"bounds":{"left":0.10887633,"top":0.057861134,"width":0.029421542,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXComboBox","text":"Search, press enter to navigate to advanced search with your text query","depth":6,"bounds":{"left":0.20694813,"top":0.06264964,"width":0.13829787,"height":0.015961692},"on_screen":true,"help_text":"","placeholder":"Search","role_description":"combo box","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Create","depth":5,"bounds":{"left":0.35355717,"top":0.057861134,"width":0.030086435,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create","depth":7,"bounds":{"left":0.3648604,"top":0.06384677,"width":0.014793883,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Rovo Ask Rovo","depth":7,"bounds":{"left":0.41206783,"top":0.057861134,"width":0.036070477,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Ask Rovo","depth":9,"bounds":{"left":0.42337102,"top":0.06384677,"width":0.020777926,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Notifications","depth":7,"bounds":{"left":0.44946808,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Notifications","depth":9,"bounds":{"left":0.45462102,"top":0.06344773,"width":0.027759308,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Help","depth":7,"bounds":{"left":0.46143618,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Help","depth":9,"bounds":{"left":0.4665891,"top":0.06344773,"width":0.010139627,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Settings","depth":7,"bounds":{"left":0.47340426,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Settings","depth":9,"bounds":{"left":0.47855717,"top":0.06344773,"width":0.017952127,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"lukas.kovalik@jiminny.com","depth":7,"bounds":{"left":0.48537233,"top":0.057861134,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"lukas.kovalik@jiminny.com","depth":9,"bounds":{"left":0.49052528,"top":0.06344773,"width":0.05867686,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"For you","depth":7,"bounds":{"left":0.08361037,"top":0.09976058,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"For you","depth":10,"bounds":{"left":0.09424867,"top":0.10574621,"width":0.01662234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Recent","depth":7,"bounds":{"left":0.08361037,"top":0.12529927,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Recent","depth":10,"bounds":{"left":0.09424867,"top":0.13128492,"width":0.015458777,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Starred","depth":7,"bounds":{"left":0.08361037,"top":0.15083799,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Starred","depth":10,"bounds":{"left":0.09424867,"top":0.15682362,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Apps","depth":7,"bounds":{"left":0.08361037,"top":0.1763767,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Apps","depth":10,"bounds":{"left":0.09424867,"top":0.18236233,"width":0.011635638,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Apps","depth":8,"bounds":{"left":0.15309176,"top":0.17956904,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Apps","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Spaces","depth":7,"bounds":{"left":0.08361037,"top":0.2019154,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXStaticText","text":"Spaces","depth":10,"bounds":{"left":0.09424867,"top":0.20790103,"width":0.016456118,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Create space","depth":8,"bounds":{"left":0.13646941,"top":0.20510775,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create space","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for spaces","depth":8,"bounds":{"left":0.14577793,"top":0.20510775,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for spaces","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Recent","depth":11,"bounds":{"left":0.08959442,"top":0.23423783,"width":0.013464096,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Jiminny (New)","depth":12,"bounds":{"left":0.08759973,"top":0.2529928,"width":0.0674867,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny (New)","depth":15,"bounds":{"left":0.09823803,"top":0.25897846,"width":0.032081116,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Jiminny (New)","depth":13,"bounds":{"left":0.08892952,"top":0.25618514,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXMenuButton","text":"Create board","depth":13,"bounds":{"left":0.13646941,"top":0.25618514,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Create board","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Jiminny (New)","depth":13,"bounds":{"left":0.14577793,"top":0.25618514,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Jiminny (New)","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Platform Team","depth":14,"bounds":{"left":0.09158909,"top":0.27853152,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Platform Team","depth":17,"bounds":{"left":0.1022274,"top":0.28451717,"width":0.032247342,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"bounds":{"left":0.15309176,"top":0.28172386,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Capture Team","depth":14,"bounds":{"left":0.09158909,"top":0.30407023,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Capture Team","depth":17,"bounds":{"left":0.1022274,"top":0.31005585,"width":0.03125,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"bounds":{"left":0.15309176,"top":0.30726257,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Enterprise Stability Issues 🤕","depth":14,"bounds":{"left":0.09158909,"top":0.32960895,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Enterprise Stability Issues 🤕","depth":17,"bounds":{"left":0.1022274,"top":0.33559456,"width":0.050531916,"height":0.030726258},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"bounds":{"left":0.15309176,"top":0.33280128,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Processing Team","depth":14,"bounds":{"left":0.09158909,"top":0.35514766,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Processing Team","depth":17,"bounds":{"left":0.1022274,"top":0.36113328,"width":0.038231384,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"bounds":{"left":0.15309176,"top":0.35834,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"SE Kanban","depth":14,"bounds":{"left":0.09158909,"top":0.38068634,"width":0.06349734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"SE Kanban","depth":17,"bounds":{"left":0.1022274,"top":0.386672,"width":0.024102394,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":15,"bounds":{"left":0.15309176,"top":0.38387868,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":17,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Service-Desk","depth":12,"bounds":{"left":0.08759973,"top":0.40622506,"width":0.0674867,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Service-Desk","depth":15,"bounds":{"left":0.09823803,"top":0.4122107,"width":0.03025266,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Service-Desk","depth":13,"bounds":{"left":0.15442154,"top":0.4094174,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Service-Desk","depth":15,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"More spaces","depth":12,"bounds":{"left":0.08759973,"top":0.43176377,"width":0.0674867,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More spaces","depth":15,"bounds":{"left":0.09823803,"top":0.43774942,"width":0.028756648,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Filters","depth":7,"bounds":{"left":0.08361037,"top":0.45730248,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Filters","depth":10,"bounds":{"left":0.09424867,"top":0.4632881,"width":0.013796543,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Filters","depth":8,"bounds":{"left":0.15309176,"top":0.46049482,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Filters","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Dashboards","depth":7,"bounds":{"left":0.08361037,"top":0.4828412,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Dashboards","depth":10,"bounds":{"left":0.09424867,"top":0.4888268,"width":0.026761968,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Create dashboard","depth":8,"bounds":{"left":0.15508644,"top":0.48603353,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Create dashboard","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Dashboards","depth":8,"bounds":{"left":0.16240026,"top":0.48603353,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Dashboards","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Operations","depth":7,"bounds":{"left":0.08361037,"top":0.5083799,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Operations","depth":10,"bounds":{"left":0.09424867,"top":0.5143655,"width":0.02443484,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions for Operations","depth":8,"bounds":{"left":0.15309176,"top":0.51157224,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions for Operations","depth":10,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Confluence , (opens new window)","depth":8,"bounds":{"left":0.08361037,"top":0.5434956,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Confluence","depth":12,"bounds":{"left":0.09424867,"top":0.5494813,"width":0.025764627,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", (opens new window)","depth":10,"bounds":{"left":0.08361037,"top":0.55706304,"width":0.04837101,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Teams , (opens new window)","depth":8,"bounds":{"left":0.08361037,"top":0.56903434,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Teams","depth":12,"bounds":{"left":0.09424867,"top":0.57501996,"width":0.014793883,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":", (opens new window)","depth":10,"bounds":{"left":0.08361037,"top":0.5826017,"width":0.04837101,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"open menu","depth":9,"bounds":{"left":0.14378324,"top":0.57222664,"width":0.0039893617,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"open menu","depth":11,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Customise sidebar","depth":7,"bounds":{"left":0.08361037,"top":0.60415006,"width":0.071476065,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Customise sidebar","depth":10,"bounds":{"left":0.09424867,"top":0.6101357,"width":0.04155585,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Resize side navigation panel","depth":8,"bounds":{"left":0.2109375,"top":0.0981644,"width":0.062333778,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Spaces","depth":8,"bounds":{"left":0.16738696,"top":0.09976058,"width":0.016289894,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Spaces","depth":10,"bounds":{"left":0.16738696,"top":0.102553874,"width":0.016289894,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"/","depth":8,"bounds":{"left":0.18683511,"top":0.102553874,"width":0.0016622341,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Jiminny (New)","depth":8,"bounds":{"left":0.19165559,"top":0.09976058,"width":0.03174867,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Jiminny (New)","depth":10,"bounds":{"left":0.19165559,"top":0.102553874,"width":0.03174867,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Platform Team","depth":5,"bounds":{"left":0.16738696,"top":0.12210695,"width":0.045877658,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Platform Team","depth":6,"bounds":{"left":0.16738696,"top":0.12210695,"width":0.045877658,"height":0.019553073},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Add people","depth":5,"bounds":{"left":0.21525931,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Add people","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Board actions","depth":5,"bounds":{"left":0.2278923,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Board actions","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Share","depth":5,"bounds":{"left":0.44148937,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXMenuButton","text":"Automation","depth":5,"bounds":{"left":0.45478722,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give feedback","depth":5,"bounds":{"left":0.4680851,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Give feedback","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Enter full screen","depth":5,"bounds":{"left":0.48138297,"top":0.118914604,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Enter full screen","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Summary","depth":8,"bounds":{"left":0.16472739,"top":0.14764565,"width":0.035904255,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Summary","depth":10,"bounds":{"left":0.17603059,"top":0.15363128,"width":0.021276595,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Timeline","depth":8,"bounds":{"left":0.20196144,"top":0.14764565,"width":0.03357713,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Timeline","depth":10,"bounds":{"left":0.21326463,"top":0.15363128,"width":0.018949468,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Backlog","depth":8,"bounds":{"left":0.23686835,"top":0.14764565,"width":0.032413565,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Backlog","depth":10,"bounds":{"left":0.24817154,"top":0.15363128,"width":0.017785905,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Active sprints","depth":8,"bounds":{"left":0.2706117,"top":0.14764565,"width":0.045212764,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Active sprints","depth":10,"bounds":{"left":0.2819149,"top":0.15363128,"width":0.030585106,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Calendar","depth":8,"bounds":{"left":0.31715426,"top":0.14764565,"width":0.03474069,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Calendar","depth":10,"bounds":{"left":0.32845744,"top":0.15363128,"width":0.020113032,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Reports","depth":8,"bounds":{"left":0.35322472,"top":0.14764565,"width":0.031914894,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Reports","depth":10,"bounds":{"left":0.3645279,"top":0.15363128,"width":0.017287234,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"Testing Board","depth":8,"bounds":{"left":0.38646942,"top":0.14764565,"width":0.046708778,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Testing Board","depth":10,"bounds":{"left":0.3977726,"top":0.15363128,"width":0.030751329,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXLink","text":"List","depth":8,"bounds":{"left":0.43450797,"top":0.14764565,"width":0.02244016,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"link","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"List","depth":10,"bounds":{"left":0.44581118,"top":0.15363128,"width":0.0078125,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"12 more tabs","depth":6,"bounds":{"left":0.4582779,"top":0.14764565,"width":0.026097074,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":7,"bounds":{"left":0.4616024,"top":0.15363128,"width":0.011469414,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"9+","depth":8,"bounds":{"left":0.4758976,"top":0.15442938,"width":0.0051529254,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Add to navigation","depth":6,"bounds":{"left":0.48570478,"top":0.15083799,"width":0.007978723,"height":0.01915403},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"As you type to search or apply filters, the board updates with work items to match.","depth":6,"bounds":{"left":0.16738696,"top":0.23463687,"width":0.18134974,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXTextField","text":"Search on current page","depth":6,"bounds":{"left":0.17569813,"top":0.22067039,"width":0.050531916,"height":0.026735835},"on_screen":true,"placeholder":"Search board","role_description":"text field","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Filter by assignee","depth":7,"bounds":{"left":0.23121676,"top":0.22226655,"width":0.03873005,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXCheckBox","text":"Filter assignees by Lukas Kovalik","depth":6,"bounds":{"left":0.23254654,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Aneliya Angelova","depth":6,"bounds":{"left":0.24052526,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Nikolay Ivanov","depth":6,"bounds":{"left":0.24850398,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Nikolay Nikolov","depth":6,"bounds":{"left":0.25648272,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Nikolay Yankov","depth":6,"bounds":{"left":0.26446143,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Steliyan Georgiev","depth":6,"bounds":{"left":0.27244017,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Filter assignees by Unassigned","depth":6,"bounds":{"left":0.28041887,"top":0.22106944,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"checkbox","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXMenuButton","text":"Epic","depth":8,"bounds":{"left":0.29238698,"top":0.22106944,"width":0.0234375,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Epic","depth":11,"bounds":{"left":0.29637632,"top":0.22705507,"width":0.009474734,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Type","depth":8,"bounds":{"left":0.31848404,"top":0.22106944,"width":0.025099734,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Type","depth":11,"bounds":{"left":0.3224734,"top":0.22705507,"width":0.011136968,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Quick filters","depth":8,"bounds":{"left":0.34624335,"top":0.22106944,"width":0.040724736,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Quick filters","depth":11,"bounds":{"left":0.35023272,"top":0.22705507,"width":0.026761968,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Complete sprint","depth":5,"bounds":{"left":0.35106382,"top":0.18914606,"width":0.04338431,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Complete sprint","depth":7,"bounds":{"left":0.3550532,"top":0.19513169,"width":0.035405584,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Sprint details","depth":5,"bounds":{"left":0.39710772,"top":0.18914606,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Sprint details","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"Group by Queries","depth":5,"bounds":{"left":0.41040558,"top":0.18914606,"width":0.041722074,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Group","depth":8,"bounds":{"left":0.41439494,"top":0.19513169,"width":0.013796543,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":": Queries","depth":8,"bounds":{"left":0.42819148,"top":0.19513169,"width":0.019946808,"height":0.01396648},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Sprint insights","depth":5,"bounds":{"left":0.45478722,"top":0.18914606,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Sprint insights","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"View settings","depth":5,"bounds":{"left":0.4680851,"top":0.18914606,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"View settings","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXMenuButton","text":"More actions","depth":5,"bounds":{"left":0.48138297,"top":0.18914606,"width":0.010638298,"height":0.025538707},"on_screen":true,"help_text":"","role_description":"menu button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More actions","depth":7,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Ready For DEV","depth":11,"bounds":{"left":0.10571808,"top":0.27214685,"width":0.042220745,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"READY FOR DEV","depth":14,"bounds":{"left":0.10571808,"top":0.2725459,"width":0.03158245,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":17,"bounds":{"left":0.14178856,"top":0.2725459,"width":0.0016622341,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"In DEV","depth":11,"bounds":{"left":0.16173537,"top":0.27214685,"width":0.024601065,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"IN DEV","depth":14,"bounds":{"left":0.16173537,"top":0.2725459,"width":0.013962766,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"2","depth":17,"bounds":{"left":0.1796875,"top":0.2725459,"width":0.002493351,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Code Review","depth":11,"bounds":{"left":0.21758644,"top":0.27214685,"width":0.0390625,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"CODE REVIEW","depth":14,"bounds":{"left":0.21758644,"top":0.2725459,"width":0.028424202,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"1","depth":17,"bounds":{"left":0.25049868,"top":0.2725459,"width":0.0016622341,"height":0.011971269},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Blocked","depth":11,"bounds":{"left":0.27360374,"top":0.27214685,"width":0.018783245,"height":0.012769354},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"}]...
|
-3350859937341429173
|
5932699938285801646
|
visual_change
|
accessibility
|
NULL
|
Platform Sprint 4 Q2 - Platform Team - Scrum Board Platform Sprint 4 Q2 - Platform Team - Scrum Board - Jira
Service-Desk - Queues - Platform team - Service space - Jira
New Tab
New Tab
Login - SonarQube Cloud
Login - SonarQube Cloud
What's New in Firefox 151 — Firefox.com
What's New in Firefox 151 — Firefox.com
New Tab
New Tab
[JY-20814] Release unused Twilio numbers on Production - Jira
[JY-20814] Release unused Twilio numbers on Production - Jira
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
SevenShores\Hubspot\Exceptions\BadRequest: Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT
New Tab
New Tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Skip to:
Top Bar
Top Bar
Sidebar
Sidebar
Main Content
Main Content
Space navigation
Space navigation
Collapse sidebar [
Collapse sidebar [
Switch sites or apps
Switch sites or apps
Go to your Jira homepage
Search, press enter to navigate to advanced search with your text query
Create
Create
Rovo Ask Rovo
Ask Rovo
Notifications
Notifications
Help
Help
Settings
Settings
[EMAIL]
[EMAIL]
For you
For you
Recent
Recent
Starred
Starred
Apps
Apps
More actions for Apps
More actions for Apps
Spaces
Spaces
Create space
Create space
More actions for spaces
More actions for spaces
Recent
Jiminny (New)
Jiminny (New)
Jiminny (New)
Create board
Create board
More actions for Jiminny (New)
More actions for Jiminny (New)
Platform Team
Platform Team
Board actions
Board actions
Capture Team
Capture Team
Board actions
Board actions
Enterprise Stability Issues 🤕
Enterprise Stability Issues 🤕
Board actions
Board actions
Processing Team
Processing Team
Board actions
Board actions
SE Kanban
SE Kanban
Board actions
Board actions
Service-Desk
Service-Desk
More actions for Service-Desk
More actions for Service-Desk
More spaces
More spaces
Filters
Filters
More actions for Filters
More actions for Filters
Dashboards
Dashboards
Create dashboard
Create dashboard
More actions for Dashboards
More actions for Dashboards
Operations
Operations
More actions for Operations
More actions for Operations
Confluence , (opens new window)
Confluence
, (opens new window)
Teams , (opens new window)
Teams
, (opens new window)
open menu
open menu
Customise sidebar
Customise sidebar
Resize side navigation panel
Spaces
Spaces
/
Jiminny (New)
Jiminny (New)
Platform Team
Platform Team
Add people
Add people
Board actions
Board actions
Share
Automation
Give feedback
Give feedback
Enter full screen
Enter full screen
Summary
Summary
Timeline
Timeline
Backlog
Backlog
Active sprints
Active sprints
Calendar
Calendar
Reports
Reports
Testing Board
Testing Board
List
List
12 more tabs
More
9+
Add to navigation
As you type to search or apply filters, the board updates with work items to match.
Search on current page
Filter by assignee
Filter assignees by Lukas Kovalik
Filter assignees by Aneliya Angelova
Filter assignees by Nikolay Ivanov
Filter assignees by Nikolay Nikolov
Filter assignees by Nikolay Yankov
Filter assignees by Steliyan Georgiev
Filter assignees by Unassigned
Epic
Epic
Type
Type
Quick filters
Quick filters
Complete sprint
Complete sprint
Sprint details
Sprint details
Group by Queries
Group
: Queries
Sprint insights
Sprint insights
View settings
View settings
More actions
More actions
Ready For DEV
READY FOR DEV
1
In DEV
IN DEV
2
Code Review
CODE REVIEW
1
Blocked...
|
NULL
|
NULL
|
NULL
|
NULL
|