# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
require 'telegram/bot'
class TelegramHelper
attr_accessor :client
=begin
check token and return bot attributes of token
bot = TelegramHelper.check_token('token')
=end
def self.check_token(token)
result = nil
begin
Telegram::Bot::Client.run(token) do |bot|
result = bot.api.getMe
end
rescue
raise Exceptions::UnprocessableEntity, 'invalid api token'
end
result
end
=begin
set webhook for bot
success = TelegramHelper.set_webhook('token', callback_url)
returns
true|false
=end
def self.set_webhook(token, callback_url)
if callback_url.match?(%r{^http://}i)
raise Exceptions::UnprocessableEntity, __('The Telegram integration can only be configured on systems using the HTTPS protocol.')
end
begin
Telegram::Bot::Client.run(token) do |bot|
bot.api.setWebhook(url: callback_url)
end
rescue
raise Exceptions::UnprocessableEntity, __('The webhook could not be saved by Telegram, seems to be an invalid URL.')
end
true
end
=begin
create or update channel, store bot attributes and verify token
channel = TelegramHelper.create_or_update_channel('token', params)
returns
channel # instance of Channel
=end
def self.create_or_update_channel(token, params, channel = nil)
# verify token
bot = check_token(token)
if !channel && bot_duplicate?(bot.id)
raise Exceptions::UnprocessableEntity, __('This bot already exists.')
end
if params[:group_id].blank?
raise Exceptions::UnprocessableEntity, __("The required parameter 'group_id' is missing.")
end
group = Group.find_by(id: params[:group_id])
if !group
raise Exceptions::UnprocessableEntity, __("The required parameter 'group_id' is invalid.")
end
# generate random callback token
callback_token = if Rails.env.test?
'callback_token'
else
SecureRandom.urlsafe_base64(10)
end
# set webhook / callback url for this bot @ telegram
callback_url = "#{Setting.get('http_type')}://#{Setting.get('fqdn')}/api/v1/channels_telegram_webhook/#{callback_token}?bid=#{bot.id}"
set_webhook(token, callback_url)
if !channel
channel = bot_by_bot_id(bot.id)
if !channel
channel = Channel.new
end
end
channel.area = 'Telegram::Bot'
channel.options = {
bot: {
id: bot.id,
username: bot.username,
first_name: bot.first_name,
last_name: bot.last_name,
},
callback_token: callback_token,
callback_url: callback_url,
api_token: token,
welcome: params[:welcome],
goodbye: params[:goodbye],
}
channel.group_id = group.id
channel.active = true
channel.save!
channel
end
=begin
check if bot already exists as channel
success = TelegramHelper.bot_duplicate?(bot_id)
returns
channel # instance of Channel
=end
def self.bot_duplicate?(bot_id, channel_id = nil)
Channel.where(area: 'Telegram::Bot').each do |channel|
next if !channel.options
next if !channel.options[:bot]
next if !channel.options[:bot][:id]
next if channel.options[:bot][:id] != bot_id
next if channel.id.to_s == channel_id.to_s
return true
end
false
end
=begin
get channel by bot_id
channel = TelegramHelper.bot_by_bot_id(bot_id)
returns
true|false
=end
def self.bot_by_bot_id(bot_id)
Channel.where(area: 'Telegram::Bot').each do |channel|
next if !channel.options
next if !channel.options[:bot]
next if !channel.options[:bot][:id]
return channel if channel.options[:bot][:id].to_s == bot_id.to_s
end
nil
end
=begin
generate message_id for message
message_id = TelegramHelper.message_id(message)
returns
message_id # 123456@telegram
=end
def self.message_id(params)
message_id = nil
%i[message edited_message].each do |key|
next if !params[key]
next if !params[key][:message_id]
message_id = params[key][:message_id]
break
end
if message_id
%i[message edited_message].each do |key|
next if !params[key]
next if !params[key][:chat]
next if !params[key][:chat][:id]
message_id = "#{message_id}.#{params[key][:chat][:id]}"
end
end
if !message_id
message_id = params[:update_id]
end
"#{message_id}@telegram"
end
=begin
client = TelegramHelper.new('token')
=end
def initialize(token)
@token = token
end
=begin
client.message(chat_id, 'some message', language_code)
=end
def message(chat_id, message, language_code = 'en')
return if Rails.env.test?
locale = Locale.find_by(alias: language_code)
if !locale
locale = Locale.where('locale LIKE :prefix', prefix: "#{SqlHelper.quote_like(language_code)}%").first
end
if locale
message = Translation.translate(locale[:locale], message)
end
Telegram::Bot::Client.run(@token) do |bot|
bot.api.sendMessage(chat_id: chat_id, text: message)
end
end
def user(params)
{
id: params[:message][:from][:id],
username: params[:message][:from][:username],
first_name: params[:message][:from][:first_name],
last_name: params[:message][:from][:last_name]
}
end
def to_user(params)
Rails.logger.debug { 'Create user from message...' }
Rails.logger.debug { params.inspect }
# do message_user lookup
message_user = user(params)
auth = Authorization.find_by(uid: message_user[:id], provider: 'telegram')
# create or update user
login = message_user[:username] || message_user[:id]
user_data = {
login: login,
firstname: message_user[:first_name],
lastname: message_user[:last_name],
}
if auth
user = User.find(auth.user_id)
user.update!(user_data)
else
if message_user[:username]
user_data[:note] = "Telegram @#{message_user[:username]}"
end
user_data[:active] = true
user_data[:role_ids] = Role.signup_role_ids
user = User.create(user_data)
end
# create or update authorization
auth_data = {
uid: message_user[:id],
username: login,
user_id: user.id,
provider: 'telegram'
}
if auth
auth.update!(auth_data)
else
Authorization.create(auth_data)
end
user
end
def to_ticket(params, user, group_id, channel)
UserInfo.current_user_id = user.id
Rails.logger.debug { 'Create ticket from message...' }
Rails.logger.debug { params.inspect }
Rails.logger.debug { user.inspect }
Rails.logger.debug { group_id.inspect }
# prepare title
title = '-'
%i[text caption].each do |area|
next if !params[:message]
next if !params[:message][area]
title = params[:message][area]
break
end
if title == '-'
%i[sticker photo document voice].each do |area|
next if !params[:message]
next if !params[:message][area]
next if !params[:message][area][:emoji]
title = params[:message][area][:emoji]
break
rescue
# just go ahead
title
end
end
if title.length > 60
title = "#{title[0, 60]}..."
end
# find ticket or create one
state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
possible_tickets = Ticket.where(customer_id: user.id).where.not(state_id: state_ids).reorder(:updated_at)
ticket = possible_tickets.find_each.find { |possible_ticket| possible_ticket.preferences[:channel_id] == channel.id }
if ticket
# check if title needs to be updated
if ticket.title == '-'
ticket.title = title
end
new_state = Ticket::State.find_by(default_create: true)
if ticket.state_id != new_state.id
ticket.state = Ticket::State.find_by(default_follow_up: true)
end
ticket.save!
return ticket
end
ticket = Ticket.new(
group_id: group_id,
title: title,
state_id: Ticket::State.find_by(default_create: true).id,
priority_id: Ticket::Priority.find_by(default_create: true).id,
customer_id: user.id,
preferences: {
channel_id: channel.id,
telegram: {
bid: params['bid'],
chat_id: params[:message][:chat][:id]
}
},
)
ticket.save!
ticket
end
def to_article(params, user, ticket, channel, article = nil)
if article
Rails.logger.debug { 'Update article from message...' }
else
Rails.logger.debug { 'Create article from message...' }
end
Rails.logger.debug { params.inspect }
Rails.logger.debug { user.inspect }
Rails.logger.debug { ticket.inspect }
UserInfo.current_user_id = user.id
if article
article.preferences[:edited_message] = {
message: {
created_at: params[:message][:date],
message_id: params[:message][:message_id],
from: params[:message][:from],
},
update_id: params[:update_id],
}
else
article = Ticket::Article.new(
ticket_id: ticket.id,
type_id: Ticket::Article::Type.find_by(name: 'telegram personal-message').id,
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
from: user(params)[:username],
to: "@#{channel[:options][:bot][:username]}",
message_id: TelegramHelper.message_id(params),
internal: false,
preferences: {
message: {
created_at: params[:message][:date],
message_id: params[:message][:message_id],
from: params[:message][:from],
},
update_id: params[:update_id],
}
)
end
# add photo
if params[:message][:photo]
# find photo with best resolution for us
photo = nil
max_width = 650 * 2
last_width = 0
last_height = 0
params[:message][:photo].each do |file|
if !photo
photo = file
last_width = file['width'].to_i
last_height = file['height'].to_i
end
next if file['width'].to_i >= max_width || file['width'].to_i <= last_width
photo = file
last_width = file['width'].to_i
last_height = file['height'].to_i
end
if last_width > 650
last_width = (last_width / 2).to_i
last_height = (last_height / 2).to_i
end
# download photo
photo_result = get_file(params, photo)
body = ""
if params[:message][:caption]
body += "
#{params[:message][:caption].text2html}"
end
article.content_type = 'text/html'
article.body = body
article.save!
return article
end
# add document
if params[:message][:document]
document = params[:message][:document]
thumb = params[:message][:document][:thumb]
body = ' '
if thumb
width = thumb[:width]
height = thumb[:height]
thumb_result = get_file(params, thumb)
body = ""
end
if params[:message][:caption]
body += "
#{params[:message][:caption].text2html}"
end
document_result = get_file(params, document)
article.content_type = 'text/html'
article.body = body
article.save!
Store.remove(
object: 'Ticket::Article',
o_id: article.id,
)
Store.create!(
object: 'Ticket::Article',
o_id: article.id,
data: document_result.body,
filename: document[:file_name],
preferences: {
'Mime-Type' => document[:mime_type],
},
)
return article
end
# add video
if params[:message][:video]
video = params[:message][:video]
thumb = params[:message][:video][:thumb]
body = ' '
if thumb
width = thumb[:width]
height = thumb[:height]
thumb_result = get_file(params, thumb)
body = ""
end
if params[:message][:caption]
body += "
#{params[:message][:caption].text2html}"
end
video_result = get_file(params, video)
article.content_type = 'text/html'
article.body = body
article.save!
Store.remove(
object: 'Ticket::Article',
o_id: article.id,
)
# get video type
type = video[:mime_type].gsub(%r{(.+/)}, '')
Store.create!(
object: 'Ticket::Article',
o_id: article.id,
data: video_result.body,
filename: video[:file_name] || "video-#{video[:file_id]}.#{type}",
preferences: {
'Mime-Type' => video[:mime_type],
},
)
return article
end
# add voice
if params[:message][:voice]
voice = params[:message][:voice]
body = ' '
if params[:message][:caption]
body = "
#{params[:message][:caption].text2html}"
end
document_result = get_file(params, voice)
article.content_type = 'text/html'
article.body = body
article.save!
Store.remove(
object: 'Ticket::Article',
o_id: article.id,
)
Store.create!(
object: 'Ticket::Article',
o_id: article.id,
data: document_result.body,
filename: voice[:file_path] || "audio-#{voice[:file_id]}.ogg",
preferences: {
'Mime-Type' => voice[:mime_type],
},
)
return article
end
# add sticker
if params[:message][:sticker]
sticker = params[:message][:sticker]
emoji = sticker[:emoji]
thumb = sticker[:thumb]
body = ' '
if thumb
width = thumb[:width]
height = thumb[:height]
thumb_result = get_file(params, thumb)
body = ""
article.content_type = 'text/html'
elsif emoji
article.content_type = 'text/plain'
body = emoji
end
article.body = body
article.save!
if sticker[:file_id]
document_result = get_file(params, sticker)
Store.remove(
object: 'Ticket::Article',
o_id: article.id,
)
Store.create!(
object: 'Ticket::Article',
o_id: article.id,
data: document_result.body,
filename: sticker[:file_name] || "#{sticker[:set_name]}.webp",
preferences: {
'Mime-Type' => 'image/webp', # mime type is not given from Telegram API but this is actually WebP
},
)
end
return article
end
# add text
if params[:message][:text]
article.content_type = 'text/plain'
article.body = params[:message][:text]
article.save!
return article
end
raise Exceptions::UnprocessableEntity, 'invalid telegram message'
end
def to_group(params, group_id, channel)
# begin import
Rails.logger.debug { 'import message' }
# map channel_post params to message
if params[:channel_post]
return if params[:channel_post][:new_chat_title] # happens when channel title is renamed, we use [:chat][:title] already, safely ignore this.
# NOTE: used .blank? which is a rails method. empty? does not work on integers (values like date, width, height) to check.
# need delete_if to remove any empty hashes, .compact only removes keys with nil values.
params[:message] = {
document: {
file_name: params.dig(:channel_post, :document, :file_name),
mime_type: params.dig(:channel_post, :document, :mime_type),
file_id: params.dig(:channel_post, :document, :file_id),
file_size: params.dig(:channel_post, :document, :file_size),
thumb: {
file_id: params.dig(:channel_post, :document, :thumb, :file_id),
file_size: params.dig(:channel_post, :document, :thumb, :file_size),
width: params.dig(:channel_post, :document, :thumb, :width),
height: params.dig(:channel_post, :document, :thumb, :height)
}.compact
}.compact_blank!,
video: {
duration: params.dig(:channel_post, :video, :duration),
width: params.dig(:channel_post, :video, :width),
height: params.dig(:channel_post, :video, :height),
mime_type: params.dig(:channel_post, :video, :mime_type),
file_id: params.dig(:channel_post, :video, :file_id),
file_size: params.dig(:channel_post, :video, :file_size),
thumb: {
file_id: params.dig(:channel_post, :video, :thumb, :file_id),
file_size: params.dig(:channel_post, :video, :thumb, :file_size),
width: params.dig(:channel_post, :video, :thumb, :width),
height: params.dig(:channel_post, :video, :thumb, :height)
}.compact
}.compact_blank!,
voice: {
duration: params.dig(:channel_post, :voice, :duration),
mime_type: params.dig(:channel_post, :voice, :mime_type),
file_id: params.dig(:channel_post, :voice, :file_id),
file_size: params.dig(:channel_post, :voice, :file_size)
}.compact,
sticker: {
width: params.dig(:channel_post, :sticker, :width),
height: params.dig(:channel_post, :sticker, :height),
emoji: params.dig(:channel_post, :sticker, :emoji),
set_name: params.dig(:channel_post, :sticker, :set_name),
file_id: params.dig(:channel_post, :sticker, :file_id),
file_path: params.dig(:channel_post, :sticker, :file_path),
file_size: params.dig(:channel_post, :sticker, :file_size),
thumb: {
file_id: params.dig(:channel_post, :sticker, :thumb, :file_id),
file_size: params.dig(:channel_post, :sticker, :thumb, :file_size),
width: params.dig(:channel_post, :sticker, :thumb, :width),
height: params.dig(:channel_post, :sticker, :thumb, :height),
file_path: params.dig(:channel_post, :sticker, :thumb, :file_path)
}.compact
}.compact_blank!,
chat: {
id: params.dig(:channel_post, :chat, :id),
first_name: params.dig(:channel_post, :chat, :title),
last_name: 'Channel',
username: "channel#{params.dig(:channel_post, :chat, :id)}"
},
from: {
id: params.dig(:channel_post, :chat, :id),
first_name: params.dig(:channel_post, :chat, :title),
last_name: 'Channel',
username: "channel#{params.dig(:channel_post, :chat, :id)}"
},
caption: params.dig(:channel_post, :caption) || {},
date: params.dig(:channel_post, :date),
message_id: params.dig(:channel_post, :message_id),
text: params.dig(:channel_post, :text),
photo: (params[:channel_post][:photo].map { |photo| { file_id: photo[:file_id], file_size: photo[:file_size], width: photo[:width], height: photo[:height] } } if params.dig(:channel_post, :photo))
}.compact_blank!
params.delete(:channel_post) # discard unused :channel_post hash
end
# checks if the channel post is being edited, and map it when it is
if params[:edited_channel_post]
# updates on telegram can only be on messages, no attachments
params[:edited_message] = {
chat: {
id: params.dig(:edited_channel_post, :chat, :id),
first_name: params.dig(:edited_channel_post, :chat, :title),
last_name: 'Channel',
username: "channel#{params.dig(:edited_channel_post, :chat, :id)}"
},
from: {
id: params.dig(:edited_channel_post, :chat, :id),
first_name: params.dig(:edited_channel_post, :chat, :title),
last_name: 'Channel',
username: "channel#{params.dig(:edited_channel_post, :chat, :id)}"
},
date: params.dig(:edited_channel_post, :date),
edit_date: params.dig(:edited_channel_post, :edit_date),
message_id: params.dig(:edited_channel_post, :message_id),
text: params.dig(:edited_channel_post, :text)
}
params.delete(:edited_channel_post) # discard unused :edited_channel_post hash
end
# prevent multiple update
return if !params[:edited_message] && Ticket::Article.exists?(message_id: TelegramHelper.message_id(params))
# update article
if params[:edited_message]
article = Ticket::Article.find_by(message_id: TelegramHelper.message_id(params))
return if !article
params[:message] = params[:edited_message]
user = to_user(params)
to_article(params, user, article.ticket, channel, article)
return article
end
# send welcome message and don't create ticket
text = params[:message][:text]
if text.present? && text.start_with?('/start')
message(params[:message][:chat][:id], channel.options[:welcome] || __('Welcome! Feel free to ask me a question!'), params[:message][:from][:language_code])
return
# find ticket and close it
elsif text.present? && text.start_with?('/end')
user = to_user(params)
# get the last ticket of customer which is not closed yet, and close it
state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
possible_tickets = Ticket.where(customer_id: user.id).where.not(state_id: state_ids).reorder(:updated_at)
ticket = possible_tickets.find_each.find { |possible_ticket| possible_ticket.preferences[:channel_id] == channel.id }
return if !ticket
ticket.state = Ticket::State.find_by(name: 'closed')
ticket.save!
return if !channel.options[:goodbye]
message(params[:message][:chat][:id], channel.options[:goodbye], params[:message][:from][:language_code])
return
end
ticket = nil
# use transaction
Transaction.execute(reset_user_id: true, context: 'telegram') do
user = to_user(params)
ticket = to_ticket(params, user, group_id, channel)
to_article(params, user, ticket, channel)
end
ticket
end
def from_article(article)
message = nil
Rails.logger.debug { "Create telegram personal message from article to '#{article[:to]}'..." }
message = {}
# TODO: create telegram message here
Rails.logger.debug { message.inspect }
message
end
def get_file(params, file)
# telegram bot files are limited up to 20MB
# https://core.telegram.org/bots/api#getfile
if !validate_file_size(file)
message_text = __('The Telegram file is larger than the allowed 20 MB.')
message(params[:message][:chat][:id], "Sorry, we could not handle your message. #{message_text}", params[:message][:from][:language_code])
raise Exceptions::UnprocessableEntity, message_text
end
result = download_file(file[:file_id])
if !validate_download(result)
message_text = __('The file could not be retrieved from the bot.')
message(params[:message][:chat][:id], "Sorry, we could not handle your message. #{message_text}", params[:message][:from][:language_code])
raise Exceptions::UnprocessableEntity, message_text
end
result
end
def download_file(file_id)
document = nil
Telegram::Bot::Client.run(@token) do |bot|
document = bot.api.getFile(file_id: file_id)
end
url = "https://api.telegram.org/file/bot#{@token}/#{document.file_path}"
UserAgent.get(
url,
{},
{
open_timeout: 20,
read_timeout: 40,
verify_ssl: true,
},
)
end
def validate_file_size(file)
Rails.logger.error 'validate_file_size'
Rails.logger.error file[:file_size]
return false if file[:file_size] >= 20.megabytes
true
end
def validate_download(result)
return false if !result.success? || !result.body
true
end
end