A retried terminate webhook for an answered short call (duration=0)
would re-enter handle_terminate, find call.in_progress? false, fall
through to no_answer, and corrupt a completed call's status. Bail when
the call is already terminal so retries don't recompute the status.
Inbound WhatsApp calls were matching contacts by stripping `+` from the
raw from_number, bypassing the BR/AR wa_id normalization that
Whatsapp::IncomingMessageServiceHelpers#processed_waid runs on the
messaging path. A wa_id whose webhook variant differs from the stored
normalized source_id (notably Brazil's added "9") would miss the
existing ContactInbox and create a fresh contact/conversation,
fragmenting history. Route the voice path through the same
Whatsapp::PhoneNumberNormalizationService so the lookup is consistent.
Meta's Calls API documents `action: 'connect'` (alongside `pre_accept`,
`accept`, `reject`, `terminate`) as the required field on POST /calls
for business-initiated calls. The previous body used `type: 'audio'`,
which is the /messages media shape — Meta currently accepts it because
unknown fields are ignored, but that's an implicit contract that could
break the moment validation tightens.
Messenger ingest saves the Attachment row before attaching the blob, so
the after_create_commit broadcast bails on file.attached? and the FE
never gets the audio bubble until a refresh (or until Whisper finishes,
or never if transcription is disabled or returns blank). Re-fire the
message update after attach completes.
Reject conversation_id hints that point to a different contact in the
same voice inbox, so reuse only happens when the agent is genuinely
inside the open thread for the dialed contact.
Accept an optional conversation_id in POST /contacts/:id/call. When the
hint matches the picked voice inbox, OutboundCallBuilder reuses that
conversation instead of creating a new one — mirroring the WhatsApp
calling flow so the agent stays in the same thread.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
- config/locales/en.yml — kept the new
conversations.messages.voice_call.{twilio,whatsapp} keys this branch added.
- enterprise/app/services/enterprise/whatsapp/providers/whatsapp_cloud_service.rb
— kept this branch's calls_phone_id_path helper that pins to
WHATSAPP_API_VERSION (v22) for the Calls API endpoints. Develop's version
from PR #14312 used the OSS phone_id_path (v13) which doesn't support the
Calls API.
- Add 'calls' to the WhatsApp WABA override_callback subscribed_fields so a
fresh embedded-signup connection (or a re-register) automatically receives
call webhooks from Meta. Previously required a manual App Dashboard step.
- Gate Channel::Whatsapp#voice_enabled? on the account-level 'channel_voice'
feature flag, matching the entitlement Twilio voice channel creation
already requires. Cascades through the controller, webhook services, and
inbox jbuilder voice_enabled serialization (FE button hides automatically).
- Return 422 (not 200) from `WhatsappCallsController#initiate` permission
branch so clients can't mistake the opt-in template path for a
successful dial. Body shape unchanged (`{ status: ... }`).
- Drop the `file.attached?` guard from `enqueue_audio_transcription`.
The social-media ingest path (Messages::Messenger::MessageBuilder)
saves the Attachment before attaching the blob, and the guard was
silently dropping transcription for those audio messages.
AudioTranscriptionJob already has retry_on FileNotFoundError to ride
out the create-then-attach race.
## Description
* Added Meta webhook HMAC validation in meta_token_verify_concern.rb.
* Wired it into instagram_controller.rb and whatsapp_controller.rb.
* WhatsApp now verifies X-Hub-Signature-256 with WHATSAPP_APP_SECRET.
* Instagram now verifies with either FB_APP_SECRET or
INSTAGRAM_APP_SECRET.
* Updated request specs so missing/invalid signatures return 401 and
valid signatures still enqueue jobs.
Fixes # (issue):
[CW-6786](https://linear.app/chatwoot/issue/CW-6786/ghsa-7rw7-pc8v-mrr3-unauthenticated-message-injection-via-missing)
## Type of change
Please delete options that are not relevant.
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
* Updated the controller specs and ran them successfully.
* The original issue is no longer reproducible.
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
The legacy-row stub for Channel::Voice belongs with the upstream
DropChannelVoice migration cleanup, not with WhatsApp call wiring.
Removing it keeps the PR's surface focused on the call pipeline.
# Pull Request Template
## Description
Captain (v1) makes false promises by saying it will handoff but doesn't.
This happens due to an exact string match comparison and the prompt
gives the model a lot of responsibilities:
- identity
- what to respond
- obey custom instructions
- decide on tool calls
This PR decouples responsibility, the core prompt responds, and an
additional llm call evaluates if handoff was needed or not after that
message.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.
Locally
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
# Pull Request Template
## Description
Fixes
https://linear.app/chatwoot/issue/CW-6903/signature-delimiter-renders-as-h2-when-using-enter-line-before
**1**. Fixes an issue where the signature delimiter `--` gets parsed as
an H2 when using **Enter** (new paragraph) before or after it, causing
it to render as a bold `\` in the message bubble.
* Ensures `--` renders as plain text
* Aligns renderer with parser behavior (both disable `lheading`)
* Prevents stray `\` from appearing as heading text
**2**. Also fixes a related editor issue where toggling signature
**off** leaves behind a stray `\` or `-- \`.
* Strips blank paragraph markers (`\`) and dangling hard breaks
(`\<newline>`) from ProseMirror serializer
* Applied in both `appendSignature` and `removeSignature`
* Replaces `trimEnd()` with shared helpers (`trimTrailingBlanks` /
`stripTrailingBlankMarkers`)
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
#### Screenshots
**Before**
<img width="194" height="204" alt="image"
src="https://github.com/user-attachments/assets/b286ab50-7f89-4910-a552-1568902b93b3"
/>
**After**
<img width="194" height="220" alt="image"
src="https://github.com/user-attachments/assets/658cd543-bce2-46e2-a319-35e5374f1aef"
/>
**Editor**
https://linear.app/chatwoot/issue/CW-6903/signature-delimiter-renders-as-in-h2-when-using-enter-line#comment-5814b882
### Steps
#### Editor
1. Enable agent signature
2. Add and remove new lines around the signature using Enter/shift enter
3. Toggle signature off
4. Notice stray `\` or `-- \` remains
#### Bubble
1. Enable agent signature
2. Send a message using Enter between lines
3. Verify `--` renders correctly (no H2, no bold `\`)
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
# Pull Request Template
## Description
This PR fixes multiple issues related to regex patterns and validation
for custom attributes.
1. Fixed regex patterns being double-escaped when saving from Add and
Edit flows
2. Fixed regex validation not being enforced in the widget pre-chat form
3. Minor UI improvements in the Add/Edit custom attribute dialog
Fixes
[CW-6625](https://linear.app/chatwoot/issue/CW-6625/bug-report-custom-attribute-regex-validation-not-working-in-ui),
https://github.com/chatwoot/chatwoot/issues/13771
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
**Loom video**
**Before**
https://www.loom.com/share/14f1983a8bc84f9fabc3663afd83cd50
**After**
https://www.loom.com/share/867c0484741140c1944fcbd43914c9c0
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
Meta delivers two payload shapes under field=calls: value.calls[] (event:
connect/terminate) and value.statuses[] (status: RINGING/ACCEPTED). The
dispatcher only forwarded the calls[] array, so status webhooks were
silently dropped.
Empirically `connect` for BUSINESS_INITIATED outbound fires when the
WebRTC tunnel is up, not on pickup — sometimes ~20s before the contact
actually answers. The terminate webhook's start_time aligns with the
ACCEPTED status timestamp, confirming ACCEPTED is the true pickup.
- whatsapp_events_job: route value.statuses[] (type=call) to
IncomingCallService under the same per-call_id mutex.
- incoming_call_service: handle_status routes ACCEPTED to
mark_outbound_accepted (flip in_progress, set started_at, broadcast
voice_call.outbound_accepted). Strip the in_progress flip from the
connect handler — connect now only stores the SDP answer + broadcasts
voice_call.outbound_connected so the browser can complete the DTLS
handshake during ringing.
Emit a conversation activity message when the call permission template is
sent (in the controller's render_permission_request) and another when the
contact accepts the prompt (in CallPermissionReplyService). Read an inbox
provider_config['call_permission_request_body'] override before falling
back to the i18n default — surfaced in the inbox UI in a separate change.
Two production-grade fixes to the existing audio transcription service.
**Independent of the WhatsApp Calling work** — these affect every audio
attachment that goes through Whisper (voice notes, call recordings,
voicemails, etc.).
## Closes
- [PLA-151 — PR-5: Recording Upload + Transcription
Pipeline](https://linear.app/chatwoot/issue/PLA-151/pr-5-recording-upload-transcription-pipeline)
## Why this is needed
### 1. Whisper rejects payloads larger than 25 MB
OpenAI's [Whisper
API](https://platform.openai.com/docs/guides/speech-to-text) hard-caps
file uploads at 25 MB. Long audio recordings — voice notes from chatty
contacts, ~70+ min Opus call recordings — currently hit OpenAI with the
full payload and 413 (\`Payload Too Large\`). The job retries via the
existing \`Faraday::BadRequestError\` discard path, but the agent still
sees a transcription failure for an attachment we knew was too big up
front.
This PR adds a pre-flight \`audio_too_large?\` check via the blob's
\`byte_size\` and returns a controlled error without hitting OpenAI. The
audio attachment is preserved (agents can still listen), only the
transcription is skipped.
### 2. Whisper hallucinates on silence at non-zero temperature
At \`temperature: 0.4\` (the previous value), Whisper produces
well-documented hallucinated repeats on silence and near-silent segments
— e.g. \`Oh, dear. Oh, dear. Oh, dear.\` filling the transcript. This
shows up in real recordings whenever there's a hold or quiet moment.
\`temperature: 0.0\` matches OpenAI's recommended default for
transcription and eliminates the spirals.
Reference:
[openai/whisper#928](https://github.com/openai/whisper/discussions/928),
[openai-python#1010](https://github.com/openai/openai-python/issues/1010).
## Are WhatsApp call recordings already handled?
Yes — by the existing pipeline, **before this PR**:
\`\`\`
Browser MediaRecorder → upload_recording (PR-4)
→ @call.message.attachments.create!(file_type: :audio, ...)
→ Enterprise::Concerns::Attachment#enqueue_audio_transcription
(after_create_commit hook)
→ Messages::AudioTranscriptionJob.perform_later(attachment.id)
→ Messages::AudioTranscriptionService → Whisper
\`\`\`
The \`after_create_commit\` hook already fires for every audio
attachment regardless of source. PR-4's \`upload_recording\` endpoint
creates the attachment; the existing job/service take it from there. No
new wiring needed.
This PR just makes the existing service more robust:
- Calls longer than ~70 min (Opus 48 kbps) no longer 413 against OpenAI
- Quiet recordings no longer produce hallucinated transcripts
## How to test
\`\`\`ruby
# In rails console with a real audio attachment:
service = Messages::AudioTranscriptionService.new(Attachment.audio.last)
# Normal-sized audio: unchanged behaviour
service.perform # => { success: true, transcriptions: ... }
# Large audio: new guard returns error instead of 413-ing OpenAI
allow(attachment.file.blob).to
receive(:byte_size).and_return(30.megabytes)
service.perform # => { error: 'Audio too large for Whisper' }
\`\`\`
Existing transcription specs cover the happy path; one new spec
exercises the byte-limit guard.
## Risk
Low. Both changes are pre-flight guards or parameter values — they
reduce the surface of OpenAI calls that can fail. Failure to transcribe
is already non-fatal (the audio attachment is preserved either way).
When an inbound voice call ends, the conversation bubble now (1) renders
an inline audio player as soon as Twilio finishes the recording and (2)
shows the call duration alongside "Call ended" so the agent gets the
at-a-glance summary without opening the recording.
Fixes
https://linear.app/chatwoot/issue/PLA-118/feat-recordings-on-calls-should-be-attached-on-the-conversation
and
https://linear.app/chatwoot/issue/PLA-119/duration-of-the-call-is-not-visible-on-the-chat-bubble
## How to test
1. Set up a Twilio voice inbox and trigger an inbound call.
2. Answer the call from an agent, talk for a few seconds, then hang up.
3. As soon as the call ends, the bubble should read **"Call ended —
0:NN"** (where NN is the call duration in seconds).
4. Wait a few seconds for Twilio to finish processing the recording
(usually <30s after hangup).
5. The same bubble should now show an inline audio player below the
duration. Press play; the recording should be audible.
6. Refresh the page — both the duration and the player should still be
there.
7. End a second call on the same conversation — its bubble should get
its own duration + player, independent of the first.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
- Fix CodeQL alerts by declaring read-only GITHUB_TOKEN scope at the
workflow level. The codespace image publish workflow additionally needs
packages: write to push to ghcr.io.
Wires Meta's WhatsApp Cloud Calling APIs into the voice subsystem so
agents can answer / place calls on WhatsApp Cloud inboxes from the
existing Voice flow. Browser ↔ Meta WebRTC is direct (no media-server
hop); Chatwoot owns the signalling, recording upload, and lifecycle
state on the Call/Message records.
Backend
- API surface: new Api::V1::Accounts::WhatsappCallsController with
show / accept / reject / terminate / initiate / upload_recording.
Enforces conversation-level Pundit visibility, blocks initiate when
the channel isn't embedded-signup voice-enabled, and surfaces the
Meta 138006 (no-call-permission) flow as a throttled opt-in template
send under a conversation lock so concurrent retries can't double-send.
- Whatsapp::CallService — accept/reject/terminate state machine with
call-level locking; wraps Meta API failures (transport or business)
as Voice::CallErrors::CallFailed so the controller renders 422.
- Whatsapp::IncomingCallService — handles Meta connect/terminate
webhooks; pins setup:active on outbound answers, materialises a
missed-call record when terminate arrives before connect, and
broadcasts voice_call.* events to assignee or account streams.
- Whatsapp::Providers::WhatsappCloudService — adds pre_accept/accept/
reject/terminate/initiate/permission-request endpoints. Uses
configurable WHATSAPP_API_VERSION (default v22) since the Calls API
needs v17+ and OSS phone_id_path is locked at v13.
- Audio recordings: Attachment after_create_commit enqueues
transcription and rebroadcasts the message so the FE bubble updates
immediately. Active Storage initializer allows audio MIME types to
serve inline. Audio transcription drops Whisper temperature to 0 to
suppress hallucinations on silence.
- Channel::Voice stub backs legacy inbox rows whose channel_type
survived the DropChannelVoice migration so jbuilder .try chains
short-circuit instead of raising.
Routes / config
- /api/v1/accounts/:id/whatsapp_calls/* (enterprise-only)
- en.yml error strings under errors.whatsapp.calls.*
The FE consumer of this API ships separately on feat/whatsapp-call-ui.
Agents can now click **Join call** directly on the incoming call bubble
in the conversation timeline. If they refresh the page or miss the
floating widget while a call is still ringing, the bubble becomes the
recovery affordance — one click joins the conference, no need to wait
for the next event.
The button only appears when the call is still ringing, no other agent
has claimed it, and the conversation is unassigned or assigned to the
current agent (mirroring the floating widget's eligibility rules). It
disappears as soon as anyone joins the call or it ends.
Fixes
https://linear.app/chatwoot/issue/PLA-117/ability-to-join-the-call-by-clicking-on-call-bubble-in-a-conversation
## How to test
1. Set up a Twilio voice inbox and trigger an inbound call to it.
2. As an agent who is eligible to answer (unassigned conversation, or
assigned to you), open the conversation **without answering from the
floating widget**. The bubble should show a teal **Join call** link
under "Not answered yet".
3. Refresh the page mid-ring — the link should still be there.
4. Click **Join call** — you should be connected to the conference, the
bubble should flip to "Call in progress / You answered", and the link
should disappear.
5. As a second agent who is **not** eligible (conversation assigned to
someone else), open the same conversation — the link should not appear.
6. Wait for the call to end — the bubble should show "Call ended" with
no Join link.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
This PR limits IMAP email fetching to 500 messages per sync run to avoid
expensive/long-running mailbox scans. It also filters out
already-imported emails and Chatwoot-generated notification emails
during the header fetch phase, before fetching full email bodies,
reducing unnecessary IMAP work.
Fixes #CW-7001 (issue) :
https://linear.app/chatwoot/issue/CW-7001/emails-not-syncing
# Pull Request Template
## Description
This PR fixes an issue where agent variables like
`{{agent.name}}`,`{{agent.first_name}}`, `{{agent.last_name}}`, and
`{{agent.email}}` were not rendering in automation messages.
In automation, these either showed blank or returned `Liquid error:
internal`, while the same variables worked fine in macros.
**Cause**
Automation messages are created without a sender, so agent data was
missing during variable rendering. This also caused errors in name
handling, and `email` was not defined at all.
**Solution**
* Handle missing agent data safely to avoid errors
* Add support for `{{agent.email}}`
* Fallback to conversation assignee when sender is not present
Fixes
https://linear.app/chatwoot/issue/CW-6979/template-variables-not-working-in-automated-messages
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Screenshots
**Automation**
<img width="759" height="284" alt="image"
src="https://github.com/user-attachments/assets/61a877b7-4984-4a7f-bbef-b8c510dcbdfe"
/>
**Before**
<img width="404" height="105" alt="image"
src="https://github.com/user-attachments/assets/da665ce8-137d-4249-8ee5-a1acc11391db"
/>
**After**
<img width="564" height="132" alt="image"
src="https://github.com/user-attachments/assets/6a80d67c-49c8-4658-b782-ae4acbc77256"
/>
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
Adds the Meta WhatsApp Cloud API surface needed for browser-based
calling. This is the second slice of the WhatsApp calling feature,
sitting on top of `feat/voice-call-model-wiring` and consumed by later
PRs (incoming-webhook pipeline, call service, frontend).
This PR ships only the provider-level HTTP wrapper and one error class.
It is feature-flag-free and does not change any user-visible behaviour
on its own — without later PRs, no caller invokes these methods.
## Linear
-
https://linear.app/chatwoot/issue/PLA-148/pr-2-meta-cloud-api-provider-methods
## What changed
- Add `Whatsapp::Providers::WhatsappCloudCallMethods`
(`enterprise/app/services/whatsapp/providers/whatsapp_cloud_call_methods.rb`)
wrapping six Meta endpoints:
- `pre_accept_call`, `accept_call`, `reject_call`, `terminate_call` —
`POST /{phone_id}/calls` with the relevant action payload.
- `send_call_permission_request` — `POST /{phone_id}/messages`
interactive `call_permission_request`.
- `initiate_call` — `POST /{phone_id}/calls` with `audio`/`offer`
session.
- Prepend the module into `Whatsapp::Providers::WhatsappCloudService`
only if defined, so OSS continues to work without the enterprise
overlay.
- Add `Voice::CallErrors::NoCallPermission`
(`enterprise/lib/voice/call_errors.rb`) — raised when Meta returns error
code `138006` from `initiate_call`. The remaining call-service errors
(`NotRinging`, `AlreadyAccepted`, `CallFailed`) will land with PR-4.
## How to test
There is no UI in this PR. Smoke-test from a Rails console with a
WhatsApp inbox configured for calling:
```ruby
inbox = Inbox.find(<id>)
svc = inbox.channel.provider_service
svc.respond_to?(:initiate_call) # => true
svc.respond_to?(:send_call_permission_request) # => true
# Optional live calls (require a real phone + Meta call-permission opt-in):
svc.send_call_permission_request('15551234567')
svc.initiate_call('15551234567', '<sdp_offer>')
```
Failure path: `initiate_call` against a contact who has not granted call
permission should raise `Voice::CallErrors::NoCallPermission` with
Meta's user-facing message.
When an agent shares a conversation link copied from a custom view (e.g.
/custom_view/{id}/conversations/{id}), the link previously broke for
recipients who didn't have access to that custom view. The conversation
now loads regardless — if the custom view isn't available to the
recipient, they're redirected to the direct conversation URL.
### How to reproduce
1. As Agent A, open a conversation from inside a personal custom view
and copy the URL from the address bar.
2. Share the URL with Agent B who does not have access to that custom
view.
3. Before this fix, the link failed to load the conversation. After this
fix, Agent B lands on the conversation via the direct URL.
### What changed
- Added a beforeEnter guard on the conversations_through_folders route.
It checks the user's available conversation custom views (fetching them
on demand for deep links), and if the foldersId in the URL isn't among
them, redirects to the inbox_conversation route with the same
conversation_id.
---------
Co-authored-by: iamsivin <iamsivin@gmail.com>
### Description
Inbound voice calls now route ownership cleanly: the call widget is
hidden from agents who aren't the conversation assignee, the first agent
to pick up becomes the assignee, and any later join attempt by another
agent is rejected with a clear "<agent> is already handling the call."
alert.
Closes
https://linear.app/chatwoot/issue/PLA-98/inbound-voice-calls-assignment-aware-visibility-auto-assignment-on
### How to test
1. As Agent A and Agent B, open the dashboard for the same voice inbox
in two browsers.
2. Place an inbound call to the inbox with the conversation
**unassigned** — both agents should see the call widget.
3. Have Agent A click **Join**. Agent A's widget transitions to the
active call; Agent B's widget disappears (conversation is now assigned
to Agent A).
4. While the call is in progress, attempt to join from a third agent
(e.g., via the bubble in the conversation timeline) — the join is
rejected with the toast `Agent A is already handling the call.`
5. Resolve the conversation, then place a second call to a conversation
that is already manually assigned to Agent A — only Agent A sees the
widget; nobody else does.
6. Race test: trigger two near-simultaneous join attempts (two agents
click Join within a few hundred ms of each other) — exactly one wins;
the other gets the conflict alert.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>