144 lines
4.1 KiB
Ruby
144 lines
4.1 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
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' => ['不。', '绝对不行。']
|
||
}.freeze
|
||
|
||
RATE_LIMIT = {
|
||
window: 60, # seconds
|
||
limit: 60 # requests per IP per window
|
||
}.freeze
|
||
|
||
$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 StandardError
|
||
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}/).first, 'en'].each do |lang|
|
||
return lang if 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?
|
||
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
|
||
|
||
status response[:status]
|
||
content_type :json
|
||
return { error: response[:message] }.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
|