23 Commits

Author SHA1 Message Date
Dylan Knutson
83ae4ebd45 initial telegram bot 2025-07-31 03:45:46 +00:00
Dylan Knutson
d899413d7c Update task task-79.1 2025-07-31 03:14:17 +00:00
Dylan Knutson
0fe7040935 Update task task-79.1 2025-07-31 03:14:11 +00:00
Dylan Knutson
2be92ac365 Create task task-79.1 2025-07-31 03:14:03 +00:00
Dylan Knutson
210b3b05c7 Update task task-79 2025-07-31 03:11:42 +00:00
Dylan Knutson
3d35a8b3b9 Update task task-79 2025-07-31 03:11:28 +00:00
Dylan Knutson
544f6764d4 add telegram-bot-ruby 2025-07-31 03:07:06 +00:00
Dylan Knutson
67339142dd Update task task-79 2025-07-31 02:55:52 +00:00
Dylan Knutson
d892e00471 Update task task-79 2025-07-31 02:54:47 +00:00
Dylan Knutson
d85d04ea53 Update task task-79 2025-07-31 02:54:38 +00:00
Dylan Knutson
6fa15fdafc Update task task-79 2025-07-31 02:53:56 +00:00
Dylan Knutson
611b20c146 Update task task-79 2025-07-31 02:53:50 +00:00
Dylan Knutson
a497fa4adf Update task task-79 2025-07-31 02:53:22 +00:00
Dylan Knutson
35fe54ccc7 Update task task-79 2025-07-31 02:53:14 +00:00
Dylan Knutson
a3898e8dba Update task task-79 2025-07-31 02:53:07 +00:00
Dylan Knutson
68776f74c6 Update task task-79 2025-07-31 02:52:45 +00:00
Dylan Knutson
b0356111b6 Update task task-79 2025-07-31 02:52:35 +00:00
Dylan Knutson
9f33f26b2b Update task task-79 2025-07-31 02:52:29 +00:00
Dylan Knutson
4e2bd344fa Create task task-79 2025-07-31 02:52:24 +00:00
Dylan Knutson
55dfc81436 fallback creator, focus dragdrop on paste 2025-07-31 02:34:47 +00:00
Dylan Knutson
118a0c58c2 load all fa ids 2025-07-31 02:30:55 +00:00
Dylan Knutson
e598529639 robots.txt for blocking ai agents 2025-07-30 23:39:46 +00:00
Dylan Knutson
eefcd9eb93 opengraph meta tags 2025-07-30 23:30:44 +00:00
31 changed files with 18980 additions and 32 deletions

View File

@@ -55,7 +55,7 @@ gem "bootsnap", require: false
group :development, :test, :staging do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", "~> 1.11", platforms: %i[mri mingw x64_mingw]
gem "debug", "~> 1.11", platforms: %i[mri mingw x64_mingw], require: false
end
group :development, :staging do
@@ -132,6 +132,9 @@ gem "dtext_rb",
ref: "5ef8fd7a5205c832f4c18197911717e7d491494e"
gem "charlock_holmes"
# Telegram Bot API
gem "telegram-bot-ruby"
# gem "pghero", git: "https://github.com/dymk/pghero", ref: "e314f99"
gem "pghero", "~> 3.6"
gem "pg_query", ">= 2"

View File

@@ -176,6 +176,28 @@ GEM
rubyzip (~> 2.0)
domain_name (0.6.20240107)
drb (2.2.3)
dry-core (1.1.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
dry-inflector (1.2.0)
dry-logic (1.6.0)
bigdecimal
concurrent-ruby (~> 1.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-struct (1.8.0)
dry-core (~> 1.1)
dry-types (~> 1.8, >= 1.8.2)
ice_nine (~> 0.11)
zeitwerk (~> 2.6)
dry-types (1.8.3)
bigdecimal (~> 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
enumerable-statistics (2.0.8)
erubi (1.13.1)
et-orbi (1.2.11)
@@ -189,6 +211,14 @@ GEM
faiss (0.3.2)
numo-narray
rice (>= 4.0.2)
faraday (2.13.4)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (3.4.1)
net-http (>= 0.5.0)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm64-darwin)
@@ -240,6 +270,7 @@ GEM
http-form_data (2.3.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
io-console (0.8.0)
irb (1.14.3)
rdoc (>= 4.0.0)
@@ -247,6 +278,7 @@ GEM
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.13.2)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@@ -288,8 +320,11 @@ GEM
mmh3 (1.2.0)
msgpack (1.7.5)
multi_json (1.15.0)
multipart-post (2.4.1)
neighbor (0.5.1)
activerecord (>= 7)
net-http (0.6.0)
uri
net-imap (0.5.4)
date
net-protocol
@@ -625,6 +660,11 @@ GEM
spoom (>= 1.2.0)
thor (>= 1.2.0)
yard-sorbet
telegram-bot-ruby (2.4.0)
dry-struct (~> 1.6)
faraday (~> 2.0)
faraday-multipart (~> 1.0)
zeitwerk (~> 2.6)
thor (1.3.2)
thruster (0.1.11-aarch64-linux)
thruster (0.1.11-arm64-darwin)
@@ -638,6 +678,7 @@ GEM
concurrent-ruby (~> 1.0)
unicode_plot (0.0.5)
enumerable-statistics (>= 2.0.1)
uri (1.0.3)
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
@@ -759,6 +800,7 @@ DEPENDENCIES
table_print
tailwindcss-rails (~> 3.0)
tapioca (= 0.16.6)
telegram-bot-ruby
thruster
timeout
turbo-rails

View File

@@ -11,6 +11,8 @@ class GlobalStatesController < ApplicationController
IB_COOKIE_KEYS = %w[inkbunny-username inkbunny-password inkbunny-sid].freeze
TELEGRAM_KEYS = %w[telegram-bot-token].freeze
def index
authorize GlobalState
@global_states = policy_scope(GlobalState).order(:key)
@@ -182,6 +184,50 @@ class GlobalStatesController < ApplicationController
end
end
def telegram_config
authorize GlobalState
@telegram_config =
TELEGRAM_KEYS.map do |key|
GlobalState.find_by(key: key) ||
GlobalState.new(key: key, value_type: :string)
end
end
def edit_telegram_config
authorize GlobalState
@telegram_config =
TELEGRAM_KEYS.map do |key|
GlobalState.find_by(key: key) ||
GlobalState.new(key: key, value_type: :string)
end
end
def update_telegram_config
authorize GlobalState
begin
ActiveRecord::Base.transaction do
telegram_config_params.each do |key, value|
state = GlobalState.find_or_initialize_by(key: key)
state.value = value
state.value_type = :string
state.save!
end
end
redirect_to telegram_config_global_states_path,
notice: "Telegram bot configuration was successfully updated."
rescue ActiveRecord::RecordInvalid => e
@telegram_config =
TELEGRAM_KEYS.map do |key|
GlobalState.find_by(key: key) ||
GlobalState.new(key: key, value_type: :string)
end
flash.now[:alert] = "Error updating Telegram bot configuration: #{e.message}"
render :edit_telegram_config, status: :unprocessable_entity
end
end
private
def set_global_state
@@ -201,4 +247,8 @@ class GlobalStatesController < ApplicationController
*IB_COOKIE_KEYS.reject { |key| key == "inkbunny-sid" },
)
end
def telegram_config_params
params.require(:telegram_config).permit(*TELEGRAM_KEYS)
end
end

View File

@@ -625,6 +625,8 @@ export default function VisualSearchForm({
if (imageFile) {
await handleImageFile(imageFile);
// focus the drag/drop zone
dragDropRef.current?.focus();
} else {
showFeedback(
'No image found in clipboard. Copy an image first, then paste here.',

View File

@@ -28,50 +28,37 @@ class Tasks::Fa::QueryMissingPostsFromFuzzysearch < EnqueueJobBase
query =
query.where(fa_id: ..greatest_ok_post_fa_id) if greatest_ok_post_fa_id
query = query.where(fa_id: ..@start_at) if @start_at
query = query.where.missing(:file).order(fa_id: :desc)
log("finding greatest qualifying fa_id...")
greatest_post_fa_id = query.first&.fa_id
log("counting posts...")
count = GlobalState.get(COUNT_KEY)&.to_i || query.count
GlobalState.set(COUNT_KEY, count.to_s)
log("loading fa_ids...")
fa_ids = query.where.missing(:file).order(fa_id: :desc).pluck(:fa_id).uniq
count = fa_ids.count
puts "number of posts to process: #{count}"
pb = create_progress_bar(count)
while greatest_post_fa_id
posts = query.where(fa_id: ..greatest_post_fa_id).limit(32).to_a
break if posts.empty?
posts.each do |post|
fa_ids.each_slice(32) do |fa_ids_slice|
fa_ids_slice.each do |fa_id|
break if interrupted?
enqueue do
Domain::Fa::Job::ScanFuzzysearchJob.perform_later(
{ fa_id: post.fa_id },
)
Domain::Fa::Job::ScanFuzzysearchJob.perform_later({ fa_id: fa_id })
end
post_desc =
"#{(post.creator&.to_param || "(none)").rjust(20)} / #{post.to_param}".ljust(
40,
)
log("migrate post :: #{post_desc}") if pb.progress % 10 == 0
if pb.progress % 10 == 0
post = Domain::Post::FaPost.find_by!(fa_id: fa_id)
post_desc =
"#{(post.creator&.to_param || "(none)").rjust(20)} / #{post.to_param}".ljust(
40,
)
log("migrate post :: #{post_desc}")
end
rescue StandardError
log("error processing post :: #{post_desc}")
log("error processing post :: #{fa_id}")
ensure
pb.progress = [pb.progress + 1, pb.total].min
end
last_processed_fa_id = posts.map(&:fa_id).compact.min
if last_processed_fa_id
save_progress(last_processed_fa_id.to_s)
GlobalState.set(COUNT_KEY, (count - pb.progress).to_s)
greatest_post_fa_id = last_processed_fa_id - 1
else
break
end
save_progress(fa_ids_slice.last.to_s)
# Check for interruption after processing batch
break if interrupted?

View File

@@ -0,0 +1,309 @@
# typed: strict
require "telegram/bot"
require "tempfile"
require "net/http"
require "uri"
module Tasks
class TelegramBotTask
extend T::Sig
include Domain::VisualSearchHelper
include Domain::DomainModelHelper
sig { params(log_sink: T.any(IO, StringIO)).void }
def initialize(log_sink: $stderr)
@log_sink = log_sink
end
sig { void }
def run
bot_token = get_bot_token
if bot_token.nil?
log(
"❌ Telegram bot token not configured. Please set it in GlobalStates.",
)
log("Go to /state/telegram-config to configure the bot token.")
return
end
log("🤖 Starting Telegram bot...")
log("Bot token: #{bot_token[0..10]}..." + "*" * (bot_token.length - 11))
log("Press Ctrl+C to stop the bot")
begin
Telegram::Bot::Client.run(bot_token) do |bot|
log("✅ Telegram bot connected successfully")
bot.listen do |message|
begin
handle_message(bot, message)
rescue StandardError => e
log("❌ Error handling message: #{e.message}")
log("Backtrace: #{e.backtrace&.first(5)&.join("\n")}")
end
end
end
rescue Telegram::Bot::Exceptions::ResponseError => e
log("❌ Telegram API error: #{e.message}")
log("Please check your bot token configuration.")
rescue StandardError => e
log("❌ Unexpected error: #{e.message}")
log("Backtrace: #{e.backtrace&.first(5)&.join("\n")}")
ensure
log("🛑 Telegram bot stopped")
end
end
private
sig { params(message: String).void }
def log(message)
@log_sink.puts(message)
end
sig { returns(T.nilable(String)) }
def get_bot_token
GlobalState.get("telegram-bot-token")
end
sig do
params(
bot: Telegram::Bot::Client,
message: Telegram::Bot::Types::Message,
).void
end
def handle_message(bot, message)
return unless message.photo || message.document
chat_id = message.chat.id
# Send initial response
response_message =
bot.api.send_message(
chat_id: chat_id,
text: "🔍 Analyzing image... Please wait...",
reply_to_message_id: message.message_id,
)
begin
# Process the image and perform visual search
search_result = process_image_message(bot, message)
if search_result
result_text = format_search_results(search_result)
else
result_text =
"❌ Could not process the image. Please make sure it's a valid image file."
end
# Update the response with results
bot.api.edit_message_text(
chat_id: chat_id,
message_id: response_message.message_id,
text: result_text,
parse_mode: "Markdown",
)
rescue StandardError => e
log("Error processing image: #{e.message}")
# Update with error message
bot.api.edit_message_text(
chat_id: chat_id,
message_id: response_message.message_id,
text:
"❌ An error occurred while processing your image. Please try again.",
)
end
end
sig do
params(
bot: Telegram::Bot::Client,
message: Telegram::Bot::Types::Message,
).returns(
T.nilable(
T::Array[Domain::VisualSearchHelper::SimilarFingerprintResult],
),
)
end
def process_image_message(bot, message)
log("📥 Received image message from chat #{message.chat.id}")
# Get the largest photo or document
image_file = get_image_file_from_message(message)
return nil unless image_file
# Download the image to a temporary file
temp_file = download_telegram_image(bot, image_file)
return nil unless temp_file
begin
# Generate fingerprints
file_path = T.must(temp_file.path)
fingerprint_value =
Domain::PostFile::BitFingerprint.from_file_path(file_path)
detail_fingerprint_value =
Domain::PostFile::BitFingerprint.detail_from_file_path(file_path)
log("🔍 Generated fingerprints, searching for similar images...")
# Find similar fingerprints using the existing helper
similar_results =
find_similar_fingerprints(
fingerprint_value: fingerprint_value,
fingerprint_detail_value: detail_fingerprint_value,
limit: 20,
oversearch: 3,
includes: {
post_file: :post,
},
)
# Filter to only >90% similarity
high_quality_matches =
similar_results.select { |result| result.similarity_percentage > 90 }
log(
"✅ Found #{high_quality_matches.length} high-quality matches (>90% similarity)",
)
high_quality_matches
rescue StandardError => e
log("❌ Error processing image: #{e.message}")
nil
ensure
# Clean up temp file
temp_file.unlink if temp_file
end
end
sig do
params(
results: T::Array[Domain::VisualSearchHelper::SimilarFingerprintResult],
).returns(String)
end
def format_search_results(results)
return "❌ No close matches found." if results.empty?
response =
"🎯 Found #{results.length} #{"match".pluralize(results.length)}"
response += " (showing first 5)" if results.length > 5
response += "\n"
results
.take(5)
.each_with_index do |result, index|
post_file = result.fingerprint.post_file
next unless post_file
post = post_file.post
next unless post
response += "- "
percentage = result.similarity_percentage.round(1)
response += "#{percentage}%"
external_url = post.external_url_for_view
if (title = post.title) && external_url
response += " - [#{post.to_param} - #{title}](#{external_url})"
elsif external_url
response += " - [#{post.to_param}](#{external_url})"
else
response += " - #{post.to_param}"
end
if post.respond_to?(:creator) && (creator = post.send(:creator))
url = creator.external_url_for_view
if url
response += " by [#{creator.name_for_view}](#{url})"
else
response += " by #{creator.name_for_view}"
end
end
response += "\n"
end
response
end
# Extract image file information from Telegram message
sig do
params(message: Telegram::Bot::Types::Message).returns(
T.nilable(
T.any(
Telegram::Bot::Types::PhotoSize,
Telegram::Bot::Types::Document,
),
),
)
end
def get_image_file_from_message(message)
if message.photo && message.photo.any?
# Get the largest photo variant
message.photo.max_by { |photo| photo.file_size || 0 }
elsif message.document
# Check if document is an image
content_type = message.document.mime_type
if content_type&.start_with?("image/")
message.document
else
log("❌ Document is not an image: #{content_type}")
nil
end
else
log("❌ No image found in message")
nil
end
end
# Download image from Telegram and save to temporary file
sig do
params(
bot: Telegram::Bot::Client,
file_info:
T.any(
Telegram::Bot::Types::PhotoSize,
Telegram::Bot::Types::Document,
),
).returns(T.nilable(Tempfile))
end
def download_telegram_image(bot, file_info)
bot_token = get_bot_token
return nil unless bot_token
begin
# Get file path from Telegram
file_response = bot.api.get_file(file_id: file_info.file_id)
file_path = file_response.file_path
unless file_path
log("❌ Could not get file path from Telegram")
return nil
end
# Download the file
file_url = "https://api.telegram.org/file/bot#{bot_token}/#{file_path}"
log("📥 Downloading image from: #{file_url[0..50]}...")
uri = URI(file_url)
downloaded_data = Net::HTTP.get(uri)
# Create temporary file
extension = File.extname(file_path)
extension = ".jpg" if extension.empty?
temp_file = Tempfile.new(["telegram_image", extension])
temp_file.binmode
temp_file.write(downloaded_data)
temp_file.close
log("✅ Downloaded image to: #{temp_file.path}")
temp_file
rescue StandardError => e
log("❌ Error downloading image: #{e.message}")
nil
end
end
end
end

View File

@@ -57,6 +57,11 @@ class GlobalStatePolicy < ApplicationPolicy
is_real_user? && is_role_admin?
end
sig { returns(T::Boolean) }
def telegram_config?
is_real_user? && is_role_admin?
end
class Scope < ApplicationPolicy::Scope
extend T::Sig

View File

@@ -11,6 +11,37 @@
}
</style>
<% end %>
<% if log_entry && is_renderable_image_type?(log_entry.content_type) && (log_entry.status_code == 200) && (blob_entry = log_entry.response) %>
<% path = blob_url(
HexUtil.bin2hex(blob_entry.sha256),
format: extension_for_content_type(blob_entry.content_type),
thumb: "content-container",
)
%>
<meta name="og:image" content="<%= path %>">
<meta name="og:image:type" content="image/jpeg">
<meta name="og:image:width" content="1200">
<meta name="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="<%= path %>">
<meta name="twitter:image:alt" content="<%= @post.title %>">
<% end %>
<%
description = []
description << "posted #{@post.posted_at.strftime("%B %d, %Y")}" if @post.respond_to?(:posted_at) && @post.posted_at.present?
description << "by #{@post.primary_creator_for_view&.name || "Unknown"}"
description << "@ #{domain_name_for_model(@post)}"
%>
<meta name="og:description" content="<%= description.join(" ") %>">
<meta name="og:url" content="<%= domain_post_url(@post) %>">
<meta name="og:type" content="article">
<%
site_name = "#{domain_name_for_model(@post)} via ReFurrer"
%>
<meta name="og:title" content="<%= @post.title %>">
<meta name="twitter:title" content="<%= @post.title %>">
<meta name="og:site_name" content="<%= site_name %>">
<meta name="og:locale" content="en_US">
<% end %>
<div class="mx-auto mt-4 flex w-full max-w-2xl flex-col gap-4 pb-4">
<%= render_for_model(@post, "section_post_title", as: :post) %>

View File

@@ -70,7 +70,7 @@
<div class="px-2 pb-2 text-xs text-gray-600 mt-auto">
<div class="flex justify-between items-center flex-wrap gap-1">
<span class="flex items-center gap-0.5">
<% if creator = post.primary_creator_for_view %>
<% if creator = post.primary_creator_for_view || post.primary_fallback_creator_for_view %>
<span class="text-gray-500 italic">by</span>
<%= render "domain/has_description_html/inline_link_domain_user", user: creator, visual_style: "sky-link", size: "small" %>
<% elsif creator = post.primary_creator_name_fallback_for_view %>

View File

@@ -0,0 +1,39 @@
---
id: task-79
title: Build Telegram bot with visual image search
status: Done
assignee: []
created_date: '2025-07-31'
updated_date: '2025-07-31'
labels:
- telegram
- api
- visual-search
- bot
dependencies: []
---
## Description
Create a Telegram bot implemented as a long-running rake task that enables users to send images and receive visual search results based on the existing fingerprinting system, providing an alternative interface to the web-based visual search functionality
## Description
## Acceptance Criteria
- [ ] Bot is implemented as a long-running rake task
- [ ] Bot API token is managed through GlobalStatesController following existing patterns
- [ ] Bot can receive and process image messages
- [ ] Bot generates fingerprints from received images
- [ ] Bot finds and returns similar posts with >90% similarity only
- [ ] Bot handles both direct image uploads and image URLs in messages
- [ ] Bot provides user-friendly response format with high-quality match results
- [ ] Bot handles errors gracefully and provides helpful error messages
- [ ] Bot responds with 'no matches found' message when no results exceed 90% similarity
- [ ] Integration tests verify end-to-end functionality
## Implementation Plan
1. Research Telegram Bot API and Ruby gems (telegram-bot-ruby)\n2. Create rake task structure for long-running bot process\n3. Add Telegram bot token management to GlobalStatesController (following FA/IB cookie pattern)\n4. Implement image message handling and download functionality\n5. Integrate with existing fingerprinting system (Domain::PostFile::BitFingerprint)\n6. Implement visual search logic using existing helpers with >90% similarity filter\n7. Design user-friendly response format for high-quality matches only\n8. Add 'no matches found' response when no results exceed 90% similarity\n9. Add error handling and logging\n10. Write unit tests for bot functionality and similarity filtering\n11. Write integration tests for end-to-end flow\n12. Add documentation for deployment and configuration
## Implementation Notes
Successfully implemented core Telegram bot functionality:\n\n1. Added telegram-bot-ruby gem to Gemfile and configured properly\n2. Integrated bot token management with GlobalStatesController following existing patterns\n3. Created TelegramBotTask class with proper signal handling for long-running process\n4. Implemented complete image message handling:\n - Downloads images from Telegram API\n - Generates fingerprints using existing BitFingerprint system\n - Performs visual search using VisualSearchHelper\n - Filters results to >90% similarity only\n - Formats results with post details and URLs\n5. Added proper error handling and logging throughout\n6. Created rake tasks for running and testing the bot\n7. All code is properly typed with Sorbet and passes linting\n\nTechnical details:\n- Handles both photo messages and image documents\n- Downloads images to temporary files and cleans up properly\n- Uses existing fingerprinting system (BitFingerprint.from_file_path)\n- Integrates with VisualSearchHelper for similarity search\n- Returns formatted results with post ID, site name, similarity %, and URLs\n- Graceful error handling with user-friendly error messages\n\nFiles modified/created:\n- Gemfile (added telegram-bot-ruby)\n- app/controllers/global_states_controller.rb (token management)\n- config/routes.rb (telegram config routes)\n- app/lib/tasks/telegram_bot_task.rb (main bot logic)\n- rake/telegram.rake (rake tasks)\n\nReady for testing and deployment!

View File

@@ -0,0 +1,32 @@
---
id: task-79.1
title: Build UI for Telegram bot token configuration
status: To Do
assignee: []
created_date: '2025-07-31'
updated_date: '2025-07-31'
labels:
- ui
- views
- telegram
- global-states
dependencies: []
parent_task_id: task-79
---
## Description
Create the view templates and UI components for GlobalStatesController telegram configuration, following the same patterns as FA cookies and IB cookies configuration pages
## Acceptance Criteria
- [ ] Telegram config page displays current token status
- [ ] Users can view and edit telegram bot token
- [ ] Form validation and error handling works correctly
- [ ] UI follows existing design patterns from FA/IB config pages
- [ ] Success/error messages display appropriately
- [ ] Navigation and breadcrumbs work correctly
## Implementation Plan
1. Examine existing FA cookies and IB cookies view templates for patterns\n2. Create telegram_config.html.erb view template\n3. Create edit_telegram_config.html.erb view template\n4. Add form fields for telegram bot token input\n5. Implement proper form validation and error display\n6. Add navigation links in appropriate places (likely GlobalStates index)\n7. Style according to existing design system\n8. Test form submission and error handling\n9. Add any necessary flash message handling\n10. Ensure responsive design works on mobile

View File

@@ -114,6 +114,10 @@ Rails.application.routes.draw do
get "ib-cookies", to: "global_states#ib_cookies"
get "ib-cookies/edit", to: "global_states#edit_ib_cookies"
patch "ib-cookies", to: "global_states#update_ib_cookies"
get "telegram-config", to: "global_states#telegram_config"
get "telegram-config/edit", to: "global_states#edit_telegram_config"
patch "telegram-config", to: "global_states#update_telegram_config"
end
end

View File

@@ -1 +1,89 @@
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
User-agent: AI2Bot
User-agent: Ai2Bot-Dolma
User-agent: aiHitBot
User-agent: Amazonbot
User-agent: Andibot
User-agent: anthropic-ai
User-agent: Applebot
User-agent: Applebot-Extended
User-agent: Awario
User-agent: bedrockbot
User-agent: Brightbot 1.0
User-agent: Bytespider
User-agent: CCBot
User-agent: ChatGPT Agent
User-agent: ChatGPT-User
User-agent: Claude-SearchBot
User-agent: Claude-User
User-agent: Claude-Web
User-agent: ClaudeBot
User-agent: cohere-ai
User-agent: cohere-training-data-crawler
User-agent: Cotoyogi
User-agent: Crawlspace
User-agent: Datenbank Crawler
User-agent: Devin
User-agent: Diffbot
User-agent: DuckAssistBot
User-agent: Echobot Bot
User-agent: EchoboxBot
User-agent: FacebookBot
User-agent: facebookexternalhit
User-agent: Factset_spyderbot
User-agent: FirecrawlAgent
User-agent: FriendlyCrawler
User-agent: Gemini-Deep-Research
User-agent: Google-CloudVertexBot
User-agent: Google-Extended
User-agent: GoogleAgent-Mariner
User-agent: GoogleOther
User-agent: GoogleOther-Image
User-agent: GoogleOther-Video
User-agent: GPTBot
User-agent: iaskspider/2.0
User-agent: ICC-Crawler
User-agent: ImagesiftBot
User-agent: img2dataset
User-agent: ISSCyberRiskCrawler
User-agent: Kangaroo Bot
User-agent: meta-externalagent
User-agent: Meta-ExternalAgent
User-agent: meta-externalfetcher
User-agent: Meta-ExternalFetcher
User-agent: MistralAI-User
User-agent: MistralAI-User/1.0
User-agent: MyCentralAIScraperBot
User-agent: netEstate Imprint Crawler
User-agent: NovaAct
User-agent: OAI-SearchBot
User-agent: omgili
User-agent: omgilibot
User-agent: Operator
User-agent: PanguBot
User-agent: Panscient
User-agent: panscient.com
User-agent: Perplexity-User
User-agent: PerplexityBot
User-agent: PetalBot
User-agent: PhindBot
User-agent: Poseidon Research Crawler
User-agent: QualifiedBot
User-agent: QuillBot
User-agent: quillbot.com
User-agent: SBIntuitionsBot
User-agent: Scrapy
User-agent: SemrushBot-OCOB
User-agent: SemrushBot-SWA
User-agent: Sidetrade indexer bot
User-agent: Thinkbot
User-agent: TikTokSpider
User-agent: Timpibot
User-agent: VelenPublicWebCrawler
User-agent: WARDBot
User-agent: Webzio-Extended
User-agent: wpbot
User-agent: YaK
User-agent: YandexAdditional
User-agent: YandexAdditionalBot
User-agent: YouBot
Disallow: /

48
rake/telegram.rake Normal file
View File

@@ -0,0 +1,48 @@
# typed: true
# frozen_string_literal: true
T.bind(self, T.all(Rake::DSL, Object))
namespace :telegram do
desc "Start the Telegram bot for visual image search"
task bot: %i[set_logger_stdout environment] do |t, args|
puts "🤖 Starting Telegram Visual Search Bot".bold
puts "Press Ctrl+C to stop the bot cleanly".yellow
task = Tasks::TelegramBotTask.new
task.run
end
desc "Test Telegram bot configuration"
task test_config: %i[environment] do |t, args|
bot_token = GlobalState.get("telegram-bot-token")
if bot_token.nil?
puts "❌ Telegram bot token not configured".red
puts "Go to /state/telegram-config to configure the bot token."
exit 1
end
puts "✅ Telegram bot token is configured".green
puts "Token: #{bot_token[0..10]}..." + "*" * (bot_token.length - 11)
# Test connection to Telegram API
begin
require "telegram/bot"
Telegram::Bot::Client.run(bot_token) do |bot|
me = bot.api.get_me
puts "✅ Successfully connected to Telegram API".green
puts "Bot username: @#{me.username}" if me.username
puts "Bot name: #{me.first_name}"
puts "Bot ID: #{me.id}"
break # Exit after getting bot info
end
rescue Telegram::Bot::Exceptions::ResponseError => e
puts "❌ Failed to connect to Telegram API: #{e.message}".red
exit 1
rescue StandardError => e
puts "❌ Unexpected error: #{e.message}".red
exit 1
end
end
end

9
sorbet/rbi/gems/dry-core@1.1.0.rbi generated Normal file
View File

@@ -0,0 +1,9 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `dry-core` gem.
# Please instead update this file by running `bin/tapioca gem dry-core`.
# THIS IS AN EMPTY RBI FILE.
# see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem

9
sorbet/rbi/gems/dry-inflector@1.2.0.rbi generated Normal file
View File

@@ -0,0 +1,9 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `dry-inflector` gem.
# Please instead update this file by running `bin/tapioca gem dry-inflector`.
# THIS IS AN EMPTY RBI FILE.
# see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem

9
sorbet/rbi/gems/dry-logic@1.6.0.rbi generated Normal file
View File

@@ -0,0 +1,9 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `dry-logic` gem.
# Please instead update this file by running `bin/tapioca gem dry-logic`.
# THIS IS AN EMPTY RBI FILE.
# see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem

925
sorbet/rbi/gems/dry-struct@1.8.0.rbi generated Normal file
View File

@@ -0,0 +1,925 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `dry-struct` gem.
# Please instead update this file by running `bin/tapioca gem dry-struct`.
# source://dry-struct//lib/dry/struct/class_interface.rb#5
module Dry
class << self
# source://dry-core/1.1.0/lib/dry/core.rb#52
def Equalizer(*keys, **options); end
# Constructor method for easily creating a {Dry::Struct}.
#
# @example
# require 'dry-struct'
#
# module Types
# include Dry.Types()
# end
#
# Person = Dry.Struct(name: Types::String, age: Types::Integer)
# matz = Person.new(name: "Matz", age: 52)
# matz.name #=> "Matz"
# matz.age #=> 52
#
# Test = Dry.Struct(expected: Types::String) { schema(schema.strict) }
# Test[expected: "foo", unexpected: "bar"]
# #=> Dry::Struct::Error: [Test.new] unexpected keys [:unexpected] in Hash input
# @return [Dry::Struct]
#
# source://dry-struct//lib/dry/struct.rb#30
def Struct(attributes = T.unsafe(nil), &block); end
# source://dry-types/1.8.3/lib/dry/types.rb#253
def Types(*namespaces, default: T.unsafe(nil), **aliases); end
end
end
# Typed {Struct} with virtus-like DSL for defining schema.
#
# ### Differences between dry-struct and virtus
#
# {Struct} look somewhat similar to [Virtus][] but there are few significant differences:
#
# * {Struct}s don't provide attribute writers and are meant to be used
# as "data objects" exclusively.
# * Handling of attribute values is provided by standalone type objects from
# [`dry-types`][].
# * Handling of attribute hashes is provided by standalone hash schemas from
# [`dry-types`][].
# * Struct classes quack like [`dry-types`][], which means you can use them
# in hash schemas, as array members or sum them
#
# {Struct} class can specify a constructor type, which uses [hash schemas][]
# to handle attributes in `.new` method.
#
# [`dry-types`]: https://github.com/dry-rb/dry-types
# [Virtus]: https://github.com/solnic/virtus
# [hash schemas]: http://dry-rb.org/gems/dry-types/hash-schemas
#
# @example
# require 'dry-struct'
#
# module Types
# include Dry.Types()
# end
#
# class Book < Dry::Struct
# attribute :title, Types::String
# attribute :subtitle, Types::String.optional
# end
#
# rom_n_roda = Book.new(
# title: 'Web Development with ROM and Roda',
# subtitle: nil
# )
# rom_n_roda.title #=> 'Web Development with ROM and Roda'
# rom_n_roda.subtitle #=> nil
#
# refactoring = Book.new(
# title: 'Refactoring',
# subtitle: 'Improving the Design of Existing Code'
# )
# refactoring.title #=> 'Refactoring'
# refactoring.subtitle #=> 'Improving the Design of Existing Code'
#
# source://dry-struct//lib/dry/struct/class_interface.rb#6
class Dry::Struct
include ::Dry::Core::Constants
include ::Dry::Core::Equalizer::Methods
extend ::Dry::Core::Extensions
extend ::Dry::Core::Constants
extend ::Dry::Core::ClassAttributes
extend ::Dry::Types::Type
extend ::Dry::Types::Builder
extend ::Dry::Struct::ClassInterface
extend ::Dry::Core::Deprecations::Interface
# @param attributes [Hash, #each]
# @return [Struct] a new instance of Struct
#
# source://dry-struct//lib/dry/struct.rb#126
def initialize(attributes); end
# Retrieves value of previously defined attribute by its' `name`
#
# @example
# class Book < Dry::Struct
# attribute :title, Types::String
# attribute :subtitle, Types::String.optional
# end
#
# rom_n_roda = Book.new(
# title: 'Web Development with ROM and Roda',
# subtitle: nil
# )
# rom_n_roda[:title] #=> 'Web Development with ROM and Roda'
# rom_n_roda[:subtitle] #=> nil
# @param name [String]
# @return [Object]
#
# source://dry-struct//lib/dry/struct.rb#147
def [](name); end
# Returns the value of attribute attributes.
#
# source://dry-struct//lib/dry/struct.rb#122
def __attributes__; end
# Create a copy of {Dry::Struct} with overriden attributes
#
# @example
# class Book < Dry::Struct
# attribute :title, Types::String
# attribute :subtitle, Types::String.optional
# end
#
# rom_n_roda = Book.new(
# title: 'Web Development with ROM and Roda',
# subtitle: '2nd edition'
# )
# #=> #<Book title="Web Development with ROM and Roda" subtitle="2nd edition">
#
# rom_n_roda.new(subtitle: '3rd edition')
# #=> #<Book title="Web Development with ROM and Roda" subtitle="3rd edition">
# @param changeset [Hash{Symbol => Object}]
# @return [Struct]
#
# source://dry-struct//lib/dry/struct.rb#202
def __new__(changeset); end
# Returns the value of attribute attributes.
#
# source://dry-struct//lib/dry/struct.rb#122
def attributes; end
# Pattern matching support
#
# @api private
#
# source://dry-struct//lib/dry/struct.rb#224
def deconstruct_keys(_keys); end
# @return [String]
#
# source://dry-struct//lib/dry/struct.rb#215
def inspect; end
# Create a copy of {Dry::Struct} with overriden attributes
#
# @example
# class Book < Dry::Struct
# attribute :title, Types::String
# attribute :subtitle, Types::String.optional
# end
#
# rom_n_roda = Book.new(
# title: 'Web Development with ROM and Roda',
# subtitle: '2nd edition'
# )
# #=> #<Book title="Web Development with ROM and Roda" subtitle="2nd edition">
#
# rom_n_roda.new(subtitle: '3rd edition')
# #=> #<Book title="Web Development with ROM and Roda" subtitle="3rd edition">
# @param changeset [Hash{Symbol => Object}]
# @return [Struct]
#
# source://dry-struct//lib/dry/struct.rb#202
def new(changeset); end
# Converts the {Dry::Struct} to a hash with keys representing
# each attribute (as symbols) and their corresponding values
#
# @example
# class Book < Dry::Struct
# attribute :title, Types::String
# attribute :subtitle, Types::String.optional
# end
#
# rom_n_roda = Book.new(
# title: 'Web Development with ROM and Roda',
# subtitle: nil
# )
# rom_n_roda.to_hash
# #=> {title: 'Web Development with ROM and Roda', subtitle: nil}
# @return [Hash{Symbol => Object}]
#
# source://dry-struct//lib/dry/struct.rb#174
def to_h; end
# Converts the {Dry::Struct} to a hash with keys representing
# each attribute (as symbols) and their corresponding values
# TODO: remove in 2.0
#
# @example
# class Book < Dry::Struct
# attribute :title, Types::String
# attribute :subtitle, Types::String.optional
# end
#
# rom_n_roda = Book.new(
# title: 'Web Development with ROM and Roda',
# subtitle: nil
# )
# rom_n_roda.to_hash
# #=> {title: 'Web Development with ROM and Roda', subtitle: nil}
# @return [Hash{Symbol => Object}]
#
# source://dry-struct//lib/dry/struct.rb#174
def to_hash; end
class << self
# source://dry-struct//lib/dry/struct.rb#94
def loader; end
def prepend(*_arg0); end
end
end
# Class-level interface of {Struct} and {Value}
#
# source://dry-struct//lib/dry/struct/class_interface.rb#8
module Dry::Struct::ClassInterface
include ::Dry::Core::Constants
include ::Dry::Core::ClassAttributes
include ::Dry::Types::Type
include ::Dry::Types::Builder
# @param other [Object, Dry::Struct]
# @return [Boolean]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#331
def ===(other); end
# Make the struct abstract. This class will be used as a default
# parent class for nested structs
#
# source://dry-struct//lib/dry/struct/class_interface.rb#387
def abstract; end
# Adds an attribute for this {Struct} with given `name` and `type`
# and modifies {.schema} accordingly.
#
# @example with nested structs
# class Language < Dry::Struct
# attribute :name, Types::String
# attribute :details, Dry::Struct do
# attribute :type, Types::String
# end
# end
#
# Language.schema # new lines for readability
# # => #<Dry::Types[
# Constructor<Schema<keys={
# name: Constrained<Nominal<String> rule=[type?(String)]>
# details: Language::Details
# }> fn=Kernel.Hash>]>
#
# ruby = Language.new(name: 'Ruby', details: { type: 'OO' })
# ruby.name #=> 'Ruby'
# ruby.details #=> #<Language::Details type="OO">
# ruby.details.type #=> 'OO'
# @example with a nested array of structs
# class Language < Dry::Struct
# attribute :name, Types::String
# attribute :versions, Types::Array.of(Types::String)
# attribute :celebrities, Types::Array.of(Dry::Struct) do
# attribute :name, Types::String
# attribute :pseudonym, Types::String
# end
# end
#
# Language.schema # new lines for readability
# => #<Dry::Types[Constructor<Schema<keys={
# name: Constrained<Nominal<String> rule=[type?(String)]>
# versions: Constrained<
# Array<Constrained<Nominal<String> rule=[type?(String)]>
# > rule=[type?(Array)]>
# celebrities: Constrained<Array<Language::Celebrity> rule=[type?(Array)]>
# }> fn=Kernel.Hash>]>
#
# ruby = Language.new(
# name: 'Ruby',
# versions: %w(1.8.7 1.9.8 2.0.1),
# celebrities: [
# { name: 'Yukihiro Matsumoto', pseudonym: 'Matz' },
# { name: 'Aaron Patterson', pseudonym: 'tenderlove' }
# ]
# )
# ruby.name #=> 'Ruby'
# ruby.versions #=> ['1.8.7', '1.9.8', '2.0.1']
# ruby.celebrities
# #=> [
# #<Language::Celebrity name='Yukihiro Matsumoto' pseudonym='Matz'>,
# #<Language::Celebrity name='Aaron Patterson' pseudonym='tenderlove'>
# ]
# ruby.celebrities[0].name #=> 'Yukihiro Matsumoto'
# ruby.celebrities[0].pseudonym #=> 'Matz'
# ruby.celebrities[1].name #=> 'Aaron Patterson'
# ruby.celebrities[1].pseudonym #=> 'tenderlove'
# @param name [Symbol] name of the defined attribute
# @param type [Dry::Types::Type, nil] or superclass of nested type
# @raise [RepeatedAttributeError] when trying to define attribute with the
# same name as previously defined one
# @return [Dry::Struct]
# @yield If a block is given, it will be evaluated in the context of
# a new struct class, and set as a nested type for the given
# attribute. A class with a matching name will also be defined for
# the nested type.
#
# source://dry-struct//lib/dry/struct/class_interface.rb#86
def attribute(name, type = T.unsafe(nil), &_arg2); end
# Adds an omittable (key is not required on initialization) attribute for this {Struct}
#
# @example
# class User < Dry::Struct
# attribute :name, Types::String
# attribute? :email, Types::String
# end
#
# User.new(name: 'John') # => #<User name="John" email=nil>
# @param name [Symbol] name of the defined attribute
# @param type [Dry::Types::Type, nil] or superclass of nested type
# @return [Dry::Struct]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#139
def attribute?(*args, &_arg1); end
# Gets the list of attribute names
#
# @return [Array<Symbol>]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#357
def attribute_names; end
# @example
# class Book < Dry::Struct
# attributes(
# title: Types::String,
# author: Types::String
# )
# end
#
# Book.schema
# # => #<Dry::Types[Constructor<Schema<keys={
# # title: Constrained<Nominal<String> rule=[type?(String)]>
# # author: Constrained<Nominal<String> rule=[type?(String)]>
# # }> fn=Kernel.Hash>]>
# @param new_schema [Hash{Symbol => Dry::Types::Type}]
# @raise [RepeatedAttributeError] when trying to define attribute with the
# same name as previously defined one
# @return [Dry::Struct]
# @see #attribute
#
# source://dry-struct//lib/dry/struct/class_interface.rb#173
def attributes(new_schema); end
# Add atributes from another struct
#
# @example
# class Address < Dry::Struct
# attribute :city, Types::String
# attribute :country, Types::String
# end
#
# class User < Dry::Struct
# attribute :name, Types::String
# attributes_from Address
# end
#
# User.new(name: 'Quispe', city: 'La Paz', country: 'Bolivia')
# @example with nested structs
# class User < Dry::Struct
# attribute :name, Types::String
# attribute :address do
# attributes_from Address
# end
# end
# @param struct [Dry::Struct]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#114
def attributes_from(struct); end
# @api private
#
# source://dry-struct//lib/dry/struct/class_interface.rb#261
def call_safe(input, &_arg1); end
# @api private
#
# source://dry-struct//lib/dry/struct/class_interface.rb#270
def call_unsafe(input); end
# @return [true]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#335
def constrained?; end
# @param constructor [#call, nil]
# @param block [#call, nil]
# @return [Dry::Struct::Constructor]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#288
def constructor(constructor = T.unsafe(nil), **_arg1, &block); end
# @return [false]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#327
def default?; end
# @param args [({Symbol => Object})]
# @return [Dry::Types::Result::Failure]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#320
def failure(*args); end
# Checks if this {Struct} has the given attribute
#
# @param key [Symbol] Attribute name
# @return [Boolean]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#352
def has_attribute?(key); end
# @api private
#
# source://dry-struct//lib/dry/struct/class_interface.rb#279
def load(attributes); end
# @return [{Symbol => Object}]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#362
def meta(meta = T.unsafe(nil)); end
# @param attributes [Hash{Symbol => Object}, Dry::Struct]
# @raise [Struct::Error] if the given attributes don't conform {#schema}
#
# source://dry-struct//lib/dry/struct/class_interface.rb#239
def new(attributes = T.unsafe(nil), safe = T.unsafe(nil), &_arg2); end
# @return [false]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#341
def optional?; end
# @return [self]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#338
def primitive; end
# @param other [Object, Dry::Struct]
# @return [Boolean]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#331
def primitive?(other); end
# @param klass [Class]
# @param args [({Symbol => Object})]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#324
def result(klass, *args); end
# @param args [({Symbol => Object})]
# @return [Dry::Types::Result::Success]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#316
def success(*args); end
# Dump to the AST
#
# @api public
# @return [Array]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#396
def to_ast(meta: T.unsafe(nil)); end
# @return [Proc]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#344
def to_proc; end
# Add an arbitrary transformation for input hash keys.
#
# @example
# class Book < Dry::Struct
# transform_keys(&:to_sym)
#
# attribute :title, Types::String
# end
#
# Book.new('title' => "The Old Man and the Sea")
# # => #<Book title="The Old Man and the Sea">
# @param proc [#call, nil]
# @param block [#call, nil]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#221
def transform_keys(proc = T.unsafe(nil), &block); end
# Add an arbitrary transformation for new attribute types.
#
# @example
# class Book < Dry::Struct
# transform_types { |t| t.meta(struct: :Book) }
#
# attribute :title, Types::String
# end
#
# Book.schema.key(:title).meta # => { struct: :Book }
# @param proc [#call, nil]
# @param block [#call, nil]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#204
def transform_types(proc = T.unsafe(nil), &block); end
# @param input [Hash{Symbol => Object}, Dry::Struct]
# @return [Dry::Types::Result]
# @yieldparam failure [Dry::Types::Result::Failure]
# @yieldreturn [Dry::Types::Result]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#296
def try(input); end
# @param input [Hash{Symbol => Object}, Dry::Struct]
# @private
# @return [Dry::Types::Result]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#306
def try_struct(input); end
# Build a sum type
#
# @param type [Dry::Types::Type]
# @return [Dry::Types::Sum]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#377
def |(type); end
private
# Constructs a type
#
# @return [Dry::Types::Type, Dry::Struct]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#430
def build_type(name, type = T.unsafe(nil), &_arg2); end
# @param new_keys [Hash{Symbol => Dry::Types::Type, Dry::Struct}]
# @raise [RepeatedAttributeError] when trying to define attribute with the
# same name as previously defined one
#
# source://dry-struct//lib/dry/struct/class_interface.rb#228
def check_schema_duplication(new_keys); end
# Retrieves default attributes from defined {.schema}.
# Used in a {Struct} constructor if no attributes provided to {.new}
#
# @return [Hash{Symbol => Object}]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#411
def default_attributes(default_schema = T.unsafe(nil)); end
# @api private
#
# source://dry-struct//lib/dry/struct/class_interface.rb#452
def define_accessors(keys); end
# Checks if the given type is a Dry::Struct
#
# @param type [Dry::Types::Type]
# @return [Boolean]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#422
def struct?(type); end
# Stores an object for building nested struct classes
#
# @return [StructBuilder]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#402
def struct_builder; end
# @api private
# @return [Boolean]
#
# source://dry-struct//lib/dry/struct/class_interface.rb#468
def valid_method_name?(key); end
end
# source://dry-struct//lib/dry/struct/compiler.rb#5
class Dry::Struct::Compiler < ::Dry::Types::Compiler
# source://dry-struct//lib/dry/struct/compiler.rb#6
def visit_struct(node); end
end
# source://dry-struct//lib/dry/struct/constructor.rb#5
class Dry::Struct::Constructor < ::Dry::Types::Constructor
# source://dry-types/1.8.3/lib/dry/types/constructor.rb#16
def primitive; end
end
# Raised when given input doesn't conform schema and constructor type
#
# source://dry-struct//lib/dry/struct/errors.rb#6
class Dry::Struct::Error < ::Dry::Types::CoercionError; end
# Helper for {Struct#to_hash} implementation
#
# source://dry-struct//lib/dry/struct/hashify.rb#6
module Dry::Struct::Hashify
class << self
# Converts value to hash recursively
#
# @param value [#to_hash, #map, Object]
# @return [Hash, Array]
#
# source://dry-struct//lib/dry/struct/hashify.rb#10
def [](value); end
end
end
# Raised when a struct doesn't have an attribute
#
# source://dry-struct//lib/dry/struct/errors.rb#18
class Dry::Struct::MissingAttributeError < ::KeyError
# @return [MissingAttributeError] a new instance of MissingAttributeError
#
# source://dry-struct//lib/dry/struct/errors.rb#19
def initialize(attribute:, klass:); end
end
# When struct class stored in ast was garbage collected because no alive objects exists
# This shouldn't happen in a working application
#
# source://dry-struct//lib/dry/struct/errors.rb#26
class Dry::Struct::RecycledStructError < ::RuntimeError
# @return [RecycledStructError] a new instance of RecycledStructError
#
# source://dry-struct//lib/dry/struct/errors.rb#27
def initialize; end
end
# Raised when defining duplicate attributes
#
# source://dry-struct//lib/dry/struct/errors.rb#9
class Dry::Struct::RepeatedAttributeError < ::ArgumentError
# @param key [Symbol] attribute name that is the same as previously defined one
# @return [RepeatedAttributeError] a new instance of RepeatedAttributeError
#
# source://dry-struct//lib/dry/struct/errors.rb#12
def initialize(key); end
end
# @private
#
# source://dry-struct//lib/dry/struct/struct_builder.rb#6
class Dry::Struct::StructBuilder < ::Dry::Struct::Compiler
# @return [StructBuilder] a new instance of StructBuilder
#
# source://dry-struct//lib/dry/struct/struct_builder.rb#9
def initialize(struct); end
# @param attr_name [Symbol|String] the name of the nested type
# @param type [Dry::Struct, Dry::Types::Type::Array, Undefined] the superclass
# of the nested struct
# @yield the body of the nested struct
#
# source://dry-struct//lib/dry/struct/struct_builder.rb#18
def call(attr_name, type, &block); end
# Returns the value of attribute struct.
#
# source://dry-struct//lib/dry/struct/struct_builder.rb#7
def struct; end
private
# @return [Boolean]
#
# source://dry-struct//lib/dry/struct/struct_builder.rb#48
def array?(type); end
# source://dry-struct//lib/dry/struct/struct_builder.rb#75
def check_name(name); end
# source://dry-struct//lib/dry/struct/struct_builder.rb#64
def const_name(type, attr_name); end
# @return [Boolean]
#
# source://dry-struct//lib/dry/struct/struct_builder.rb#52
def optional?(type); end
# source://dry-struct//lib/dry/struct/struct_builder.rb#54
def parent(type); end
# @return [Boolean]
#
# source://dry-struct//lib/dry/struct/struct_builder.rb#46
def type?(type); end
# source://dry-struct//lib/dry/struct/struct_builder.rb#89
def visit_array(node); end
# source://dry-struct//lib/dry/struct/struct_builder.rb#84
def visit_constrained(node); end
# source://dry-struct//lib/dry/struct/struct_builder.rb#96
def visit_constructor(node); end
# source://dry-struct//lib/dry/struct/struct_builder.rb#94
def visit_nominal(*_arg0); end
end
# A sum type of two or more structs
# As opposed to Dry::Types::Sum::Constrained
# this type tries no to coerce data first.
#
# source://dry-struct//lib/dry/struct/sum.rb#8
class Dry::Struct::Sum < ::Dry::Types::Sum::Constrained
# @return [boolean]
#
# source://dry-struct//lib/dry/struct/sum.rb#39
def ===(value); end
# source://dry-struct//lib/dry/struct/sum.rb#9
def call(input); end
# @param input [Hash{Symbol => Object}, Dry::Struct]
# @return [Dry::Types::Result]
# @yieldparam failure [Dry::Types::Result::Failure]
# @yieldreturn [Dry::Types::Result]
#
# source://dry-struct//lib/dry/struct/sum.rb#19
def try(input); end
# Build a new sum type
#
# @param type [Dry::Types::Type]
# @return [Dry::Types::Sum]
#
# source://dry-struct//lib/dry/struct/sum.rb#30
def |(type); end
protected
# @private
#
# source://dry-struct//lib/dry/struct/sum.rb#44
def try_struct(input, &block); end
end
# @private
#
# source://dry-struct//lib/dry/struct/version.rb#6
Dry::Struct::VERSION = T.let(T.unsafe(nil), String)
# {Value} objects behave like {Struct}s but *deeply frozen*
# using [`ice_nine`](https://github.com/dkubb/ice_nine)
#
# @example
# class Location < Dry::Struct::Value
# attribute :lat, Types::Float
# attribute :lng, Types::Float
# end
#
# loc1 = Location.new(lat: 1.23, lng: 4.56)
# loc2 = Location.new(lat: 1.23, lng: 4.56)
#
# loc1.frozen? #=> true
# loc2.frozen? #=> true
# loc1 == loc2 #=> true
# @see https://github.com/dkubb/ice_nine
#
# source://dry-struct//lib/dry/struct/value.rb#26
class Dry::Struct::Value < ::Dry::Struct
class << self
# @param attributes [Hash{Symbol => Object}, Dry::Struct]
# @return [Value]
# @see https://github.com/dkubb/ice_nine
#
# source://dry-struct//lib/dry/struct/value.rb#32
def new(*_arg0); end
end
end
# source://dry-struct//lib/dry/struct/printer.rb#6
module Dry::Types
extend ::Dry::Core::Constants
class << self
# source://dry-types/1.8.3/lib/dry/types/constraints.rb#13
def Rule(options); end
# source://dry-types/1.8.3/lib/dry/types.rb#115
def [](name); end
# source://dry-types/1.8.3/lib/dry/types.rb#163
def const_missing(const); end
# source://dry-types/1.8.3/lib/dry/types.rb#82
def container; end
# source://dry-types/1.8.3/lib/dry/types.rb#197
def define_builder(method, &block); end
# source://dry-types/1.8.3/lib/dry/types.rb#149
def identifier(klass); end
# source://dry-types/1.8.3/lib/dry/types.rb#73
def included(*_arg0); end
# source://dry-types/1.8.3/lib/dry/types.rb#33
def loader; end
# source://dry-core/1.1.0/lib/dry/core/deprecations.rb#202
def module(*args, &block); end
# source://dry-types/1.8.3/lib/dry/types.rb#104
def register(name, type = T.unsafe(nil), &block); end
# source://dry-types/1.8.3/lib/dry/types.rb#91
def registered?(class_or_identifier); end
# source://dry-types/1.8.3/lib/dry/types/constraints.rb#26
def rule_compiler; end
# source://dry-types/1.8.3/lib/dry/types.rb#158
def type_map; end
end
end
# @api private
#
# source://dry-struct//lib/dry/struct/printer.rb#8
class Dry::Types::Printer
# source://dry-types/1.8.3/lib/dry/types/printer.rb#38
def initialize; end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#43
def call(type); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#49
def visit(type, &_arg1); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#62
def visit_any(_); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#64
def visit_array(type); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#70
def visit_array_member(array); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#152
def visit_callable(callable); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#102
def visit_composition(composition, &_arg1); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#91
def visit_constrained(constrained); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#78
def visit_constructor(constructor); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#126
def visit_default(default); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#108
def visit_enum(enum); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#250
def visit_hash(hash); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#240
def visit_key(key); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#146
def visit_lax(lax); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#226
def visit_map(map); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#140
def visit_nominal(type); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#269
def visit_options(options, meta = T.unsafe(nil)); end
# source://dry-types/1.8.3/lib/dry/types/printer.rb#187
def visit_schema(schema); end
# @api private
#
# source://dry-types/1.8.3/lib/dry/types/printer.rb#78
def visit_struct_constructor(constructor); end
# @api private
#
# source://dry-struct//lib/dry/struct/printer.rb#12
def visit_struct_sum(sum); end
end

3971
sorbet/rbi/gems/dry-types@1.8.3.rbi generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `faraday-multipart` gem.
# Please instead update this file by running `bin/tapioca gem faraday-multipart`.
# THIS IS AN EMPTY RBI FILE.
# see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem

View File

@@ -0,0 +1,9 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `faraday-net_http` gem.
# Please instead update this file by running `bin/tapioca gem faraday-net_http`.
# THIS IS AN EMPTY RBI FILE.
# see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem

9
sorbet/rbi/gems/faraday@2.13.4.rbi generated Normal file
View File

@@ -0,0 +1,9 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `faraday` gem.
# Please instead update this file by running `bin/tapioca gem faraday`.
# THIS IS AN EMPTY RBI FILE.
# see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem

9
sorbet/rbi/gems/ice_nine@0.11.2.rbi generated Normal file
View File

@@ -0,0 +1,9 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `ice_nine` gem.
# Please instead update this file by running `bin/tapioca gem ice_nine`.
# THIS IS AN EMPTY RBI FILE.
# see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem

2032
sorbet/rbi/gems/json@2.13.2.rbi generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `multipart-post` gem.
# Please instead update this file by running `bin/tapioca gem multipart-post`.
# THIS IS AN EMPTY RBI FILE.
# see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem

4096
sorbet/rbi/gems/net-http@0.6.0.rbi generated Normal file

File diff suppressed because it is too large Load Diff

4716
sorbet/rbi/gems/telegram-bot-ruby@2.4.0.rbi generated Normal file

File diff suppressed because it is too large Load Diff

2426
sorbet/rbi/gems/uri@1.0.3.rbi generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
# typed: strict
# Shim for telegram-bot-ruby to provide proper typing for Client.run method
module Telegram
module Bot
class Client
sig do
params(
args: T.untyped,
blk: T.proc.params(bot: Telegram::Bot::Client).void,
).void
end
def self.run(*args, &blk)
end
# Override the api method to properly type the return value
sig { returns(Telegram::Bot::Api) }
def api
end
end
class Api
# Define commonly used API methods
sig do
params(
chat_id: T.untyped,
text: String,
reply_to_message_id: T.untyped,
).returns(Telegram::Bot::Types::Message)
end
def send_message(chat_id:, text:, reply_to_message_id: nil)
end
sig do
params(
chat_id: T.untyped,
message_id: T.untyped,
text: String,
parse_mode: T.nilable(String),
).returns(Telegram::Bot::Types::Message)
end
def edit_message_text(chat_id:, message_id:, text:, parse_mode: nil)
end
sig { params(file_id: String).returns(Telegram::Bot::Types::File) }
def get_file(file_id:)
end
sig { returns(Telegram::Bot::Types::User) }
def get_me
end
end
end
end

View File

@@ -6,6 +6,17 @@
module ::MakeMakefile; end
module ::Spring; end
module Dry::Core::Cache; end
module Dry::Core::Cache::Methods; end
module Dry::Core::ClassAttributes; end
module Dry::Core::Constants; end
module Dry::Core::Container::Configuration; end
module Dry::Core::Container::Mixin; end
module Dry::Core::Container::Mixin::Initializer; end
module Dry::Core::Deprecations::Interface; end
module Dry::Core::Equalizer::Methods; end
module Dry::Core::Extensions; end
module Dry::Inflector; end
module SyntaxTree::Haml; end
module SyntaxTree::Haml::Format::Formatter; end
module SyntaxTree::RBS; end

View File

@@ -28,6 +28,11 @@ require "sorbet-runtime"
require "syntax_tree"
require "timeout"
require "rake/dsl_definition"
require "dry-types"
require "dry-struct"
require "dry/core"
require "dry/types"
require "telegram/bot"
require "prometheus_exporter/client"
require "prometheus_exporter/metric"