Files
leadchat/app/models/channel/telegram.rb
Sivin Varghese 399c91adaa feat: Standardize rich editor across all channels (#12600)
# Pull Request Template

## Description

This PR includes,

1. **Channel-specific formatting and menu options** for the rich reply
editor.
2. **Removal of the plain reply editor** and full **standardization** on
the rich reply editor across all channels.
3. **Fix for multiple canned responses insertion:**
* **Before:** The plain editor only allowed inserting canned responses
at the beginning of a message, making it impossible to combine multiple
canned responses in a single reply. This caused inconsistent behavior
across the app.
* **Solution:** Replaced the plain reply editor with the rich
(ProseMirror) editor to ensure a unified experience. Agents can now
insert multiple canned responses at any cursor position.
4. **Floating editor menu** for the reply box to improve accessibility
and overall user experience.
5. **New Strikethrough formatting option** added to the editor menu.

---

**Editor repo PR**:
https://github.com/chatwoot/prosemirror-schema/pull/36

Fixes https://github.com/chatwoot/chatwoot/issues/12517,
[CW-5924](https://linear.app/chatwoot/issue/CW-5924/standardize-the-editor),
[CW-5679](https://linear.app/chatwoot/issue/CW-5679/allow-inserting-multiple-canned-responses-in-a-single-message)

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Screenshot
**Dark**
<img width="850" height="345" alt="image"
src="https://github.com/user-attachments/assets/47748e6c-380f-44a3-9e3b-c27e0c830bd0"
/>

**Light**
<img width="850" height="345" alt="image"
src="https://github.com/user-attachments/assets/6746cf32-bf63-4280-a5bd-bbd42c3cbe84"
/>


## 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>
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
2025-12-08 14:43:45 +05:30

172 lines
5.6 KiB
Ruby

# == Schema Information
#
# Table name: channel_telegram
#
# id :bigint not null, primary key
# bot_name :string
# bot_token :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
#
# Indexes
#
# index_channel_telegram_on_bot_token (bot_token) UNIQUE
#
class Channel::Telegram < ApplicationRecord
include Channelable
# TODO: Remove guard once encryption keys become mandatory (target 3-4 releases out).
encrypts :bot_token, deterministic: true if Chatwoot.encryption_configured?
self.table_name = 'channel_telegram'
EDITABLE_ATTRS = [:bot_token].freeze
before_validation :ensure_valid_bot_token, on: :create
validates :bot_token, presence: true, uniqueness: true
before_save :setup_telegram_webhook
def name
'Telegram'
end
def telegram_api_url
"https://api.telegram.org/bot#{bot_token}"
end
def send_message_on_telegram(message)
message_id = send_message(message) if message.outgoing_content.present?
message_id = Telegram::SendAttachmentsService.new(message: message).perform if message.attachments.present?
message_id
end
def get_telegram_profile_image(user_id)
# get profile image from telegram
response = HTTParty.get("#{telegram_api_url}/getUserProfilePhotos", query: { user_id: user_id })
return nil unless response.success?
photos = response.parsed_response.dig('result', 'photos')
return if photos.blank?
get_telegram_file_path(photos.first.last['file_id'])
end
def get_telegram_file_path(file_id)
response = HTTParty.get("#{telegram_api_url}/getFile", query: { file_id: file_id })
return nil unless response.success?
"https://api.telegram.org/file/bot#{bot_token}/#{response.parsed_response['result']['file_path']}"
end
def process_error(message, response)
return unless response.parsed_response['ok'] == false
# https://github.com/TelegramBotAPI/errors/tree/master/json
message.external_error = "#{response.parsed_response['error_code']}, #{response.parsed_response['description']}"
message.status = :failed
message.save!
end
def chat_id(message)
message.conversation[:additional_attributes]['chat_id']
end
def business_connection_id(message)
message.conversation[:additional_attributes]['business_connection_id']
end
def reply_to_message_id(message)
message.content_attributes['in_reply_to_external_id']
end
private
def ensure_valid_bot_token
response = HTTParty.get("#{telegram_api_url}/getMe")
unless response.success?
errors.add(:bot_token, 'invalid token')
return
end
self.bot_name = response.parsed_response['result']['username']
end
def setup_telegram_webhook
HTTParty.post("#{telegram_api_url}/deleteWebhook")
response = HTTParty.post("#{telegram_api_url}/setWebhook",
body: {
url: "#{ENV.fetch('FRONTEND_URL', nil)}/webhooks/telegram/#{bot_token}"
})
errors.add(:bot_token, 'error setting up the webook') unless response.success?
end
def send_message(message)
response = message_request(
chat_id(message),
message.outgoing_content,
reply_markup(message),
reply_to_message_id(message),
business_connection_id: business_connection_id(message)
)
process_error(message, response)
response.parsed_response['result']['message_id'] if response.success?
end
def reply_markup(message)
return unless message.content_type == 'input_select'
{
one_time_keyboard: true,
inline_keyboard: message.content_attributes['items'].map do |item|
[{
text: item['title'],
callback_data: item['value']
}]
end
}.to_json
end
def convert_markdown_to_telegram_html(text)
# ref: https://core.telegram.org/bots/api#html-style
# Escape HTML entities first to prevent HTML injection
# This ensures only markdown syntax is converted, not raw HTML
escaped_text = CGI.escapeHTML(text)
# Parse markdown with extensions:
# - strikethrough: support ~~text~~
# - hardbreaks: preserve all newlines as <br>
html = CommonMarker.render_html(escaped_text, [:HARDBREAKS], [:strikethrough]).strip
# Convert paragraph breaks to double newlines to preserve them
# CommonMarker creates <p> tags for paragraph breaks, but Telegram doesn't support <p>
html_with_breaks = html.gsub(%r{</p>\s*<p>}, "\n\n")
# Remove opening and closing <p> tags
html_with_breaks = html_with_breaks.gsub(%r{</?p>}, '')
# Sanitize to only allowed tags
stripped_html = Rails::HTML5::SafeListSanitizer.new.sanitize(html_with_breaks, tags: %w[b strong i em u ins s strike del a code pre blockquote],
attributes: %w[href])
# Convert <br /> tags to newlines for Telegram
stripped_html.gsub(%r{<br\s*/?>}, "\n")
end
def message_request(chat_id, text, reply_markup = nil, reply_to_message_id = nil, business_connection_id: nil)
# text is already converted to HTML by MessageContentPresenter
business_body = {}
business_body[:business_connection_id] = business_connection_id if business_connection_id
HTTParty.post("#{telegram_api_url}/sendMessage",
body: {
chat_id: chat_id,
text: text,
reply_markup: reply_markup,
parse_mode: 'HTML',
reply_to_message_id: reply_to_message_id
}.merge(business_body))
end
end