telegem 2.1.0 → 3.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 +4 -4
- data/Readme.md +2 -2
- data/Starts_HallofFame.md +65 -0
- data/Test-Projects/bot.md +145 -0
- data/Test-Projects/bot1.rb +91 -0
- data/bin/telegem-ssl +44 -0
- data/docs-src/Bot-registration_.PNG +0 -0
- data/docs-src/bot.md +349 -180
- data/docs-src/ctx.md +399 -0
- data/docs-src/webhook.md +341 -0
- data/lib/api/client.rb +72 -27
- data/lib/core/bot.rb +81 -149
- data/lib/telegem.rb +1 -1
- data/lib/webhook/server.rb +149 -290
- data/public/index.html +481 -0
- metadata +32 -24
- data/Test-Projects/Movie-tracker-bot/Gemfile +0 -2
- data/Test-Projects/Movie-tracker-bot/bot.rb +0 -62
- data/Test-Projects/Movie-tracker-bot/handlers/.gitkeep +0 -0
- data/Test-Projects/Movie-tracker-bot/handlers/add_1_.rb +0 -160
- data/Test-Projects/Movie-tracker-bot/handlers/add_2_.rb +0 -139
- data/Test-Projects/Movie-tracker-bot/handlers/premium.rb +0 -13
- data/Test-Projects/Movie-tracker-bot/handlers/report.rb +0 -31
- data/Test-Projects/Movie-tracker-bot/handlers/search.rb +0 -150
- data/Test-Projects/Movie-tracker-bot/handlers/sponsor.rb +0 -14
- data/Test-Projects/Movie-tracker-bot/handlers/start.rb +0 -48
- data/Test-Projects/Movie-tracker-bot/handlers/watch.rb +0 -210
- data/Test-Projects/Test-submitted-by-marvel/.gitkeep +0 -0
- data/Test-Projects/Test-submitted-by-marvel/Marvel-bot.md +0 -3
- data/Test-Projects/bot_test1.rb +0 -75
- data/Test-Projects/pizza_test_bot_guide.md +0 -163
- data/docs-src/understanding-ctx.md +0 -581
- /data/{Test-Projects → bin}/.gitkeep +0 -0
- /data/{Test-Projects/Movie-tracker-bot → public}/.gitkeep +0 -0
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
class WatchHandler
|
|
2
|
-
def initialize(bot, db)
|
|
3
|
-
@bot = bot
|
|
4
|
-
@db = db
|
|
5
|
-
setup_watch_commands
|
|
6
|
-
setup_inline_handlers
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def setup_watch_commands
|
|
10
|
-
# Command: /watch - Show all subscribed shows with inline buttons
|
|
11
|
-
@bot.command("watch") do |ctx|
|
|
12
|
-
show_watch_menu(ctx)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Command: /watched <show_id> s<season>e<episode>
|
|
16
|
-
@bot.command("watched") do |ctx|
|
|
17
|
-
handle_watched_command(ctx)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def show_watch_menu(ctx)
|
|
22
|
-
user_id = ctx.from.id
|
|
23
|
-
|
|
24
|
-
# Get user's shows with their current progress
|
|
25
|
-
shows = @db.execute(<<-SQL, [user_id])
|
|
26
|
-
SELECT
|
|
27
|
-
movies.id,
|
|
28
|
-
movies.title,
|
|
29
|
-
watched.season as current_season,
|
|
30
|
-
watched.episode as current_episode
|
|
31
|
-
FROM watched
|
|
32
|
-
JOIN movies ON movies.id = watched.movie_id
|
|
33
|
-
WHERE watched.telegram_id = ?
|
|
34
|
-
ORDER BY movies.title
|
|
35
|
-
SQL
|
|
36
|
-
|
|
37
|
-
if shows.empty?
|
|
38
|
-
ctx.reply("📭 You're not tracking any shows yet!\nUse /search to find shows, then /add <id>")
|
|
39
|
-
return
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Create message with inline keyboard
|
|
43
|
-
message = "📺 *Mark Episodes Watched*\n\n"
|
|
44
|
-
message += "Click a show to mark episodes:\n\n"
|
|
45
|
-
|
|
46
|
-
shows.each do |id, title, season, episode|
|
|
47
|
-
message += "🎬 *#{title}*\n"
|
|
48
|
-
message += " Current: S#{season}E#{episode}\n"
|
|
49
|
-
message += " Next: S#{season}E#{episode + 1}\n\n"
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Create inline keyboard with ONE button per show
|
|
53
|
-
inline = Telegem.inline do
|
|
54
|
-
shows.each do |id, title, season, episode|
|
|
55
|
-
# Button shows: "Stranger Things (S1E3)"
|
|
56
|
-
row button "#{title} (S#{season}E#{episode})",
|
|
57
|
-
callback_data: "select_show:#{id}:#{season}:#{episode}"
|
|
58
|
-
end
|
|
59
|
-
# Add a "Mark All Caught Up" button
|
|
60
|
-
row button "✅ Mark All As Caught Up", callback_data: "catchup_all"
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
ctx.reply(message, reply_markup: inline, parse_mode: 'Markdown')
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def setup_inline_handlers
|
|
67
|
-
@bot.on(:callback_query) do |ctx|
|
|
68
|
-
case ctx.data
|
|
69
|
-
when /^select_show:(\d+):(\d+):(\d+)$/
|
|
70
|
-
handle_show_selection(ctx, $1.to_i, $2.to_i, $3.to_i)
|
|
71
|
-
when /^mark_episode:(\d+):(\d+):(\d+)$/
|
|
72
|
-
handle_mark_episode(ctx, $1.to_i, $2.to_i, $3.to_i)
|
|
73
|
-
when "catchup_all"
|
|
74
|
-
handle_catchup_all(ctx)
|
|
75
|
-
when "back_to_shows"
|
|
76
|
-
show_watch_menu(ctx)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def handle_show_selection(ctx, show_id, current_season, current_episode)
|
|
82
|
-
# Get show title
|
|
83
|
-
show = @db.execute(
|
|
84
|
-
"SELECT title FROM movies WHERE id = ?",
|
|
85
|
-
[show_id]
|
|
86
|
-
).first
|
|
87
|
-
|
|
88
|
-
return unless show
|
|
89
|
-
|
|
90
|
-
show_title = show[0]
|
|
91
|
-
|
|
92
|
-
# Create episode selection buttons
|
|
93
|
-
message = "🎬 *#{show_title}*\n"
|
|
94
|
-
message += "Mark which episode you watched:\n\n"
|
|
95
|
-
message += "Current: S#{current_season}E#{current_episode}\n\n"
|
|
96
|
-
|
|
97
|
-
# Create buttons for next 5 episodes
|
|
98
|
-
inline = Telegem.inline do
|
|
99
|
-
# Show 5 episodes: current+1 to current+5
|
|
100
|
-
(1..5).each do |offset|
|
|
101
|
-
ep_num = current_episode + offset
|
|
102
|
-
row button "✅ S#{current_season}E#{ep_num}",
|
|
103
|
-
callback_data: "mark_episode:#{show_id}:#{current_season}:#{ep_num}"
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Next season button
|
|
107
|
-
row button "➡️ Next Season (S#{current_season + 1}E1)",
|
|
108
|
-
callback_data: "mark_episode:#{show_id}:#{current_season + 1}:1"
|
|
109
|
-
|
|
110
|
-
# Back button
|
|
111
|
-
row button "🔙 Back to Shows", callback_data: "back_to_shows"
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Edit the message with new buttons
|
|
115
|
-
ctx.edit_message_text(message, reply_markup: inline, parse_mode: 'Markdown')
|
|
116
|
-
ctx.answer_callback_query(text: "Select episode for #{show_title}")
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def handle_mark_episode(ctx, show_id, season, episode)
|
|
120
|
-
user_id = ctx.from.id
|
|
121
|
-
|
|
122
|
-
# Update watched table
|
|
123
|
-
@db.execute(<<-SQL, [season, episode, user_id, show_id])
|
|
124
|
-
UPDATE watched
|
|
125
|
-
SET season = ?, episode = ?, updated_at = CURRENT_TIMESTAMP
|
|
126
|
-
WHERE telegram_id = ? AND movie_id = ?
|
|
127
|
-
SQL
|
|
128
|
-
|
|
129
|
-
# Get show title for response
|
|
130
|
-
show = @db.execute(
|
|
131
|
-
"SELECT title FROM movies WHERE id = ?",
|
|
132
|
-
[show_id]
|
|
133
|
-
).first
|
|
134
|
-
|
|
135
|
-
show_title = show[0] if show
|
|
136
|
-
|
|
137
|
-
# Show confirmation
|
|
138
|
-
ctx.answer_callback_query(
|
|
139
|
-
text: "✅ Marked #{show_title} S#{season}E#{episode} as watched!"
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
# Update message to show new status
|
|
143
|
-
message = "✅ *Updated!*\n\n"
|
|
144
|
-
message += "🎬 #{show_title}\n"
|
|
145
|
-
message += "Now watching: S#{season}E#{episode}\n"
|
|
146
|
-
message += "Next alert: S#{season}E#{episode + 1}\n\n"
|
|
147
|
-
message += "Click another show or use /watch again."
|
|
148
|
-
|
|
149
|
-
# Keep back button
|
|
150
|
-
inline = Telegem.inline do
|
|
151
|
-
row button "🔙 Back to Shows", callback_data: "back_to_shows"
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
ctx.edit_message_text(message, reply_markup: inline, parse_mode: 'Markdown')
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def handle_catchup_all(ctx)
|
|
158
|
-
user_id = ctx.from.id
|
|
159
|
-
|
|
160
|
-
# For each show, set episode to a high number (like 999 to mark as "caught up")
|
|
161
|
-
shows_updated = @db.execute(<<-SQL, [user_id])
|
|
162
|
-
UPDATE watched
|
|
163
|
-
SET episode = 999, updated_at = CURRENT_TIMESTAMP
|
|
164
|
-
WHERE telegram_id = ? AND episode < 999
|
|
165
|
-
SQL
|
|
166
|
-
|
|
167
|
-
ctx.answer_callback_query(
|
|
168
|
-
text: "✅ Marked all shows as caught up!"
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
# Go back to show list
|
|
172
|
-
show_watch_menu(ctx)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def handle_watched_command(ctx)
|
|
176
|
-
# Text command version: /watched 1 s1e3
|
|
177
|
-
args = ctx.message.text.split
|
|
178
|
-
|
|
179
|
-
if args.size != 3
|
|
180
|
-
ctx.reply("Usage: /watched <show_id> <episode>\nExample: /watched 1 s1e3")
|
|
181
|
-
return
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
show_id = args[1].to_i
|
|
185
|
-
episode_str = args[2]
|
|
186
|
-
|
|
187
|
-
match = episode_str.match(/s(\d+)e(\d+)/i)
|
|
188
|
-
unless match
|
|
189
|
-
ctx.reply("❌ Invalid format. Use: s1e3, s2e5, etc.")
|
|
190
|
-
return
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
season = match[1].to_i
|
|
194
|
-
episode = match[2].to_i
|
|
195
|
-
user_id = ctx.from.id
|
|
196
|
-
|
|
197
|
-
# Update database
|
|
198
|
-
@db.execute(<<-SQL, [season, episode, user_id, show_id])
|
|
199
|
-
UPDATE watched
|
|
200
|
-
SET season = ?, episode = ?, updated_at = CURRENT_TIMESTAMP
|
|
201
|
-
WHERE telegram_id = ? AND movie_id = ?
|
|
202
|
-
SQL
|
|
203
|
-
|
|
204
|
-
if @db.changes > 0
|
|
205
|
-
ctx.reply("✅ Updated! You've watched up to S#{season}E#{episode}")
|
|
206
|
-
else
|
|
207
|
-
ctx.reply("❌ You're not tracking that show. Use /add first.")
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|
|
File without changes
|
data/Test-Projects/bot_test1.rb
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# Test-Projects/bot_test1.rb
|
|
3
|
-
require 'telegem'
|
|
4
|
-
|
|
5
|
-
puts "🚀 Starting Bot Test 1..."
|
|
6
|
-
puts "Ruby: #{RUBY_VERSION}"
|
|
7
|
-
puts "Telegem: #{Telegem::VERSION rescue 'Not loaded'}"
|
|
8
|
-
|
|
9
|
-
begin
|
|
10
|
-
# 1. Create bot with your token
|
|
11
|
-
bot = Telegem.new('8013082846:AAEnG0T1pnLhOpjwpF3I-A4DX4aQy_HPsAc')
|
|
12
|
-
puts "✅ Bot object created"
|
|
13
|
-
|
|
14
|
-
# 2. Add a command
|
|
15
|
-
bot.command('start') do |ctx|
|
|
16
|
-
ctx.reply "🤖 Test Bot 1 is working!"
|
|
17
|
-
puts "✅ Command '/start' would reply"
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
bot.command('ping') do |ctx|
|
|
21
|
-
ctx.reply "🏓 Pong! #{Time.now}"
|
|
22
|
-
puts "✅ Command '/ping' would reply"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
puts "✅ Commands registered"
|
|
26
|
-
|
|
27
|
-
# 3. Try a simple API call (getMe)
|
|
28
|
-
puts "📡 Testing API connection..."
|
|
29
|
-
me = bot.api.call('getMe') rescue nil
|
|
30
|
-
|
|
31
|
-
if me
|
|
32
|
-
puts "✅ API Connected! Bot: @#{me['username']} (#{me['first_name']})"
|
|
33
|
-
else
|
|
34
|
-
puts "⚠️ API test failed (might be network/CI issue)"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# 4. Start polling for 10 seconds
|
|
38
|
-
puts "🔄 Starting bot polling (10 seconds test)..."
|
|
39
|
-
|
|
40
|
-
# Run polling in a thread with timeout
|
|
41
|
-
polling_thread = Thread.new do
|
|
42
|
-
begin
|
|
43
|
-
bot.start_polling(timeout: 5, limit: 1)
|
|
44
|
-
rescue => e
|
|
45
|
-
puts "⚠️ Polling error (normal in CI): #{e.message}"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Wait 10 seconds then stop
|
|
50
|
-
sleep 10
|
|
51
|
-
puts "⏱️ 10 seconds passed, stopping bot..."
|
|
52
|
-
|
|
53
|
-
# Try to stop gracefully
|
|
54
|
-
bot.shutdown rescue nil
|
|
55
|
-
polling_thread.kill if polling_thread.alive?
|
|
56
|
-
|
|
57
|
-
puts "🎉 Test completed successfully!"
|
|
58
|
-
puts "✅ Bot framework works!"
|
|
59
|
-
puts "✅ Commands work!"
|
|
60
|
-
puts "✅ Async system works!"
|
|
61
|
-
|
|
62
|
-
rescue LoadError => e
|
|
63
|
-
puts "❌ LOAD ERROR: #{e.message}"
|
|
64
|
-
puts "Backtrace:"
|
|
65
|
-
puts e.backtrace.first(5)
|
|
66
|
-
exit 1
|
|
67
|
-
|
|
68
|
-
rescue => e
|
|
69
|
-
puts "❌ ERROR: #{e.class}: #{e.message}"
|
|
70
|
-
puts "Backtrace (first 3 lines):"
|
|
71
|
-
puts e.backtrace.first(3)
|
|
72
|
-
exit 1
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
puts "✨ All tests passed!"
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
```ruby
|
|
3
|
-
require 'telegem'
|
|
4
|
-
|
|
5
|
-
# 1. Create bot
|
|
6
|
-
bot = Telegem.new(ENV['PIZZA_BOT_TOKEN'])
|
|
7
|
-
|
|
8
|
-
# 2. Welcome command
|
|
9
|
-
bot.command('start') do |ctx|
|
|
10
|
-
ctx.reply "🍕 Welcome to PizzaBot!"
|
|
11
|
-
ctx.reply "Use /order to start ordering"
|
|
12
|
-
ctx.reply "Use /menu to see options"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# 3. Menu command
|
|
16
|
-
bot.command('menu') do |ctx|
|
|
17
|
-
menu = <<~MENU
|
|
18
|
-
*Our Menu:*
|
|
19
|
-
|
|
20
|
-
🍕 Pizzas:
|
|
21
|
-
- Margherita: $10
|
|
22
|
-
- Pepperoni: $12
|
|
23
|
-
- Veggie: $11
|
|
24
|
-
|
|
25
|
-
🥤 Drinks:
|
|
26
|
-
- Cola: $2
|
|
27
|
-
- Water: $1
|
|
28
|
-
|
|
29
|
-
Use /order to order!
|
|
30
|
-
MENU
|
|
31
|
-
|
|
32
|
-
ctx.reply menu, parse_mode: 'Markdown'
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# 4. Order command - Starts a scene
|
|
36
|
-
bot.scene :ordering do
|
|
37
|
-
step :choose_pizza do |ctx|
|
|
38
|
-
keyboard = ctx.keyboard do
|
|
39
|
-
row "Margherita", "Pepperoni"
|
|
40
|
-
row "Veggie", "Cancel"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
ctx.reply "Choose your pizza:", reply_markup: keyboard
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
step :save_pizza do |ctx|
|
|
47
|
-
ctx.session[:pizza] = ctx.message.text
|
|
48
|
-
ctx.reply "Great! #{ctx.session[:pizza]} selected."
|
|
49
|
-
ctx.reply "What's your address?"
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
step :save_address do |ctx|
|
|
53
|
-
ctx.session[:address] = ctx.message.text
|
|
54
|
-
|
|
55
|
-
# Show summary
|
|
56
|
-
summary = <<~SUMMARY
|
|
57
|
-
*Order Summary:*
|
|
58
|
-
|
|
59
|
-
Pizza: #{ctx.session[:pizza]}
|
|
60
|
-
Address: #{ctx.session[:address]}
|
|
61
|
-
|
|
62
|
-
Confirm? (Yes/No)
|
|
63
|
-
SUMMARY
|
|
64
|
-
|
|
65
|
-
ctx.reply summary, parse_mode: 'Markdown'
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
step :confirm do |ctx|
|
|
69
|
-
if ctx.message.text.downcase == 'yes'
|
|
70
|
-
ctx.reply "✅ Order placed! Delivery in 30 minutes."
|
|
71
|
-
ctx.reply "Use /track to track your order"
|
|
72
|
-
else
|
|
73
|
-
ctx.reply "❌ Order cancelled."
|
|
74
|
-
end
|
|
75
|
-
ctx.leave_scene
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Handle "Cancel" at any step
|
|
79
|
-
on_enter do |ctx|
|
|
80
|
-
ctx.session[:order_id] = rand(1000..9999)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# 5. Start the order scene
|
|
85
|
-
bot.command('order') do |ctx|
|
|
86
|
-
ctx.enter_scene(:ordering)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# 6. Track order (simple version)
|
|
90
|
-
bot.command('track') do |ctx|
|
|
91
|
-
if ctx.session[:order_id]
|
|
92
|
-
ctx.reply "Order ##{ctx.session[:order_id]} is being prepared!"
|
|
93
|
-
else
|
|
94
|
-
ctx.reply "No active order. Use /order to start."
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# 7. Handle keyboard button presses
|
|
99
|
-
bot.on(:message) do |ctx|
|
|
100
|
-
# Skip if it's a command or in scene
|
|
101
|
-
next if ctx.message.command? || ctx.scene
|
|
102
|
-
|
|
103
|
-
text = ctx.message.text
|
|
104
|
-
|
|
105
|
-
case text
|
|
106
|
-
when "Margherita", "Pepperoni", "Veggie"
|
|
107
|
-
ctx.reply "Use /order to order a #{text} pizza!"
|
|
108
|
-
when "Cancel"
|
|
109
|
-
ctx.reply "Cancelled."
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# 8. Start bot
|
|
114
|
-
if ENV['WEBHOOK']
|
|
115
|
-
bot.webhook_server(port: ENV['PORT'] || 3000).run
|
|
116
|
-
else
|
|
117
|
-
puts "🍕 PizzaBot starting..."
|
|
118
|
-
bot.start_polling
|
|
119
|
-
end
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
🧪 Let's Test It!
|
|
125
|
-
|
|
126
|
-
Create the file:
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
mkdir -p examples
|
|
130
|
-
touch examples/pizza_bot.rb
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Run it:
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
# Set your token
|
|
137
|
-
export PIZZA_BOT_TOKEN="123456:your_token_here"
|
|
138
|
-
|
|
139
|
-
# Run the bot
|
|
140
|
-
ruby examples/pizza_bot.rb
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Test the flow:
|
|
144
|
-
|
|
145
|
-
1. /start → Shows welcome
|
|
146
|
-
2. /menu → Shows pizza menu
|
|
147
|
-
3. /order → Starts ordering scene
|
|
148
|
-
4. Choose "Margherita" → Asks for address
|
|
149
|
-
5. Type your address → Shows summary
|
|
150
|
-
6. Type "yes" → Order confirmed!
|
|
151
|
-
7. /track → Shows order status
|
|
152
|
-
|
|
153
|
-
---
|
|
154
|
-
|
|
155
|
-
🏗️ Your Homework:
|
|
156
|
-
|
|
157
|
-
Can you add:
|
|
158
|
-
|
|
159
|
-
1. A /help command showing all commands?
|
|
160
|
-
2. A "size" step in the scene (Small/Medium/Large)?
|
|
161
|
-
3. A middleware that logs all orders to a file?
|
|
162
|
-
|
|
163
|
-
This is how you learn: Build, break, fix, improve. You now understand then library enough to code without AI help!
|