telegram_workflow 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 73a03ae1f9bea58f5cdc6bd1917107795429c95c2a54b98bd9ae3e8d95dd5bfe
4
+ data.tar.gz: 762fbb323b0e4529118e7af5c1f5f35b1bad859fcc0b7b036fdc60d57a791f59
5
+ SHA512:
6
+ metadata.gz: 2c1b7bc39edcd1e4c133939aa3c7289b887a9540ba8ff64415b7428f9841e86b0f1f42b46734ba6f1359657e3dc4607e468b5d1a68720c76961a0c66fd80a788
7
+ data.tar.gz: 5049a58f13c5717412ecd524a7219b8bdbf8d77d24447c5e0471f60c9256fc363b23c95639aed0d97ca0352edc10d1fa349cbc36690fe373da8919de5830d8fa
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.0
6
+ before_install: gem install bundler -v 2.1.4
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at nanosamoilov@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in telegram_workflow.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "rspec", "~> 3.0"
@@ -0,0 +1,72 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ telegram_workflow (0.1.0)
5
+ http
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (6.0.2.2)
11
+ concurrent-ruby (~> 1.0, >= 1.0.2)
12
+ i18n (>= 0.7, < 2)
13
+ minitest (~> 5.1)
14
+ tzinfo (~> 1.1)
15
+ zeitwerk (~> 2.2)
16
+ addressable (2.7.0)
17
+ public_suffix (>= 2.0.2, < 5.0)
18
+ concurrent-ruby (1.1.6)
19
+ diff-lcs (1.3)
20
+ domain_name (0.5.20190701)
21
+ unf (>= 0.0.5, < 1.0.0)
22
+ ffi (1.12.2)
23
+ ffi-compiler (1.0.1)
24
+ ffi (>= 1.0.0)
25
+ rake
26
+ http (4.4.1)
27
+ addressable (~> 2.3)
28
+ http-cookie (~> 1.0)
29
+ http-form_data (~> 2.2)
30
+ http-parser (~> 1.2.0)
31
+ http-cookie (1.0.3)
32
+ domain_name (~> 0.5)
33
+ http-form_data (2.3.0)
34
+ http-parser (1.2.1)
35
+ ffi-compiler (>= 1.0, < 2.0)
36
+ i18n (1.8.2)
37
+ concurrent-ruby (~> 1.0)
38
+ minitest (5.14.0)
39
+ public_suffix (4.0.4)
40
+ rake (12.3.3)
41
+ rspec (3.9.0)
42
+ rspec-core (~> 3.9.0)
43
+ rspec-expectations (~> 3.9.0)
44
+ rspec-mocks (~> 3.9.0)
45
+ rspec-core (3.9.1)
46
+ rspec-support (~> 3.9.1)
47
+ rspec-expectations (3.9.1)
48
+ diff-lcs (>= 1.2.0, < 2.0)
49
+ rspec-support (~> 3.9.0)
50
+ rspec-mocks (3.9.1)
51
+ diff-lcs (>= 1.2.0, < 2.0)
52
+ rspec-support (~> 3.9.0)
53
+ rspec-support (3.9.2)
54
+ thread_safe (0.3.6)
55
+ tzinfo (1.2.7)
56
+ thread_safe (~> 0.1)
57
+ unf (0.1.4)
58
+ unf_ext
59
+ unf_ext (0.0.7.7)
60
+ zeitwerk (2.3.0)
61
+
62
+ PLATFORMS
63
+ ruby
64
+
65
+ DEPENDENCIES
66
+ activesupport (~> 6.0.0)
67
+ rake (~> 12.0)
68
+ rspec (~> 3.0)
69
+ telegram_workflow!
70
+
71
+ BUNDLED WITH
72
+ 2.1.4
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Roman Samoilov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,360 @@
1
+ # TelegramWorkflow
2
+
3
+ TelegramWorkflow is a simple utility to help you organize the code to create Telegram bots.
4
+
5
+ It includes the HTTP client, which implements the complete Telegram API and a set of helpers to improve
6
+ the development experience.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'telegram_workflow'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle install
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install telegram_workflow
23
+
24
+ ## Core Concepts
25
+
26
+ ### Actions
27
+
28
+ In Rails we split the logic to process the requests into Controllers and Actions.
29
+ Similar to this approach, the gem suggests to split the logic to process the bot requests into Actions and Steps.
30
+
31
+ This is how a simple action could look like:
32
+
33
+ ```ruby
34
+ class Ping < TelegramWorkflow::Action
35
+ def initial
36
+ on_redirect do
37
+ client.send_message text: "Say ping:"
38
+ end
39
+
40
+ on_message do
41
+ client.send_message text: "pong"
42
+ end
43
+ end
44
+ end
45
+ ```
46
+
47
+ What's going on here:
48
+ * An action is created by defining a class that inherits from `TelegramWorkflow::Action`.
49
+ * The action has `initial` step. Every action should have at lease this step.
50
+ * The step method defines two optional callbacks. The `on_redirect` callback is called once the flow gets into the initial step. `on_message` callback is being called once a user sends a message back to the bot.
51
+
52
+ ### Redirection
53
+
54
+ `redirect_to` function allows to redirect between actions and steps, making it possible to create complex workflows.
55
+
56
+ ```ruby
57
+ class SelectMovie < TelegramWorkflow::Action
58
+ def initial
59
+ on_redirect do
60
+ client.send_message text: "Select a genre:"
61
+ end
62
+
63
+ on_message do
64
+ flash[:genre] = params.message_text
65
+ redirect_to :suggest
66
+ end
67
+ end
68
+
69
+ def suggest
70
+ on_redirect do
71
+ suggested_movie = find_a_movie_based_on_a_genre(flash[:genre])
72
+ client.send_message text: "You will love this one - #{suggested_movie.name}"
73
+ end
74
+ end
75
+ end
76
+ ```
77
+
78
+ Here we ask a user to select a movie genre. When a user responds to the bot, the response is saved into a temporary store. After that, a message with the suggested movie is sent back to the user.
79
+
80
+ This was an example of redirection between steps. Let's now add another action to rate the movie:
81
+
82
+ ```diff
83
+ class SelectMovie < TelegramWorkflow::Action
84
+ def suggest
85
+ on_redirect do
86
+ ...
87
+ + redirect_to RateMovie
88
+ end
89
+ end
90
+ end
91
+ ```
92
+ ```ruby
93
+ class RateMovie < TelegramWorkflow::Action
94
+ def initial
95
+ on_redirect do
96
+ client.send_message text: "Rate the movie:"
97
+ end
98
+
99
+ on_message do
100
+ # save the response
101
+ end
102
+ end
103
+ end
104
+ ```
105
+
106
+ Here you can see an example of redirection to another action.
107
+ Having a bot logic split over such small actions improves code maintanability and allows to follow SRP.
108
+
109
+ ## Global Objects
110
+
111
+ Each action has a set of globally accessible objects:
112
+
113
+ Object | Description
114
+ -------|------------
115
+ [params](README.md#params) | Instance of `TelegramWorkflow::Params`.
116
+ [client](README.md#client) | Instance of `TelegramWorkflow::Client`. Can be [customized](README.md#client-customization).
117
+ [session](README.md#session) | Persistent store to keep session data. Instance of `Hash`.
118
+ [flash](README.md#flash) | Temporary store to keep some data between different steps. Instance of `Hash`.
119
+
120
+ ## Public API
121
+
122
+ ### params
123
+
124
+ The `params` object encapsulates the logic to parse Telegram params.
125
+ It implements useful methods, like `message_text`, `callback_data` or `deep_link_payload` to fetch user submitted data from the params.
126
+
127
+ ### client
128
+
129
+ This is an instance of `TelegramWorkflow::Client` class, which implements a complete Telegram Bot API.
130
+ The methods to access the API are called after raw Telegram API methods.
131
+ For example, if you needed to call a [sendLocation](https://core.telegram.org/bots/api#sendlocation) method, you would use the following code:
132
+
133
+ ```ruby
134
+ client.send_location latitude: 40.748, longitude: -73.985, live_period: 120
135
+ ```
136
+
137
+ `chat_id` parameter should be omitted.
138
+
139
+ ### session
140
+
141
+ This is a persistent store to save the data associated with a user, e.g. current user's id, some settings or anything you would store in a session in a regular web application.
142
+
143
+ ### flash
144
+
145
+ This is a temporary store to save the data between the steps. The data persists while redirecting between the steps, but **gets deleted automatically when redirecting to another action**.
146
+
147
+ ### redirect_to(action_or_class, flash_params = {})
148
+
149
+ As you already know, this function allows to build complex workflows by redirecting between actions and steps.
150
+ The function expects either a symbol or instance of `Class` as a first argument. Passing a symbol will redirect to another step inside the current action. Passing instance of `Class` will redirect to another action.
151
+
152
+ ```ruby
153
+ # redirect to a step
154
+ redirect_to :suggest
155
+
156
+ # redirect to an action
157
+ redirect_to RateMovie
158
+ ```
159
+
160
+ Sometimes you will need to share some data between the actions. You could use `session` for this, but a more appropriate solution would be to have `redirect_to` function to preserve the flash between actions.
161
+ Check out this example:
162
+
163
+ ```ruby
164
+ class AskForBirthday < TelegramWorkflow::Action
165
+ def initial
166
+ on_redirect do
167
+ client.send_message text: "What year is your birthday?"
168
+ end
169
+
170
+ on_message do
171
+ birthday = params.message_text.to_i
172
+ redirect_to DisplayAge, birthday: birthday
173
+ end
174
+ end
175
+ end
176
+
177
+ class DisplayAge < TelegramWorkflow::Action
178
+ def initial
179
+ on_redirect do
180
+ age = Date.today.year - flash[:birthday]
181
+ client.send_message text: "You are #{age}!"
182
+ end
183
+ end
184
+ end
185
+ ```
186
+
187
+ You can see that despite the fact that flash is being cleared when redirecting to another action, passing `birthday` value to the `redirect_to` call made it accessible via flash in the action we redirected to.
188
+
189
+ ## Configuration
190
+
191
+ Configure the gem using the `TelegramWorkflow.configure` call.
192
+ The two required parameters are `start_action` and `api_token`.
193
+
194
+ ```ruby
195
+ TelegramWorkflow.configure do |config|
196
+ config.start_action = <Start Action Class>
197
+ config.api_token = <Your Token>
198
+ end
199
+ ```
200
+
201
+ Method | Default | Description
202
+ -------|---------|------------
203
+ api_token | | This is the token you get from `@botfather` to access the Telegram API.
204
+ start_action | | This is an entry-point action, which is called every time `/start` or `/startgroup` command is sent to the chat. You cannnot redirect to this action or call it manually. Use it to set the things up, e.g. create a user record or store current user's id in the session.
205
+ session_store | `Rails.cache` or `InMemoryStore.new` | This is the session store. Default implementation stores session in memory, which means it will be reset after server shutdown. Can be [customized](README.md#customization). Use `TelegramWorkflow::Stores::File` for persistent file store.
206
+ logger | `Rails.logger` or `STDOUT` | Logger object. Can be [customized](README.md#customization).
207
+ client | `TelegramWorkflow::Client` | The object which implements Telegram API. Can be [customized](README.md#client-customization).
208
+ webhook_url | nil | The webhook url. Set it only if you are using webhooks for getting updates. TelegramWorkflow will create a webhook subscription automatically.
209
+
210
+ ## Updates
211
+
212
+ The gem implements both methods of getting updates from the Telegram API.
213
+
214
+ ### Webhooks
215
+
216
+ * Configure the gem with `webhook_url` value.
217
+ * Process the updates with the following code in your controller:
218
+
219
+ ```ruby
220
+ class TelegramWebhooksController < ApplicationController
221
+ def create
222
+ TelegramWorkflow.process(params)
223
+ end
224
+ end
225
+ ```
226
+
227
+ ### Long polling
228
+
229
+ * Make sure you don't configure the gem with `webhook_url` value.
230
+ * Run the following code:
231
+
232
+ ```ruby
233
+ TelegramWorkflow.updates.each do |params|
234
+ TelegramWorkflow.process(params)
235
+ end
236
+ ```
237
+
238
+ Be aware that `TelegramWorkflow.updates.each` call is blocking.
239
+
240
+ Since most of the time will be spent on waiting for the Telegram API to respond, you might also want to process the updates in parallel:
241
+
242
+ ```ruby
243
+ require "concurrent-ruby"
244
+
245
+ pool = Concurrent::CachedThreadPool.new
246
+
247
+ TelegramWorkflow.updates.each do |params|
248
+ pool.post { TelegramWorkflow.process(params) }
249
+ end
250
+ ```
251
+
252
+ ## Customization
253
+
254
+ Object | Customization
255
+ -------|--------------
256
+ logger | An object that responds to `info` and `error` methods.
257
+ session_store | An object that responds to `read` and `write` methods. Refer to [InMemoryStore](lib/telegram_workflow/stores/in_memory.rb) class definition.
258
+ client | An object that responds to `new(chat_id)` method.
259
+
260
+ ### Client Customization
261
+
262
+ Use this customization to abstract your action's code from the Telegram API implementation details.
263
+
264
+ Create a customized client:
265
+
266
+ ```ruby
267
+ class MyClient < TelegramWorkflow::Client
268
+ def send_prize_location(user)
269
+ # this is an example call
270
+ prize = user.find_last_prize
271
+
272
+ send_venue latitude: prize.latitude,
273
+ longitude: prize.longitude,
274
+ address: prize.address
275
+ title: "Collect the last prize here!",
276
+ reply_markup: { keyboard: [[{ text: "Give me a hint" }], [{ text: "Give me anohter hint" }]] }
277
+ end
278
+ end
279
+ ```
280
+
281
+ Now, configure the gem to use the customized client:
282
+
283
+ ```ruby
284
+ TelegramWorkflow.configure do |config|
285
+ config.client = MyClient
286
+ end
287
+ ```
288
+
289
+ Then, in your action:
290
+
291
+ ```ruby
292
+ class FindPrize < TelegramWorkflow::Action
293
+ def initial
294
+ on_redirect do
295
+ client.send_prize_location(current_user)
296
+ end
297
+ end
298
+ end
299
+ ```
300
+
301
+ ## Testing
302
+
303
+ Testing utility provides `send_message` helper to emulate messages sent into the chat. Currently it accepts either `message_text` or `callback_data` as arguments.
304
+
305
+ Also, `subject.client` and `subject.flow` spies are available to track redirects and calls to the API client inside your actions.
306
+ Store your tests under `spec/telegram_actions` or tag them with `type: :telegram_action`.
307
+
308
+ Suppose we have the following action:
309
+
310
+ ```ruby
311
+ class AskForBirthday < TelegramWorkflow::Action
312
+ def initial
313
+ on_redirect do
314
+ client.send_message text: "What year is your birthday?"
315
+ end
316
+
317
+ on_message do
318
+ Birthday.create! date: params.message_text
319
+ redirect_to DisplayAge
320
+ end
321
+ end
322
+ end
323
+ ```
324
+
325
+ Now, let's add some tests for this action:
326
+
327
+ ```ruby
328
+ require "telegram_workflow/rspec"
329
+
330
+ RSpec.describe AskForBirthday, type: :telegram_action do
331
+ it "asks for user's birthday" do
332
+ expect(subject.client).to have_received(:send_message).with(text: "What year is your birthday?")
333
+ expect {
334
+ send_message message_text: "10/10/2000"
335
+ }.to change { Birthday.count }.by(1)
336
+
337
+ expect(subject.flow).to have_received(:redirect_to).with(DisplayAge)
338
+ end
339
+ end
340
+ ```
341
+
342
+ As you can see, testing utility starts the flow automatically, calling `initial` step on `described_class`.
343
+
344
+ ## Development
345
+
346
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
347
+
348
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
349
+
350
+ ## Contributing
351
+
352
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rsamoilov/telegram_workflow. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/rsamoilov/telegram_workflow/blob/master/CODE_OF_CONDUCT.md).
353
+
354
+ ## License
355
+
356
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
357
+
358
+ ## Code of Conduct
359
+
360
+ Everyone interacting in the TelegramWorkflow project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/rsamoilov/telegram_workflow/blob/master/CODE_OF_CONDUCT.md).