diff --git a/README.md b/README.md index 9733ea7..164ebd4 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,18 @@ Telegram::Bot.configure do |config| end ``` +## Validating data received via the Web App + +To validate data received via the [Web App](https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app), you can use the `Telegram::Bot::WebApp` class. +This class has a method `#verify_data_init` receive string with telegram data init that returns `Telegram::Bot::Types::WebAppUser` if the data is valid and `false` if it is not. + +```ruby +bot = Telegram::Bot::Client.new(token) +bot.web_app.verify_data_init(td_data_init) +``` + - `td_data_init` - String with telegram data init. + + ## Boilerplates If you don't know how to setup database for your bot or how to use it with different languages diff --git a/lib/telegram/bot/client.rb b/lib/telegram/bot/client.rb index 6e7806b..c2f07af 100644 --- a/lib/telegram/bot/client.rb +++ b/lib/telegram/bot/client.rb @@ -3,7 +3,7 @@ module Telegram module Bot class Client - attr_reader :api, :options + attr_reader :api, :options, :web_app attr_accessor :logger def self.run(*args, &block) @@ -13,6 +13,7 @@ def self.run(*args, &block) def initialize(token, hash = {}) @options = default_options.merge(hash) @api = Api.new(token, url: options.delete(:url), environment: options.delete(:environment)) + @web_app = WebApp.new(token) @logger = options.delete(:logger) end diff --git a/lib/telegram/bot/types/web_app_user.rb b/lib/telegram/bot/types/web_app_user.rb new file mode 100644 index 0000000..0f9f7c9 --- /dev/null +++ b/lib/telegram/bot/types/web_app_user.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Telegram + module Bot + module Types + class WebAppUser < Base + attribute :id, Types::Integer + attribute :first_name, Types::String + attribute? :is_bot, Types::Bool + attribute? :last_name, Types::String + attribute? :username, Types::String + attribute? :language_code, Types::String + attribute? :is_premium, Types::True + attribute? :added_to_attachment_menu, Types::True + attribute? :allows_write_to_pm, Types::Bool + attribute? :photo_url, Types::String + end + end + end +end diff --git a/lib/telegram/bot/web_app.rb b/lib/telegram/bot/web_app.rb new file mode 100644 index 0000000..ef6499b --- /dev/null +++ b/lib/telegram/bot/web_app.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'openssl' +require 'json' +require 'uri' + +module Telegram + module Bot + class WebApp + TG_KEY = 'WebAppData' + + def initialize(token) + @token = token + end + + def verify_data_init(data_init) + decoded_data = URI.decode_www_form(data_init).to_h + data_check_string = build_data_check_string(decoded_data) + + secret_key = hmac_sha256(TG_KEY, @token) + hex = hex_encode(hmac_sha256(secret_key, data_check_string)) + + return false unless decoded_data['hash'] == hex + + Types::WebAppUser.new(JSON.parse(decoded_data['user'], symbolize_names: true)) + end + + private + + def build_data_check_string(decoded_data) + decoded_data.filter_map { |key, value| "#{key}=#{value}" unless key == 'hash' }.sort.join("\n") + end + + def hmac_sha256(key, data) + OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, data) + end + + def hex_encode(data) + data.unpack1('H*') + end + end + end +end diff --git a/spec/lib/telegram/bot/web_app_spec.rb b/spec/lib/telegram/bot/web_app_spec.rb new file mode 100644 index 0000000..6793934 --- /dev/null +++ b/spec/lib/telegram/bot/web_app_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +RSpec.describe Telegram::Bot::WebApp do + let(:web_app) { described_class.new(token) } + let(:token) { '6066223859:AAHTBqDfGUUXbb9R0HQmYlrJ6_moCPb6-wI' } + + describe '#verify_data_init' do + subject { web_app.verify_data_init(data_init) } + + context 'when data_init is invalid' do + # rubocop:disable Layout/LineLength + let(:data_init) do + 'query_id=AAFiJKgRAAAAAGIkqBEsKipR&user=%7B%22id%22%3A296232034%2C%22first_name%22%3A%22%D0%9C%D0%B8%D1%85%D0%B0%D0%B8%D0%BB%22%2C%22last_name%22%3A%22%D0%92%D0%B0%D0%BB%D0%BE%D0%B2%22%2C%22username%22%3A%22valovm%22%2C%22language_code%22%3A%22en%22%2C%22is_premium%22%3Atrue%2C%22allows_write_to_pm%22%3Atrue%7D&auth_date=1708281435&hash=c7813b4aee476fa0c79a08e182b197ed8d97a8010cebc813320ea61b0ced2b66' + end + # rubocop:enable Layout/LineLength + + it { is_expected.to be_a(Telegram::Bot::Types::WebAppUser) } + end + + context 'when data_init is valid' do + let(:data_init) { 'data_init_not_valid' } + + it { is_expected.to be_falsey } + end + end +end