diligense 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fee0b40f03c5b52e6cb0683a3586a66cb92e8a9708d72a9facda56b9a3b661ec
4
+ data.tar.gz: 1e4e59ca8ec828f4188dcd52536f472f9c51e13b41b5c4bba0e6dc67a4216fcd
5
+ SHA512:
6
+ metadata.gz: a1da038a14a705b7424317995d45ef3dd3484f4756f60fd589525afae89f612fbdac5e0b3b7f387717b95ee355426fc974bed630df2f7c45314ca32a6d724527
7
+ data.tar.gz: d2e2fc4002332fbde439116abf9058c0b3371457d9e85ed9aff0be50405ff17ebbe7a4c163711b56214f62755a82b4461bf4b157c828b627d33a95c311eeb0a0
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-10-30
11
+
12
+ ### Added
13
+ - Initial release
14
+ - Rails-inspired routing system for Telegram bots
15
+ - Controller-based message handling
16
+ - Basic bot functionality with HTTParty integration
17
+ - Zeitwerk for autoloading
data/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # Ruby Telegram Bot
2
+
3
+ A Ruby application for Telegram bot with Rails-like structure.
4
+
5
+ ## Setup
6
+
7
+ 1. Install dependencies:
8
+ ```bash
9
+ bundle install
10
+ ```
11
+
12
+ 2. Run the application:
13
+ ```bash
14
+ ./bin/bot
15
+ ```
16
+
17
+ ## Ruby Version
18
+
19
+ This project uses Ruby 3.4.4
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diligense
4
+ class ApiClient
5
+ BASE_URL = 'https://api.telegram.org/bot'
6
+
7
+ attr_reader :bot_info
8
+
9
+ def initialize(token)
10
+ @token = token
11
+ @bot_info = fetch_bot_info
12
+ print_bot_info
13
+ end
14
+
15
+ def get_updates(offset: nil, timeout: 30)
16
+ params = { timeout: timeout }
17
+ params[:offset] = offset if offset
18
+
19
+ response = HTTParty.get("#{BASE_URL}#{@token}/getUpdates", query: params)
20
+
21
+ return [] unless response.success?
22
+
23
+ response.parsed_response['result']
24
+ end
25
+
26
+ def send_message(chat_id:, text:, **options)
27
+ params = {
28
+ chat_id: chat_id,
29
+ text: text
30
+ }.merge(options)
31
+
32
+ response = HTTParty.post("#{BASE_URL}#{@token}/sendMessage", body: params)
33
+
34
+ unless response.success?
35
+ raise "Failed to send message: #{response.code} - #{response.message}"
36
+ end
37
+
38
+ response.parsed_response['result']
39
+ end
40
+
41
+ private
42
+
43
+ def fetch_bot_info
44
+ response = HTTParty.get("#{BASE_URL}#{@token}/getMe")
45
+
46
+ unless response.success?
47
+ raise "Failed to fetch bot info: #{response.code} - #{response.message}"
48
+ end
49
+
50
+ response.parsed_response['result']
51
+ end
52
+
53
+ def print_bot_info
54
+ return unless @bot_info
55
+
56
+ puts "\n🤖 Bot Information:"
57
+ puts " ID: #{@bot_info['id']}"
58
+ puts " Name: #{@bot_info['first_name']}"
59
+ puts " Username: @#{@bot_info['username']}" if @bot_info['username']
60
+ puts " Can join groups: #{@bot_info['can_join_groups']}"
61
+ puts " Can read all group messages: #{@bot_info['can_read_all_group_messages']}"
62
+ puts " Supports inline queries: #{@bot_info['supports_inline_queries']}"
63
+ puts
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,59 @@
1
+ module Diligense
2
+ class Application
3
+ def initialize(root_path)
4
+ @root_path = root_path
5
+
6
+ @loader = Zeitwerk::Loader.new
7
+ @loader.push_dir(File.join(@root_path, 'app'))
8
+ @loader.enable_reloading
9
+ @loader.setup
10
+
11
+ token = ENV['TG_TOKEN']
12
+ raise 'TG_TOKEN environment variable is not set' unless token
13
+
14
+ @api_client = ApiClient.new(token)
15
+ @offset = nil
16
+ @router = load_routes
17
+ end
18
+
19
+ def run
20
+ loop do
21
+ updates = @api_client.get_updates(offset: @offset)
22
+
23
+ updates.each do |update|
24
+ if update.include?('message')
25
+ message_update = Types::Updates::MessageUpdate.new(update)
26
+ dispatch_message(message_update)
27
+ end
28
+
29
+ @offset = update['update_id'] + 1
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def load_routes
37
+ routes_path = File.join(@root_path, 'config', 'routes.rb')
38
+ router = eval(File.read(routes_path))
39
+ puts "Loaded #{router.routes.size} route(s)"
40
+ router
41
+ end
42
+
43
+ def dispatch_message(message_update)
44
+ handler_class_name = @router.match(message_update)
45
+
46
+ unless handler_class_name
47
+ puts "No route matched for message #{message_update.id}"
48
+ return
49
+ end
50
+
51
+ handler_class = Object.const_get(handler_class_name)
52
+ handler = handler_class.new(message_update: message_update, api_client: @api_client)
53
+ handler.call
54
+ rescue StandardError => e
55
+ puts "Error handling message: #{e.message}"
56
+ puts e.backtrace.first(5)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diligense
4
+ class MessageController
5
+ attr_reader :message
6
+
7
+ def initialize(message_update:, api_client:)
8
+ @message = message_update.message
9
+ @api_client = api_client
10
+ @message_update = message_update
11
+
12
+ @message&.instance_variable_set(:@api_client, @api_client)
13
+ end
14
+
15
+ def call
16
+ return unless should_call_plain_message?
17
+
18
+ plain_message if respond_to?(:plain_message)
19
+ end
20
+
21
+ private
22
+
23
+ def should_call_plain_message?
24
+ return false unless @message_update.type == :message
25
+ return false unless @message
26
+ return false if @message.has_attachments?
27
+
28
+ true
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diligense
4
+ class Route
5
+ attr_reader :type, :matcher, :handler
6
+
7
+ def initialize(type:, matcher:, handler:)
8
+ @type = type
9
+ @matcher = matcher
10
+ @handler = handler
11
+ end
12
+
13
+ def match(message_update)
14
+ result = (
15
+ case @type
16
+ when :chat_id
17
+ match_chat_id?(message_update)
18
+ when :user_id
19
+ match_user_id?(message_update)
20
+ when :chat_type
21
+ match_chat_type?(message_update)
22
+ when :message_type
23
+ match_message_type?(message_update)
24
+ when :text_pattern
25
+ match_text_pattern?(message_update)
26
+ when :default
27
+ true
28
+ else
29
+ false
30
+ end
31
+ )
32
+
33
+ result ? @handler : nil
34
+ end
35
+
36
+ private
37
+
38
+ def match_chat_id?(message_update)
39
+ chat_id = extract_chat_id(message_update)
40
+ return false unless chat_id
41
+
42
+ @matcher == chat_id
43
+ end
44
+
45
+ def match_user_id?(message_update)
46
+ user_id = extract_user_id(message_update)
47
+ return false unless user_id
48
+
49
+ @matcher == user_id
50
+ end
51
+
52
+ def match_chat_type?(message_update)
53
+ chat_type = extract_chat_type(message_update)
54
+ return false unless chat_type
55
+
56
+ if @matcher.is_a?(Array)
57
+ @matcher.include?(chat_type)
58
+ else
59
+ @matcher == chat_type
60
+ end
61
+ end
62
+
63
+ def match_message_type?(message_update)
64
+ raw_message = message_update.instance_variable_get(:@raw)['message']
65
+ return false unless raw_message
66
+
67
+ raw_message.key?(@matcher.to_s)
68
+ end
69
+
70
+ def match_text_pattern?(message_update)
71
+ raw_message = message_update.instance_variable_get(:@raw)['message']
72
+ return false unless raw_message
73
+
74
+ text = raw_message['text']
75
+ return false unless text
76
+
77
+ case @matcher
78
+ when Regexp
79
+ @matcher.match?(text)
80
+ when String
81
+ text == @matcher
82
+ else
83
+ false
84
+ end
85
+ end
86
+
87
+ def extract_chat_id(message_update)
88
+ raw_message = message_update.instance_variable_get(:@raw)['message']
89
+ raw_message&.dig('chat', 'id')
90
+ end
91
+
92
+ def extract_user_id(message_update)
93
+ raw_message = message_update.instance_variable_get(:@raw)['message']
94
+ raw_message&.dig('from', 'id')
95
+ end
96
+
97
+ def extract_chat_type(message_update)
98
+ raw_message = message_update.instance_variable_get(:@raw)['message']
99
+ raw_message&.dig('chat', 'type')
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diligense
4
+ class Router
5
+ attr_reader :routes
6
+
7
+ def initialize
8
+ @routes = []
9
+ end
10
+
11
+ def draw(&block)
12
+ instance_eval(&block)
13
+ self
14
+ end
15
+
16
+ def chat(chat_id, to:)
17
+ add_route(type: :chat_id, matcher: chat_id, handler: to)
18
+ end
19
+
20
+ def user(user_id, to:)
21
+ add_route(type: :user_id, matcher: user_id, handler: to)
22
+ end
23
+
24
+ def dialog(to:)
25
+ add_route(type: :chat_type, matcher: 'private', handler: to)
26
+ end
27
+
28
+ def group(to:)
29
+ add_route(type: :chat_type, matcher: %w[group supergroup], handler: to)
30
+ end
31
+
32
+ def channel(to:)
33
+ add_route(type: :chat_type, matcher: 'channel', handler: to)
34
+ end
35
+
36
+ def message_type(type, to:)
37
+ add_route(type: :message_type, matcher: type, handler: to)
38
+ end
39
+
40
+ def text_matches(pattern, to:)
41
+ add_route(type: :text_pattern, matcher: pattern, handler: to)
42
+ end
43
+
44
+ def default(to:)
45
+ add_route(type: :default, matcher: nil, handler: to)
46
+ end
47
+
48
+ def match(message_update)
49
+ @routes.each do |route|
50
+ handler = route.match(message_update)
51
+ return handler if handler
52
+ end
53
+ nil
54
+ end
55
+
56
+ private
57
+
58
+ def add_route(type:, matcher:, handler:)
59
+ @routes << Route.new(type: type, matcher: matcher, handler: handler)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Diligense
6
+ module Types
7
+ class Message
8
+ attr_reader :id, :content, :created_at
9
+
10
+ def initialize(raw, api_client: nil)
11
+ @raw = raw
12
+ @api_client = api_client
13
+
14
+ @id = raw.fetch('message_id')
15
+ @created_at = Time.at(raw.fetch('date')).to_datetime
16
+ end
17
+
18
+ def text
19
+ @raw['text']
20
+ end
21
+
22
+ def has_attachments?
23
+ attachment_keys = %w[photo video document audio voice video_note sticker animation]
24
+ attachment_keys.any? { |key| @raw.key?(key) }
25
+ end
26
+
27
+ def reply(text)
28
+ raise 'API client not set' unless @api_client
29
+
30
+ chat_id = @raw.dig('chat', 'id')
31
+ @api_client.send_message(chat_id: chat_id, text: text)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Diligense
6
+ module Types
7
+ module Updates
8
+ class MessageUpdate
9
+ attr_reader :id, :message
10
+
11
+ def initialize(raw)
12
+ @raw = raw
13
+
14
+ @id = raw.fetch('update_id')
15
+ @message = Types::Message.new(raw.fetch('message')) if raw['message']
16
+ end
17
+
18
+ def type
19
+ return :message if @raw['message']
20
+
21
+ :unknown
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diligense
4
+ VERSION = '0.0.1'
5
+ end
data/lib/diligense.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+ require 'httparty'
5
+
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.setup
8
+
9
+ module Diligense
10
+ end
11
+
12
+ require_relative 'diligense/version'
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: diligense
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Artem Levenkov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: httparty
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.21'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.21'
26
+ - !ruby/object:Gem::Dependency
27
+ name: zeitwerk
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.6'
40
+ description: A Ruby framework for building Telegram bots with Rails-like conventions
41
+ including routing, controllers, and message handling
42
+ email:
43
+ - me@alev.pro
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - README.md
50
+ - Rakefile
51
+ - lib/diligense.rb
52
+ - lib/diligense/api_client.rb
53
+ - lib/diligense/application.rb
54
+ - lib/diligense/message_controller.rb
55
+ - lib/diligense/route.rb
56
+ - lib/diligense/router.rb
57
+ - lib/diligense/types/message.rb
58
+ - lib/diligense/types/updates/message_update.rb
59
+ - lib/diligense/version.rb
60
+ homepage: https://github.com/alev-pro/diligense
61
+ licenses:
62
+ - MIT
63
+ metadata:
64
+ homepage_uri: https://github.com/alev-pro/diligense
65
+ source_code_uri: https://github.com/alev-pro/diligense
66
+ changelog_uri: https://github.com/alev-pro/diligense/blob/master/CHANGELOG.md
67
+ rubygems_mfa_required: 'true'
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 3.4.4
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.6.7
83
+ specification_version: 4
84
+ summary: Rails-inspired framework for building Telegram bots
85
+ test_files: []