139 lines
3.8 KiB
Ruby
139 lines
3.8 KiB
Ruby
|
||
require 'sinatra'
|
||
require 'json'
|
||
require 'rack/accept'
|
||
require 'time'
|
||
|
||
use Rack::Accept
|
||
|
||
NO_RESPONSES = {
|
||
"en" => [
|
||
"No.", "Nope.", "Nah.", "Nuh-uh.", "Negative.", "Nay.",
|
||
"Hell no.", "Not in a million years.", "Get lost.",
|
||
"Not happening, buddy.", "Why don't you try asking someone else?",
|
||
"Are you kidding me?", "Absolutely not, go away.",
|
||
"I’m terribly sorry, but no.", "I regret to inform you that I cannot comply.",
|
||
"Unfortunately, that will not be possible at this time.",
|
||
"I must respectfully decline your request.", "I wish I could, but sadly I cannot.",
|
||
"It would be my pleasure to help, but I must regretfully decline.",
|
||
"With the greatest respect, I must say no.",
|
||
"I apologize, but I must kindly refuse your request."
|
||
],
|
||
"de" => ["Nein.", "Keineswegs."],
|
||
"fr" => ["Non.", "Jamais."],
|
||
"ja" => ["いいえ", "ダメです。"],
|
||
"ru" => ["Нет", "Никак нет"],
|
||
"pt" => ["Não.", "De jeito nenhum."],
|
||
"es" => ["No.", "Para nada."],
|
||
"pl" => ["Nie.", "W żadnym wypadku."],
|
||
"fi" => ["Ei.", "Ei todellakaan."],
|
||
"tr" => ["Hayır.", "Bu değil."],
|
||
"he" => ["לא"],
|
||
"hi" => ["नहीं", "बिलकुल नहीं"],
|
||
"no" => ["Nei.", "Absolutt ikke.", "Ikke tale om."],
|
||
"sv" => ["Nej.", "Inte alls."],
|
||
"da" => ["Nej.", "Ikke en chance."],
|
||
"it" => ["No.", "Assolutamente no."],
|
||
"nl" => ["Nee.", "Absoluut niet."],
|
||
"cs" => ["Ne.", "Rozhodně ne."],
|
||
"ko" => ["아니요.", "절대 안 돼요."],
|
||
"zh" => ["不。", "绝对不行。"]
|
||
}
|
||
|
||
RATE_LIMIT = {
|
||
window: 60, # seconds
|
||
limit: 60 # requests per IP per window
|
||
}
|
||
|
||
$requests = {}
|
||
|
||
helpers do
|
||
def client_ip
|
||
request.env['HTTP_X_FORWARDED_FOR'] || request.ip
|
||
end
|
||
|
||
def log_to_journald(message)
|
||
IO.popen(["/usr/bin/systemd-cat", "--identifier=noaas"], "w") { |io| io.puts message }
|
||
rescue
|
||
puts message # fallback to stdout
|
||
end
|
||
|
||
def log_structured(lang:, format:, status:, duration_ms:)
|
||
log_entry = {
|
||
timestamp: Time.now.utc.iso8601,
|
||
ip: client_ip,
|
||
method: request.request_method,
|
||
path: request.path,
|
||
user_agent: request.user_agent,
|
||
language: lang,
|
||
format: format,
|
||
status: status,
|
||
duration_ms: duration_ms
|
||
}
|
||
log_to_journald(JSON.generate(log_entry))
|
||
end
|
||
|
||
def rate_limited?
|
||
ip = client_ip
|
||
now = Time.now.to_i
|
||
$requests[ip] ||= []
|
||
$requests[ip].reject! { |timestamp| timestamp < now - RATE_LIMIT[:window] }
|
||
if $requests[ip].size >= RATE_LIMIT[:limit]
|
||
true
|
||
else
|
||
$requests[ip] << now
|
||
false
|
||
end
|
||
end
|
||
|
||
def preferred_language
|
||
params["lang"] || request.env["HTTP_ACCEPT_LANGUAGE"].to_s.scan(/[a-z]{2}/).find do |lang|
|
||
NO_RESPONSES.key?(lang)
|
||
end || "en"
|
||
end
|
||
|
||
def no_message(lang)
|
||
NO_RESPONSES[lang].sample
|
||
end
|
||
end
|
||
|
||
before do
|
||
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||
end
|
||
|
||
after do
|
||
duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time) * 1000).round(2)
|
||
lang = preferred_language
|
||
format = request.preferred_type&.to_s || request.accept.first.to_s
|
||
log_structured(lang: lang, format: format, status: response.status, duration_ms: duration)
|
||
end
|
||
|
||
get '/' do
|
||
if rate_limited?
|
||
status 429
|
||
content_type :json
|
||
return { error: "Too Many Requests" }.to_json
|
||
end
|
||
|
||
lang = preferred_language
|
||
message = no_message(lang)
|
||
|
||
if request.preferred_type.to_s == 'application/json' || request.accept.include?('application/json')
|
||
content_type :json
|
||
status 200
|
||
{ message: message, language: lang }.to_json
|
||
else
|
||
content_type :html
|
||
status 200
|
||
"<!DOCTYPE html>
|
||
<html lang='#{lang}'>
|
||
<head>
|
||
<meta charset='UTF-8'>
|
||
<title>No as a Service</title>
|
||
</head>
|
||
<body style='font-family: sans-serif; text-align: center; margin-top: 20%; font-size: 2em;'>
|
||
<p>#{message}</p>
|
||
</body>
|
||
</html>"
|
||
end
|
||
end
|