2025-05-24 20:27:14 +02:00
|
|
|
|
# frozen_string_literal: true
|
2025-05-13 07:58:31 +02:00
|
|
|
|
|
|
|
|
|
|
require 'sinatra'
|
|
|
|
|
|
require 'json'
|
|
|
|
|
|
require 'rack/accept'
|
|
|
|
|
|
require 'time'
|
|
|
|
|
|
|
|
|
|
|
|
use Rack::Accept
|
|
|
|
|
|
|
|
|
|
|
|
NO_RESPONSES = {
|
2025-05-24 20:27:14 +02:00
|
|
|
|
'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.'
|
2025-05-13 07:58:31 +02:00
|
|
|
|
],
|
2025-05-24 20:27:14 +02:00
|
|
|
|
'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' => ['不。', '绝对不行。']
|
|
|
|
|
|
}.freeze
|
2025-05-13 07:58:31 +02:00
|
|
|
|
|
|
|
|
|
|
RATE_LIMIT = {
|
|
|
|
|
|
window: 60, # seconds
|
|
|
|
|
|
limit: 60 # requests per IP per window
|
2025-05-24 20:27:14 +02:00
|
|
|
|
}.freeze
|
2025-05-13 07:58:31 +02:00
|
|
|
|
|
|
|
|
|
|
$requests = {}
|
|
|
|
|
|
|
|
|
|
|
|
helpers do
|
|
|
|
|
|
def client_ip
|
|
|
|
|
|
request.env['HTTP_X_FORWARDED_FOR'] || request.ip
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def log_to_journald(message)
|
2025-05-24 20:27:14 +02:00
|
|
|
|
IO.popen(['/usr/bin/systemd-cat', '--identifier=noaas'], 'w') { |io| io.puts message }
|
|
|
|
|
|
rescue StandardError
|
2025-05-13 07:58:31 +02:00
|
|
|
|
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
|
2025-05-24 20:27:14 +02:00
|
|
|
|
[params['lang'], request.env['HTTP_ACCEPT_LANGUAGE'].to_s.scan(/[a-z]{2}/).first, 'en'].each do |lang|
|
|
|
|
|
|
return lang if NO_RESPONSES.key?(lang)
|
|
|
|
|
|
end || 'en'
|
2025-05-13 07:58:31 +02:00
|
|
|
|
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?
|
2025-05-24 20:27:14 +02:00
|
|
|
|
response = [{ status: 402, message: "I'm not paid enough for this." },
|
|
|
|
|
|
{ status: 418, message: "I'm just a teapot." },
|
|
|
|
|
|
{ status: 429, message: 'Not so fast!' }].sample
|
2025-05-13 20:00:18 +02:00
|
|
|
|
|
|
|
|
|
|
status response[:status]
|
2025-05-13 07:58:31 +02:00
|
|
|
|
content_type :json
|
2025-05-13 20:00:18 +02:00
|
|
|
|
return { error: response[:message] }.to_json
|
2025-05-13 07:58:31 +02:00
|
|
|
|
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
|