Files

290 lines
16 KiB
Plaintext
Raw Permalink Normal View History

# ╔══════════════════════════════════════════════════════════════════╗
# ║ KISALT — KabaLang ile bağlantı kısaltıcı ║
# ║ ║
# ║ · Sayfalar sunucuda KabaLang ile üretilir (şablon motoru yok) ║
# ║ · /k/{kod} → gerçek HTTP 302 yönlendirmesi + tıklama kaydı ║
# ║ · İstatistik sayfasındaki grafik: sunucuda üretilen SVG ║
# ╚══════════════════════════════════════════════════════════════════╝
#
# Çalıştır: kabalang calistir web/kisalt/kisalt.kbl
# Aç: http://127.0.0.1:8472
kullan kaba.web
kullan kaba.json
kullan kaba.veritabani
kullan kaba.sistem
# Karıştırılabilecek harfler (0/o, 1/l/i) bilerek yok.
ALFABE = "abcdefghjkmnpqrstuvwxyz23456789"
KOD_UZUNLUGU = 6
PORT = 8472
# Barındıran ortam (ör. Online IDE) portu PORT ortam değişkeniyle verir.
port_ortam = cevre.al("PORT")
eger port_ortam != "":
PORT = sayi(port_ortam)
db = cevre.al("KISALT_DB")
eger db == "":
db = "kisalt.db"
# ── Stil: tüm sayfalar aynı görünümü paylaşır ───────────────────────
STIL = """
:root { --zemin:#0a0d15; --yuzey:#121826; --yuzey2:#182033; --cizgi:#232c42;
--metin:#e6ebf5; --soluk:#8a94ab; --mavi:#377ef4; --cam:#22d3ee; --yesil:#34d399; }
* { box-sizing: border-box; }
body { margin:0; min-height:100vh; color:var(--metin);
font:15px/1.55 Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
background: radial-gradient(900px 480px at 50% -12%, #377ef433, transparent 65%),
radial-gradient(600px 360px at 95% 10%, #22d3ee18, transparent 60%), var(--zemin); }
a { color: var(--cam); text-decoration: none; }
a:hover { text-decoration: underline; }
.kap { max-width: 980px; margin: 0 auto; padding: 0 24px 56px; }
nav { display:flex; align-items:center; gap:12px; padding: 22px 0; }
.logo { width:36px; height:36px; border-radius:11px; display:grid; place-items:center; font-weight:800;
background:linear-gradient(135deg,var(--mavi),var(--cam)); box-shadow:0 6px 22px #377ef455; }
nav b { font-size: 18px; letter-spacing: -.01em; }
nav span { margin-left:auto; color:var(--soluk); font-size:13px; }
.kahraman { text-align:center; padding: 46px 0 30px; }
.kahraman h1 { font-size: clamp(30px, 5vw, 48px); line-height:1.1; margin:0 0 12px; letter-spacing:-.03em; }
.kahraman h1 em { font-style: normal; background: linear-gradient(90deg, var(--mavi), var(--cam));
-webkit-background-clip: text; background-clip: text; color: transparent; }
.kahraman p { color: var(--soluk); margin: 0 0 28px; }
form { display:flex; gap:10px; max-width: 720px; margin: 0 auto; background: var(--yuzey);
border:1px solid var(--cizgi); border-radius: 16px; padding: 8px; box-shadow: 0 20px 60px #0007; }
input { flex:1; min-width:0; font:inherit; color:var(--metin); background:transparent; border:0; padding: 12px 14px; outline:none; }
button { font:inherit; font-weight:600; color:#fff; border:0; border-radius: 11px; padding: 12px 22px; cursor:pointer;
background: linear-gradient(135deg, var(--mavi), #2563eb); box-shadow: 0 6px 18px #377ef450; }
#sonuc { max-width:720px; margin: 14px auto 0; min-height: 24px; font-size: 14px; color: var(--soluk); }
#sonuc .kisa { color: var(--yesil); font-weight: 600; font-family: ui-monospace, monospace; font-size: 15px; }
.kutular { display:grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin: 34px 0 18px; }
.kutu { background: var(--yuzey); border:1px solid var(--cizgi); border-radius: 16px; padding: 16px 18px; }
.kutu small { color: var(--soluk); font-size: 12px; text-transform: uppercase; letter-spacing: .07em; }
.kutu div { font-size: 26px; font-weight: 700; letter-spacing: -.02em; }
table { width:100%; border-collapse: collapse; background: var(--yuzey); border:1px solid var(--cizgi);
border-radius: 16px; overflow:hidden; }
th, td { text-align:left; padding: 12px 16px; border-bottom: 1px solid var(--cizgi); font-size: 14px; }
th { color: var(--soluk); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .07em; background: var(--yuzey2); }
tr:last-child td { border-bottom: 0; }
td.hedef { color: var(--soluk); max-width: 360px; overflow:hidden; text-overflow: ellipsis; white-space: nowrap; }
td.kod { font-family: ui-monospace, monospace; }
.sayac { font-weight: 700; color: var(--metin); }
.grafik { background: var(--yuzey); border:1px solid var(--cizgi); border-radius: 16px; padding: 18px; margin-top: 14px; }
.grafik svg { width: 100%; height: auto; display:block; }
.adres { word-break: break-all; color: var(--soluk); }
footer { text-align:center; color: var(--soluk); font-size: 12px; margin-top: 30px; }
@media (max-width: 680px) {
.kap { padding: 0 16px 40px; }
form { flex-direction: column; }
.kutular { grid-template-columns: 1fr 1fr; }
.kutular .kutu:last-child { grid-column: 1 / -1; }
td.hedef, th.hedef { display: none; }
}
"""
fonksiyon sayfa(baslik, govde):
don "<!doctype html><html lang='tr'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'><title>" + web.kacir(baslik) + "</title><style>" + STIL + "</style></head><body><div class='kap'><nav><div class='logo'>K</div><b>Kısalt</b><span>KabaLang · SQLite · 302</span></nav>" + govde + "<footer>Bu sayfa sunucuda KabaLang ile üretildi.</footer></div></body></html>"
# ── Veritabanı ──────────────────────────────────────────────────────
fonksiyon hazirla(db):
veritabani.sqlite(db, "CREATE TABLE IF NOT EXISTS baglanti(kod TEXT PRIMARY KEY, adres TEXT NOT NULL, olusturma TEXT, tiklama INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS tiklama(kod TEXT, zaman TEXT);")
# Satırları `|` ile ayrılmış sütunlara böler. Adres son sütundur ve
# kendi içinde `|` taşıyabilir → kalan parçalar geri birleştirilir.
fonksiyon satirlar(ham):
sonuc = []
eger ham == "":
don sonuc
dongu s icinde ham.bol("\n"):
sonuc.ekle(s)
don sonuc
fonksiyon son_sutun(parcalar, bas):
don parcalar.dilim(bas, parcalar.uzunluk() - bas).birlestir("|")
# ── Kısa kod ────────────────────────────────────────────────────────
fonksiyon yeni_kod():
kod = ""
dongu _ icinde 1..KOD_UZUNLUGU:
kod = kod + ALFABE.dilim(rastgele.sayi(0, ALFABE.uzunluk() - 1), 1)
don kod
fonksiyon kod_gecerli(k):
eger k.uzunluk() != KOD_UZUNLUGU:
don yanlis
dongu i icinde 0..k.uzunluk() - 1:
eger degil ALFABE.icerir(k.dilim(i, 1)):
don yanlis
don dogru
# Hedef adres yalnız http(s) olabilir: `javascript:` ya da `file:` ile
# başlayan bir "bağlantı" kısaltıcıyı saldırı aracına çevirirdi.
fonksiyon adres_dogrula(a):
eger a.uzunluk() > 2000:
don "adres çok uzun"
eger degil (a.ile_baslar("https://") veya a.ile_baslar("http://")):
don "adres http:// ya da https:// ile başlamalı"
eger a.icerir(" ") veya a.icerir("\n") veya a.icerir("\r") veya a.icerir("\t"):
don "adres boşluk içeremez"
eger a.uzunluk() < 12:
don "adres eksik görünüyor"
don ""
fonksiyon kaydet(db, adres):
kod = yeni_kod()
# Çakışma olasılığı çok düşük ama sıfır değil: boş kod bulana dek dene.
iken veritabani.sqlite(db, "SELECT count(*) FROM baglanti WHERE kod = ?", [kod]) != "0":
kod = yeni_kod()
veritabani.sqlite(db, "INSERT INTO baglanti(kod, adres, olusturma) VALUES (?, ?, ?)", [kod, adres, zaman.simdi()])
don kod
# Tanıtım verisi: ilk açılışta birkaç bağlantı ve geçmiş tıklamalar.
fonksiyon ornek_veri(db):
eger veritabani.sqlite(db, "SELECT count(*) FROM baglanti") != "0":
don 0
adresler = ["https://kabaforce.com", "https://dev.kabaforce.com/kabalang/", "https://forge.kabaforce.com/KabaLang/ornekler", "https://maps.kabaforce.com/?q=Galata+Kulesi", "https://dev.kabaforce.com/kabalang/kilavuz/12-web-uygulamasi"]
agirlik = [7, 11, 5, 3, 2]
dongu i icinde 0..adresler.uzunluk() - 1:
kod = kaydet(db, adresler[i])
veritabani.sqlite(db, "UPDATE baglanti SET olusturma = datetime('now', 'localtime', ?) WHERE kod = ?", ["-" + metin(7 + i) + " days", kod])
# Son yedi güne yayılmış tıklamalar.
dongu gun icinde 0..6:
adet = rastgele.sayi(0, agirlik[i])
dongu _ icinde 1..adet:
veritabani.sqlite(db, "INSERT INTO tiklama(kod, zaman) VALUES (?, datetime('now', 'localtime', ?))", [kod, "-" + metin(gun) + " days"])
veritabani.sqlite(db, "UPDATE baglanti SET tiklama = (SELECT count(*) FROM tiklama WHERE tiklama.kod = baglanti.kod) WHERE kod = ?", [kod])
don 1
hazirla(db)
ornek_veri(db)
# ── Sayfalar ────────────────────────────────────────────────────────
fonksiyon ana_sayfa(_istek):
toplam = veritabani.sqlite(db, "SELECT count(*) FROM baglanti")
tik = veritabani.sqlite(db, "SELECT coalesce(sum(tiklama), 0) FROM baglanti")
bugun = veritabani.sqlite(db, "SELECT count(*) FROM tiklama WHERE date(zaman) = date('now', 'localtime')")
g = "<section class='kahraman'><h1>Uzun bağlantılar,<br><em>kısa ve ölçülebilir.</em></h1>"
g = g + "<p>Bir adres yapıştırın; KabaLang ona kısa bir kod versin ve her tıklamayı saysın.</p>"
g = g + "<form id='f'><input id='adres' placeholder='https://…' autocomplete='off' required><button>Kısalt</button></form>"
g = g + "<div id='sonuc'></div></section>"
g = g + "<div class='kutular'>"
g = g + "<div class='kutu'><small>Bağlantı</small><div>" + toplam + "</div></div>"
g = g + "<div class='kutu'><small>Toplam tıklama</small><div>" + tik + "</div></div>"
g = g + "<div class='kutu'><small>Bugün</small><div>" + bugun + "</div></div>"
g = g + "</div>"
g = g + "<table><tr><th>Kısa kod</th><th class='hedef'>Hedef</th><th>Tıklama</th><th></th></tr>"
dongu s icinde satirlar(veritabani.sqlite(db, "SELECT kod, tiklama, adres FROM baglanti ORDER BY olusturma DESC LIMIT 12")):
p = s.bol("|")
kod = p[0]
# Kod yalnız ALFABE harflerinden oluşur → href içinde güvenli.
# Hedef adres kullanıcıdan geldi → yalnız METİN olarak, kaçırılarak basılır.
g = g + "<tr><td class='kod'><a href='/k/" + kod + "'>/k/" + kod + "</a></td>"
g = g + "<td class='hedef'>" + web.kacir(son_sutun(p, 2)) + "</td>"
g = g + "<td class='sayac'>" + p[1] + "</td>"
g = g + "<td><a href='/istatistik/" + kod + "'>istatistik →</a></td></tr>"
g = g + "</table>"
# Küçük bir istemci betiği: formu JSON API'ye gönderir.
g = g + "<script>document.getElementById('f').onsubmit=async e=>{e.preventDefault();const s=document.getElementById('sonuc');"
g = g + "const y=await fetch('/api/kisalt',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({adres:document.getElementById('adres').value})});"
g = g + "const d=await y.json();s.replaceChildren();if(!y.ok){s.textContent='⚠ '+d.hata;return;}"
g = g + "const a=document.createElement('a');a.className='kisa';a.href=d.kisa;a.textContent=location.host+d.kisa;s.append('✓ Hazır: ',a);setTimeout(()=>location.reload(),1600);};</script>"
don web.html(sayfa("Kısalt · KabaLang", g))
fonksiyon kisalt_api(istek):
govde = istek.govde()
eger degil json.gecerli_mi(govde):
don web.json(json.uret({"hata": "gövde geçerli JSON değil"}), 400)
adres = json.al(govde, "adres").kirp()
sorun = adres_dogrula(adres)
eger sorun != "":
don web.json(json.uret({"hata": sorun}), 422)
kod = kaydet(db, adres)
don web.json(json.uret({"kod": kod, "kisa": "/k/" + kod, "adres": adres}), 201)
fonksiyon bulunamadi_sayfasi():
g = "<section class='kahraman'><h1>Bu kod <em>bulunamadı</em></h1><p>Bağlantı silinmiş ya da hiç var olmamış olabilir.</p><p><a href='/'>← ana sayfa</a></p></section>"
don web.html(sayfa("Bulunamadı · Kısalt", g), 404)
fonksiyon yonlendir(istek):
kod = istek.parametre("kod")
eger degil kod_gecerli(kod):
don bulunamadi_sayfasi()
adres = veritabani.sqlite(db, "SELECT adres FROM baglanti WHERE kod = ?", [kod])
eger adres == "":
don bulunamadi_sayfasi()
veritabani.sqlite(db, "INSERT INTO tiklama(kod, zaman) VALUES (?, datetime('now', 'localtime'))", [kod])
veritabani.sqlite(db, "UPDATE baglanti SET tiklama = tiklama + 1 WHERE kod = ?", [kod])
# Gerçek HTTP yönlendirmesi: 302 + Location başlığı.
don web.yanit("", 302).baslik("Location", adres)
# Son 7 günün tıklamalarını SVG çubuk grafiğe çevirir — tamamen sunucuda.
fonksiyon yedi_gun_svg(db, kod):
gunler = []
adetler = []
en_cok = 1
dongu i icinde 0..6:
geri = 6 - i
gun = veritabani.sqlite(db, "SELECT strftime('%d.%m', 'now', 'localtime', ?)", ["-" + metin(geri) + " days"])
adet = sayi(veritabani.sqlite(db, "SELECT count(*) FROM tiklama WHERE kod = ? AND date(zaman) = date('now', 'localtime', ?)", [kod, "-" + metin(geri) + " days"]))
gunler.ekle(gun)
adetler.ekle(adet)
eger adet > en_cok:
en_cok = adet
gen = 700
yuk = 220
cubuk = 64
aralik = 36
svg = "<svg viewBox='0 0 " + metin(gen) + " " + metin(yuk + 40) + "' xmlns='http://www.w3.org/2000/svg'>"
svg = svg + "<defs><linearGradient id='g' x1='0' y1='1' x2='0' y2='0'><stop offset='0' stop-color='#377ef4'/><stop offset='1' stop-color='#22d3ee'/></linearGradient></defs>"
dongu i icinde 0..6:
h = adetler[i] * (yuk - 30) // en_cok
eger h < 3:
h = 3
x = 30 + i * (cubuk + aralik)
y = yuk - h
svg = svg + "<rect x='" + metin(x) + "' y='" + metin(y) + "' width='" + metin(cubuk) + "' height='" + metin(h) + "' rx='10' fill='url(#g)'/>"
svg = svg + "<text x='" + metin(x + cubuk // 2) + "' y='" + metin(y - 8) + "' fill='#e6ebf5' font-size='15' font-weight='700' text-anchor='middle' font-family='system-ui'>" + metin(adetler[i]) + "</text>"
svg = svg + "<text x='" + metin(x + cubuk // 2) + "' y='" + metin(yuk + 26) + "' fill='#8a94ab' font-size='13' text-anchor='middle' font-family='system-ui'>" + gunler[i] + "</text>"
don svg + "</svg>"
fonksiyon istatistik(istek):
kod = istek.parametre("kod")
eger degil kod_gecerli(kod):
don bulunamadi_sayfasi()
satir = veritabani.sqlite(db, "SELECT tiklama, olusturma, adres FROM baglanti WHERE kod = ?", [kod])
eger satir == "":
don bulunamadi_sayfasi()
p = satir.bol("|")
son = veritabani.sqlite(db, "SELECT coalesce(max(zaman), '—') FROM tiklama WHERE kod = ?", [kod])
g = "<section class='kahraman' style='padding-bottom:10px'><h1><em>/k/" + kod + "</em></h1>"
g = g + "<p class='adres'>→ " + web.kacir(son_sutun(p, 2)) + "</p></section>"
g = g + "<div class='kutular'>"
g = g + "<div class='kutu'><small>Toplam tıklama</small><div>" + p[0] + "</div></div>"
g = g + "<div class='kutu'><small>Oluşturuldu</small><div style='font-size:17px;padding-top:7px'>" + p[1] + "</div></div>"
g = g + "<div class='kutu'><small>Son tıklama</small><div style='font-size:17px;padding-top:7px'>" + son + "</div></div>"
g = g + "</div>"
g = g + "<div class='grafik'><small style='color:#8a94ab;text-transform:uppercase;letter-spacing:.07em;font-size:12px'>Son 7 gün</small>" + yedi_gun_svg(db, kod) + "</div>"
g = g + "<p style='text-align:center;margin-top:22px'><a href='/'>← tüm bağlantılar</a></p>"
don web.html(sayfa("/k/" + kod + " · istatistik", g))
# Tek iş parçacığı: tıklama sayacı yarış koşuluna girmesin.
sunucu = web.sunucu(PORT, "127.0.0.1", 1)
sunucu.govde_siniri(8192)
sunucu.al("/", ana_sayfa)
sunucu.gonder("/api/kisalt", kisalt_api)
sunucu.al("/k/{kod}", yonlendir)
sunucu.al("/istatistik/{kod}", istatistik)
sunucu.bulunamadi((_istek) => bulunamadi_sayfasi())
yazdir("🔗 Kısalt hazır — http://127.0.0.1:" + metin(PORT))
sunucu.basla()