no-as-a-service/app.rb
2025-05-13 07:58:31 +02:00

139 lines
3.8 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.",
"Im 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