Compare commits
23 Commits
7aaf471f09
...
83ae4ebd45
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83ae4ebd45 | ||
|
|
d899413d7c | ||
|
|
0fe7040935 | ||
|
|
2be92ac365 | ||
|
|
210b3b05c7 | ||
|
|
3d35a8b3b9 | ||
|
|
544f6764d4 | ||
|
|
67339142dd | ||
|
|
d892e00471 | ||
|
|
d85d04ea53 | ||
|
|
6fa15fdafc | ||
|
|
611b20c146 | ||
|
|
a497fa4adf | ||
|
|
35fe54ccc7 | ||
|
|
a3898e8dba | ||
|
|
68776f74c6 | ||
|
|
b0356111b6 | ||
|
|
9f33f26b2b | ||
|
|
4e2bd344fa | ||
|
|
55dfc81436 | ||
|
|
118a0c58c2 | ||
|
|
e598529639 | ||
|
|
eefcd9eb93 |
5
Gemfile
5
Gemfile
@@ -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"
|
||||
|
||||
42
Gemfile.lock
42
Gemfile.lock
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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?
|
||||
|
||||
309
app/lib/tasks/telegram_bot_task.rb
Normal file
309
app/lib/tasks/telegram_bot_task.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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!
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
48
rake/telegram.rake
Normal 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
9
sorbet/rbi/gems/dry-core@1.1.0.rbi
generated
Normal 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
9
sorbet/rbi/gems/dry-inflector@1.2.0.rbi
generated
Normal 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
9
sorbet/rbi/gems/dry-logic@1.6.0.rbi
generated
Normal 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
925
sorbet/rbi/gems/dry-struct@1.8.0.rbi
generated
Normal 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
3971
sorbet/rbi/gems/dry-types@1.8.3.rbi
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
sorbet/rbi/gems/faraday-multipart@1.1.1.rbi
generated
Normal file
9
sorbet/rbi/gems/faraday-multipart@1.1.1.rbi
generated
Normal 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
|
||||
9
sorbet/rbi/gems/faraday-net_http@3.4.1.rbi
generated
Normal file
9
sorbet/rbi/gems/faraday-net_http@3.4.1.rbi
generated
Normal 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
9
sorbet/rbi/gems/faraday@2.13.4.rbi
generated
Normal 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
9
sorbet/rbi/gems/ice_nine@0.11.2.rbi
generated
Normal 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
2032
sorbet/rbi/gems/json@2.13.2.rbi
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
sorbet/rbi/gems/multipart-post@2.4.1.rbi
generated
Normal file
9
sorbet/rbi/gems/multipart-post@2.4.1.rbi
generated
Normal 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
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
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
2426
sorbet/rbi/gems/uri@1.0.3.rbi
generated
Normal file
File diff suppressed because it is too large
Load Diff
54
sorbet/rbi/shims/telegram-bot-ruby.rbi
Normal file
54
sorbet/rbi/shims/telegram-bot-ruby.rbi
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user