telegram_workflow 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "telegram_workflow"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,38 @@
1
+ require "http"
2
+
3
+ module TelegramWorkflow
4
+ module Stores
5
+ end
6
+
7
+ def self.process(params)
8
+ Workflow.new(params).process
9
+ end
10
+
11
+ def self.updates(offset: nil, limit: nil, timeout: 60, allowed_updates: nil)
12
+ params = {}
13
+ params[:offset] = offset if offset
14
+ params[:limit] = limit if limit
15
+ params[:timeout] = timeout if timeout
16
+ params[:allowed_updates] = allowed_updates if allowed_updates
17
+
18
+ Updates.new(params).enum
19
+ end
20
+ end
21
+
22
+ require "telegram_workflow/action"
23
+ require "telegram_workflow/client"
24
+ require "telegram_workflow/config"
25
+ require "telegram_workflow/errors"
26
+ require "telegram_workflow/params"
27
+ require "telegram_workflow/session"
28
+ require "telegram_workflow/version"
29
+ require "telegram_workflow/updates"
30
+ require "telegram_workflow/workflow"
31
+ require "telegram_workflow/stores/in_memory"
32
+ require "telegram_workflow/stores/file"
33
+
34
+ TelegramWorkflow.__after_configuration do |config|
35
+ if config.webhook_url
36
+ TelegramWorkflow::Client.new.__setup_webhook
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ class TelegramWorkflow::Action
2
+ extend ::Forwardable
3
+ def_delegators :@__workflow, :client, :params, :redirect_to
4
+
5
+ def initialize(workflow, session, flash)
6
+ @__workflow = workflow
7
+ @__session = session
8
+ @__flash = flash
9
+ end
10
+
11
+ def shared
12
+ :__continue
13
+ end
14
+
15
+ def on_redirect(&block)
16
+ @on_redirect = block
17
+ end
18
+
19
+ def on_message(&block)
20
+ @on_message = block
21
+ end
22
+
23
+ def __reset_callbacks
24
+ @on_redirect = @on_message = nil
25
+ end
26
+
27
+ def __run_on_redirect
28
+ @on_redirect.call if @on_redirect
29
+ end
30
+
31
+ def __run_on_message
32
+ @on_message.call if @on_message
33
+ end
34
+
35
+ private
36
+
37
+ def session
38
+ @__session
39
+ end
40
+
41
+ def flash
42
+ @__flash
43
+ end
44
+ end
@@ -0,0 +1,140 @@
1
+ class TelegramWorkflow::Client
2
+ API_VERSION = "4.8"
3
+ WebhookFilePath = Pathname.new("tmp/telegram_workflow/webhook_url.txt")
4
+
5
+ AVAILABLE_ACTIONS = %i(
6
+ getUpdates
7
+ getWebhookInfo
8
+
9
+ getMe
10
+ sendMessage
11
+ forwardMessage
12
+ sendPhoto
13
+ sendAudio
14
+ sendDocument
15
+ sendVideo
16
+ sendAnimation
17
+ sendVoice
18
+ sendVideoNote
19
+ sendMediaGroup
20
+ sendLocation
21
+ editMessageLiveLocation
22
+ stopMessageLiveLocation
23
+ sendVenue
24
+ sendContact
25
+ sendPoll
26
+ sendDice
27
+ sendChatAction
28
+ getUserProfilePhotos
29
+ getFile
30
+ kickChatMember
31
+ unbanChatMember
32
+ restrictChatMember
33
+ promoteChatMember
34
+ setChatAdministratorCustomTitle
35
+ setChatPermissions
36
+ exportChatInviteLink
37
+ setChatPhoto
38
+ deleteChatPhoto
39
+ setChatTitle
40
+ setChatDescription
41
+ pinChatMessage
42
+ unpinChatMessage
43
+ leaveChat
44
+ getChat
45
+ getChatAdministrators
46
+ getChatMembersCount
47
+ getChatMember
48
+ setChatStickerSet
49
+ deleteChatStickerSet
50
+ answerCallbackQuery
51
+ setMyCommands
52
+ getMyCommands
53
+
54
+ editMessageText
55
+ editMessageCaption
56
+ editMessageMedia
57
+ editMessageReplyMarkup
58
+ stopPoll
59
+ deleteMessage
60
+
61
+ sendSticker
62
+ getStickerSet
63
+ uploadStickerFile
64
+ createNewStickerSet
65
+ addStickerToSet
66
+ setStickerPositionInSet
67
+ deleteStickerFromSet
68
+ setStickerSetThumb
69
+
70
+ answerInlineQuery
71
+
72
+ sendInvoice
73
+ answerShippingQuery
74
+ answerPreCheckoutQuery
75
+
76
+ setPassportDataErrors
77
+
78
+ sendGame
79
+ setGameScore
80
+ getGameHighScores
81
+ )
82
+
83
+ AVAILABLE_ACTIONS.each do |action|
84
+ method_name = action.to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
85
+
86
+ define_method(method_name) do |params = {}|
87
+ make_request(action, params)
88
+ end
89
+ end
90
+
91
+ def initialize(chat_id = nil)
92
+ @chat_id = chat_id
93
+ @webhook_url = TelegramWorkflow.config.webhook_url
94
+ @api_url = "https://api.telegram.org/bot#{TelegramWorkflow.config.api_token}"
95
+ end
96
+
97
+ def set_webhook(params = {})
98
+ make_request("setWebhook", params)
99
+ cached_webhook_url(new_url: @webhook_url)
100
+ end
101
+
102
+ def delete_webhook
103
+ make_request("deleteWebhook", {})
104
+ cached_webhook_url(new_url: "")
105
+ end
106
+
107
+ def __setup_webhook
108
+ TelegramWorkflow.config.logger.info "[TelegramWorkflow] Checking webhook setup..."
109
+
110
+ if cached_webhook_url != @webhook_url
111
+ TelegramWorkflow.config.logger.info "[TelegramWorkflow] Setting up a new webhook..."
112
+ set_webhook(url: @webhook_url)
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def cached_webhook_url(new_url: nil)
119
+ unless WebhookFilePath.exist?
120
+ WebhookFilePath.dirname.mkpath
121
+ WebhookFilePath.write("")
122
+ end
123
+
124
+ if new_url.nil?
125
+ WebhookFilePath.read
126
+ else
127
+ WebhookFilePath.write(new_url)
128
+ end
129
+ end
130
+
131
+ def make_request(action, params)
132
+ response = ::HTTP.post("#{@api_url}/#{action}", json: { chat_id: @chat_id, **params })
133
+
134
+ if response.code != 200
135
+ raise TelegramWorkflow::Errors::ApiError, response.parse["description"]
136
+ end
137
+
138
+ response.parse
139
+ end
140
+ end
@@ -0,0 +1,48 @@
1
+ unless defined?(Rails)
2
+ require "logger"
3
+ end
4
+
5
+ module TelegramWorkflow
6
+ class << self
7
+ def config
8
+ @config ||= Configuration.new
9
+ end
10
+
11
+ def configure
12
+ yield(config)
13
+ config.verify!
14
+
15
+ @__after_configuration.call(config)
16
+ end
17
+
18
+ def __after_configuration(&block)
19
+ @__after_configuration = block
20
+ end
21
+ end
22
+
23
+ class Configuration
24
+ attr_accessor :session_store, :logger, :client, :start_action, :webhook_url, :api_token
25
+
26
+ REQUIRED_PARAMS = %i(session_store start_action api_token)
27
+
28
+ def initialize
29
+ @client = TelegramWorkflow::Client
30
+
31
+ if defined?(Rails)
32
+ @session_store = Rails.cache
33
+ @logger = Rails.logger
34
+ else
35
+ @session_store = TelegramWorkflow::Stores::InMemory.new
36
+ @logger = Logger.new(STDOUT)
37
+ end
38
+ end
39
+
40
+ def verify!
41
+ blank_params = REQUIRED_PARAMS.select { |p| send(p).nil? }
42
+
43
+ if blank_params.any?
44
+ raise TelegramWorkflow::Errors::MissingConfiguration, blank_params
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,23 @@
1
+ module TelegramWorkflow::Errors
2
+ class DoubleRedirect < StandardError
3
+ def initialize(msg = "Redirect was called multiple times in the step callback.")
4
+ super
5
+ end
6
+ end
7
+
8
+ class SharedRedirect < StandardError
9
+ def initialize(msg = "You cannot redirect to a shared step.")
10
+ super
11
+ end
12
+ end
13
+
14
+ class ApiError < StandardError
15
+ end
16
+
17
+ class MissingConfiguration < StandardError
18
+ def initialize(missing_config_params)
19
+ msg = "Missing required configuration params: #{missing_config_params.join(", ")}"
20
+ super(msg)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TelegramWorkflow::Params
4
+ def initialize(params)
5
+ @params = params
6
+ end
7
+
8
+ def [](key)
9
+ @params[key]
10
+ end
11
+
12
+ def user
13
+ @user ||= @params.dig("message", "from") ||
14
+ @params.dig("callback_query", "from") ||
15
+ @params.dig("pre_checkout_query", "from") ||
16
+ @params.dig("shipping_query", "from") ||
17
+ @params.dig("inline_query", "from") ||
18
+ @params.dig("chosen_inline_result", "from")
19
+ end
20
+
21
+ def language_code
22
+ user["language_code"]
23
+ end
24
+
25
+ def user_id
26
+ user["id"]
27
+ end
28
+
29
+ def username
30
+ user["username"]
31
+ end
32
+
33
+ def chat_id
34
+ @params.dig("message", "chat", "id") ||
35
+ @params.dig("callback_query", "message", "chat", "id") ||
36
+ @params.dig("edited_message", "chat", "id") ||
37
+ @params.dig("channel_post", "chat", "id") ||
38
+ @params.dig("edited_channel_post", "chat", "id")
39
+ end
40
+
41
+ def message_text
42
+ @params.dig("message", "text")
43
+ end
44
+
45
+ def callback_data
46
+ @params.dig("callback_query", "data")
47
+ end
48
+
49
+ def start?
50
+ !!message_text&.start_with?("/start")
51
+ end
52
+
53
+ def command?
54
+ !!message_text&.start_with?("/")
55
+ end
56
+
57
+ def deep_link_payload
58
+ match = /\A\/(startgroup|start) (?<payload>.+)\z/.match(message_text)
59
+ match["payload"] if match
60
+ end
61
+ end
@@ -0,0 +1,80 @@
1
+ module TelegramActionExampleGroup
2
+ def self.included(klass)
3
+ klass.class_eval do
4
+ klass.metadata[:type] = :telegram_action
5
+
6
+ subject { double(client: spy, flow: spy) }
7
+
8
+ let(:current_action) { described_class }
9
+ let(:action_params) do
10
+ {
11
+ "update_id" => 111111111,
12
+ "message" => {
13
+ "message_id" => 200,
14
+ "from" => {
15
+ "id" => 112233445,
16
+ },
17
+ "text" => ""
18
+ },
19
+ "callback_query" => {
20
+ "data" => ""
21
+ }
22
+ }
23
+ end
24
+
25
+ before do
26
+ TelegramWorkflow.config.session_store = TelegramWorkflow::Stores::InMemory.new
27
+ TelegramWorkflow.config.start_action = TestStartAction
28
+ send_message message_text: "/start"
29
+ end
30
+
31
+ include InstanceMethods
32
+ end
33
+ end
34
+
35
+ module InstanceMethods
36
+ def send_message(message_text: "", callback_data: "")
37
+ action_params["message"]["text"] = message_text
38
+ action_params["callback_query"]["data"] = callback_data
39
+
40
+ workflow = TestFlow.new(action_params)
41
+ workflow.example_group = self
42
+
43
+ workflow.process
44
+ end
45
+ end
46
+
47
+ class TestFlow < TelegramWorkflow::Workflow
48
+ attr_accessor :example_group
49
+
50
+ def client
51
+ example_group.subject.client
52
+ end
53
+
54
+ def redirect_to(action_or_step, session_params = nil)
55
+ super
56
+
57
+ if session_params
58
+ example_group.subject.flow.send(:redirect_to, action_or_step, session_params)
59
+ else
60
+ example_group.subject.flow.send(:redirect_to, action_or_step)
61
+ end
62
+ end
63
+ end
64
+
65
+ class TelegramWorkflow::Action
66
+ def_delegators :@__workflow, :example_group
67
+ end
68
+
69
+ class TestStartAction < TelegramWorkflow::Action
70
+ def initial
71
+ on_message { redirect_to example_group.current_action }
72
+ end
73
+ end
74
+ end
75
+
76
+ RSpec.configure do |config|
77
+ config.include TelegramActionExampleGroup,
78
+ type: :telegram_action,
79
+ file_path: %r(spec/telegram_actions)
80
+ end