telegram_workflow 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +72 -0
- data/LICENSE.txt +21 -0
- data/README.md +360 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/telegram_workflow.rb +38 -0
- data/lib/telegram_workflow/action.rb +44 -0
- data/lib/telegram_workflow/client.rb +140 -0
- data/lib/telegram_workflow/config.rb +48 -0
- data/lib/telegram_workflow/errors.rb +23 -0
- data/lib/telegram_workflow/params.rb +61 -0
- data/lib/telegram_workflow/rspec.rb +80 -0
- data/lib/telegram_workflow/session.rb +46 -0
- data/lib/telegram_workflow/stores/file.rb +18 -0
- data/lib/telegram_workflow/stores/in_memory.rb +13 -0
- data/lib/telegram_workflow/updates.rb +18 -0
- data/lib/telegram_workflow/version.rb +3 -0
- data/lib/telegram_workflow/workflow.rb +95 -0
- data/telegram_workflow.gemspec +28 -0
- metadata +111 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
@@ -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
|