...
 
Commits (5)
......@@ -6,64 +6,64 @@ require "../../src/repository/watch_targets"
module Repository
class WatchTargetsTest < Minitest::Test
def test_add
redis.hset "ddw:wt:nova", "Extralarge", "[420]"
Repository::WatchTargets.add(1337, "nova", BadDragon::Size::Extralarge)
redis.hset "ddw:wt:nova_spec", "Extralarge", "[420]"
Repository::WatchTargets.add(1337, "nova_spec", BadDragon::Size::Extralarge)
users = JSON.parse(redis.hget("ddw:wt:nova", "Extralarge") || "[]").as_a.map(&.as_i)
users = JSON.parse(redis.hget("ddw:wt:nova_spec", "Extralarge") || "[]").as_a.map(&.as_i)
assert_equal [420, 1337], users
end
def test_add_same_user
redis.hset "ddw:wt:nova", "Extralarge", "[420, 1337]"
Repository::WatchTargets.add(1337, "nova", BadDragon::Size::Extralarge)
redis.hset "ddw:wt:nova_spec", "Extralarge", "[420, 1337]"
Repository::WatchTargets.add(1337, "nova_spec", BadDragon::Size::Extralarge)
users = JSON.parse(redis.hget("ddw:wt:nova", "Extralarge") || "[]").as_a.map(&.as_i)
users = JSON.parse(redis.hget("ddw:wt:nova_spec", "Extralarge") || "[]").as_a.map(&.as_i)
assert_equal [420, 1337], users
end
def test_add_nonexistent_key
redis.del "ddw:wt:nova"
Repository::WatchTargets.add(1337, "nova", BadDragon::Size::Extralarge)
redis.del "ddw:wt:nova_spec"
Repository::WatchTargets.add(1337, "nova_spec", BadDragon::Size::Extralarge)
users = JSON.parse(redis.hget("ddw:wt:nova", "Extralarge") || "[]").as_a.map(&.as_i)
users = JSON.parse(redis.hget("ddw:wt:nova_spec", "Extralarge") || "[]").as_a.map(&.as_i)
assert_equal [1337], users
end
def test_remove
redis.hset "ddw:wt:nova", "Extralarge", "[1337, 69, 420]"
Repository::WatchTargets.remove(69, "nova", BadDragon::Size::Extralarge)
redis.hset "ddw:wt:nova_spec", "Extralarge", "[1337, 69, 420]"
Repository::WatchTargets.remove(69, "nova_spec", BadDragon::Size::Extralarge)
users = JSON.parse(redis.hget("ddw:wt:nova", "Extralarge") || "[]").as_a.map(&.as_i)
users = JSON.parse(redis.hget("ddw:wt:nova_spec", "Extralarge") || "[]").as_a.map(&.as_i)
assert_equal [1337, 420], users
end
def test_remove_nonexistent_user
redis.hset "ddw:wt:nova", "Extralarge", "[1337, 69, 420]"
Repository::WatchTargets.remove(666, "nova", BadDragon::Size::Extralarge)
redis.hset "ddw:wt:nova_spec", "Extralarge", "[1337, 69, 420]"
Repository::WatchTargets.remove(666, "nova_spec", BadDragon::Size::Extralarge)
users = JSON.parse(redis.hget("ddw:wt:nova", "Extralarge") || "[]").as_a.map(&.as_i)
users = JSON.parse(redis.hget("ddw:wt:nova_spec", "Extralarge") || "[]").as_a.map(&.as_i)
assert_equal [1337, 69, 420], users
end
def test_remove_nonexistent_key
redis.del "ddw:wt:nova"
Repository::WatchTargets.remove(1337, "nova", BadDragon::Size::Extralarge)
redis.del "ddw:wt:nova_spec"
Repository::WatchTargets.remove(1337, "nova_spec", BadDragon::Size::Extralarge)
redis_response = redis.hget("ddw:wt:nova", "Extralarge")
redis_response = redis.hget("ddw:wt:nova_spec", "Extralarge")
assert_equal "[]", redis_response
end
def test_watchers
redis.hset "ddw:wt:nova", "Extralarge", "[1337, 69, 420]"
redis.hset "ddw:wt:nova_spec", "Extralarge", "[1337, 69, 420]"
users = Repository::WatchTargets.watchers("nova", BadDragon::Size::Extralarge)
users = Repository::WatchTargets.watchers("nova_spec", BadDragon::Size::Extralarge)
assert_equal [1337, 69, 420], users
end
def test_watchers_nonexistent_key
redis.del "ddw:wt:nova"
redis.del "ddw:wt:nova_spec"
users = Repository::WatchTargets.watchers("nova", BadDragon::Size::Extralarge)
users = Repository::WatchTargets.watchers("nova_spec", BadDragon::Size::Extralarge)
assert_equal [] of Int32, users
end
......
......@@ -9,7 +9,7 @@ module BadDragon
JSON.mapping(
id: Int32,
sku: String,
price: Int32,
price: Float32,
flop_reason: String,
type: String,
size: BadDragon::Size,
......
require "../../connection/redis"
require "../../bad_dragon/entities/toy"
require "json"
module Repository
# The WatchNotifications repository manages the toy notifications for a
# watcher.
#
# Redis considerations
# --------------------
#
# - Redis key prefix: `ddw:wn:`
# - A redis key for the Telegram chat IDs `3038326` would be e.g. `ddw:wn:3038326`
# - The value is a JSON array containing the toy IDs
class WatchNotifications
KEY_BASE = "ddw:wn"
private macro redis_key(key)
"#{KEY_BASE}:#{{{key}}}"
end
def self.add(user_id : Int64, toy : BadDragon::Entities::Toy)
Application.logger.info "adding toy #{toy.id} (#{toy.sku}/#{toy.size}) for #{user_id} to the notified list"
key = redis_key(user_id)
loop do
Application.logger.debug "[redis] WATCH #{key.inspect}"
redis.watch(key) # optimistic locking
notified_toys = notifications(user_id)
notified_toys << toy.id
notified_toys.uniq!
Application.logger.debug "[redis] SET #{key.inspect} #{notified_toys.to_json.inspect}"
response = redis.multi(&.set(key, notified_toys.to_json))
break unless response.empty?
end
end
def self.notifications(user_id) : Array(Int32)
Application.logger.debug "[redis] GET #{redis_key(user_id).inspect}"
JSON.parse(
redis.get(redis_key(user_id)) || "[]"
).as_a.map(&.as_i)
end
def self.watching?(user_id : Int64, toy)
notifications(user_id).includes?(toy.id)
end
private def self.redis
Connection::Redis.connection
end
end
end
......@@ -13,7 +13,7 @@ module Repository
# - Redis key prefix: `ddw:wt:`
# - A redis key for the toy `rex` would be e.g. `ddw:wt:rex`
# - The keys of that hash are the toy sizes, e.g. `extralarge`
# - The value is a JSON array containing the Telegram user IDs
# - The value is a JSON array containing the Telegram chat IDs
#
# This way, we can simply iterate over all toys, and get the watchers for a
# given toy/size combination.
......@@ -24,7 +24,7 @@ module Repository
"#{KEY_BASE}:#{{{key}}}"
end
def self.add(user_id : Int32, toy_sku, toy_size : BadDragon::Size)
def self.add(user_id : Int64, toy_sku, toy_size : BadDragon::Size)
Application.logger.info "adding watch #{toy_sku}/#{toy_size} for #{user_id}"
key = redis_key(toy_sku)
loop do
......@@ -63,7 +63,23 @@ module Repository
JSON.parse(
redis.hget(redis_key(toy_sku), toy_size) || "[]"
).as_a.map(&.as_i)
).as_a.map(&.as_i64)
end
def self.watches : Hash(String, Array(BadDragon::Size))
Application.logger.debug "[redis] KEYS #{KEY_BASE}:*"
toys = redis.keys("#{KEY_BASE}:*").map(&.to_s)
returned_watches = {} of String => Array(BadDragon::Size)
toys.each do |redis_key|
toy_sku = redis_key.sub(/^#{KEY_BASE}:/, "")
Application.logger.debug "[redis] HKEYS #{redis_key}"
returned_watches[toy_sku] = ((redis.hkeys(redis_key).map(&.to_s) || [] of String).map { |x| BadDragon::Size.parse(x) })
end
returned_watches
end
private def self.redis
......
require "./cli_commands/notify"
require "./cli_commands/start_bot"
require "./cli_commands/help"
......
require "./base"
require "../notify"
require "../../errors"
module UseCase
module CliCommands
class Notify < CliCommands::Base
def self.command_name
"notify"
end
def self.description
"Fetches toys and notifies the watchers"
end
def call(_argv)
check_sanity!
UseCase::Notify.call
end
private def check_sanity!
unless ENV.has_key?("TELEGRAM_API_TOKEN")
raise Errors::ConfigurationError.new("TELEGRAM_API_TOKEN is not set")
end
end
end
end
end
require "./base"
require "../application"
require "../bad_dragon/api/client"
require "../bot"
require "../repository/watch_notifications"
require "../repository/watch_targets"
module UseCase
class Notify < Base
def call
watched_toys = fetch_watches
toys = fetch_toys
notify(toys, watched_toys)
end
private def fetch_toys
Application.logger.info "[notify] fetching all toys"
client = BadDragon::API::Client.new
client.inventory_toys
end
private def fetch_watches
Application.logger.info "[notify] fetching watches"
Repository::WatchTargets.watches
end
private def notify(toys, watched_toys)
filtered_toys = toys.select do |toy|
watched_toys.has_key?(toy.sku) &&
watched_toys[toy.sku].includes?(toy.size)
end
filtered_toys.each do |toy|
watchers = Repository::WatchTargets.watchers(toy.sku, toy.size)
watchers.each do |watcher|
send_notification(watcher, toy)
end
end
end
private def send_notification(watcher, toy)
return if Repository::WatchNotifications.watching?(watcher, toy)
Application.logger.info "[notify] notifying #{watcher} for #{toy.sku}/#{toy.size}"
bot = ::Bot.new
bot.send_message(
chat_id: watcher,
text: "*browses the bad dragon clearance page and notices a #{toy.sku} in #{toy.size}* OwO what's this??\n\nget it while it's hot: https://bad-dragon.com/shop/clearance?sizes[]=#{toy.size.to_s.downcase}&skus[][]=#{toy.sku}"
)
unless toy.images.empty?
toy.images.each do |image|
bot.send_photo(
chat_id: watcher,
photo: image.full_filename
)
end
end
Repository::WatchNotifications.add(watcher, toy)
end
end
end
require "./slash_commands/start"
require "./slash_commands/help"
require "./slash_commands/add_watch"
require "./slash_commands/remove_watch"
......@@ -27,7 +27,7 @@ module UseCase
sku = params.shift
size = BadDragon::Size.parse(params.shift)
Repository::WatchTargets.add(msg.from.not_nil!.id, sku, size)
Repository::WatchTargets.add(msg.chat.id, sku, size)
bot.reply msg, "I will notify you if there is a #{sku} in #{size} in the inventory."
end
......
require "telegram_bot"
require "./base"
require "../../application"
require "../../bad_dragon/size"
require "../../repository/watch_targets"
module UseCase
module SlashCommands
class RemoveWatch < SlashCommands::Base
def self.command_name
"removeWatch"
end
def self.help_arguments
"SKU SIZE"
end
def self.help_text
"Already found something? Use this command to remove a toy from your watchlist"
end
def call(bot, msg, params)
return log_warning if msg.from.nil?
return bot.reply(msg, usage) unless params.size == 2
sku = params.shift
size = BadDragon::Size.parse(params.shift)
Repository::WatchTargets.remove(msg.chat.id, sku, size)
bot.reply msg, "I will no longer notify you if there is a #{sku} in #{size} in the inventory."
end
private def log_warning
Application.logger.warn "got a removeWatch request using a message without a user -- ignoring"
end
private def usage
usage = <<-EOF
Usage: /removewatch SKU SIZE
Parameters:
SKU - the SKU of the toy
SIZE - the size of the toy
Allowed values for SIZE:
EOF
usage += "\n"
usage += BadDragon::Size.values.map(&.to_s).join(", ")
end
end
end
end