Created
March 14, 2017 20:05
-
-
Save aranajhonny/7df82f568cb554b2942ecd82d17a31df to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; News. 2 Sep 06. | |
; to run news: | |
; arc> (load "news.arc") | |
; arc> (nsv) | |
; put usernames of admins, separated by whitespace, in arc/admins | |
(= this-site* "My Forum" | |
site-url* "http://news.yourdomain.com/" | |
parent-url* "http://www.yourdomain.com" | |
favicon-url* "" | |
site-desc* "What this site is about." ; for rss feed | |
site-color* orange | |
prefer-url* t) | |
(= rootdir* "arc/news_public_html/") | |
; Structures | |
; Could add (html) types like choice, yesno to profile fields. But not | |
; as part of deftem, which is defstruct. Need another mac on top of | |
; deftem. Should not need the type specs in user-fields. | |
(deftem profile | |
id nil | |
name nil | |
created (seconds) | |
auth 0 | |
member nil | |
submitted nil | |
karma 1 | |
weight .5 | |
ignore nil | |
nodowns nil | |
email nil | |
about nil | |
showdead nil | |
noprocrast nil | |
firstview nil | |
lastview nil | |
maxvisit 20 | |
minaway 180 | |
topcolor nil | |
keys nil) | |
(deftem item | |
id nil | |
type nil | |
by nil | |
ip nil | |
time (seconds) | |
url nil | |
title nil | |
text nil | |
votes nil ; elts each (time ip user type score) | |
score 0 | |
sockvotes 0 | |
dead nil | |
deleted nil | |
parent nil | |
kids nil) | |
; Load and Save | |
(= newsdir* "arc/news/" | |
storydir* "arc/news/story/" | |
profdir* "arc/news/profile/" | |
votedir* "arc/news/vote/") | |
(= votes* (table) profs* (table)) | |
(def nsv ((o port 8080)) | |
(map ensure-dir (list arcdir* newsdir* storydir* votedir* profdir*)) | |
(unless stories* (load-items)) | |
(if (empty profs*) (load-users)) | |
(asv port)) | |
(def load-users () | |
(pr "load users: ") | |
(noisy-each 100 id (dir profdir*) | |
(load-user id))) | |
(def load-user (u) | |
(= (votes* u) (load-table (+ votedir* u)) | |
(profs* u) (temload 'profile (+ profdir* u))) | |
u) | |
(def init-user (u) | |
(= (votes* u) (table) (profs* u) (inst 'profile 'id u)) | |
(save-votes u) | |
(save-prof u) | |
u) | |
; Need this because can create users on the server (for other apps) | |
; without setting up places to store their state as news users. | |
; See the admin op in app.arc. So all calls to login-page from the | |
; news app need to call this in the after-login fn. | |
(def ensure-news-user (u) | |
(if (profs* u) u (init-user u))) | |
(def save-votes (u) (save-table (votes* u) (+ votedir* u))) | |
(def save-prof (u) (save-table (profs* u) (+ profdir* u))) | |
(mac uvar (u k) `((profs* ,u) ',k)) | |
(mac karma (u) `(uvar ,u karma)) | |
(def users (f) (keep f (keys profs*))) | |
(= stories* nil comments* nil items* (table) url->story* (table) | |
maxid* 0 initload* 15000) | |
; The dir expression yields stories in order of file creation time | |
; (because arc infile truncates), so could just rev the list instead of | |
; sorting, but sort anyway. | |
; Note that stories* etc only include the initloaded (i.e. recent) | |
; ones, plus those created since this server process started. | |
; Could be smarter about preloading by keeping track of most popular pages. | |
(def load-items () | |
(pr "load items: ") | |
(with (items (table) | |
ids (sort > (map [coerce _ 'int] (dir storydir*)))) | |
(if ids (= maxid* (car ids))) | |
(noisy-each 100 id (firstn initload* ids) | |
(let i (load-item id) | |
(push i (items i!type)))) | |
(= stories* (rev items!story) comments* (rev items!comment)) | |
(hook 'initload items)) | |
(ensure-topstories)) | |
(def ensure-topstories () | |
(aif (errsafe (readfile1 (+ newsdir* "topstories"))) | |
(= ranked-stories* (map item it)) | |
(do (prn "ranking stories.") | |
(gen-topstories)))) | |
(def astory (i) (is i!type 'story)) | |
(def acomment (i) (is i!type 'comment)) | |
(def load-item (id) | |
(let i (temload 'item (string storydir* id)) | |
(= (items* id) i (i 'id) id) | |
(awhen (and (astory i) (live i) i!url) | |
(= (url->story* it) i)) | |
i)) | |
(def new-item-id () | |
(let id (++ maxid*) | |
(if (file-exists (string storydir* id)) (new-item-id) id))) | |
(def item (id) | |
(or (items* id) (errsafe (load-item id)))) | |
(def kids (x) (map item x!kids)) | |
; For use on external item references (from urls). | |
; Checks id is int because people try e.g. item?id=363/blank.php | |
(def safe-item (id) | |
(let id (if (isa id 'string) (saferead id) id) | |
(and (ok-id id) (item id)))) | |
(def ok-id (id) | |
(and (exact id) (<= 1 id maxid*))) | |
(def arg->item (req key) | |
(safe-item:saferead (arg req key))) | |
(def live (i) (nor i!dead i!deleted)) | |
(def live-child (d) (find live (kids d))) | |
(def save-item (i) (save-table i (string storydir* i!id))) | |
(def kill (i) | |
(assert i!dead) | |
(save-item i)) | |
(def newslog args (apply srvlog 'news args)) | |
; Ranking | |
; Votes divided by the age in hours to the gravityth power. | |
; Would be interesting to scale gravity in a slider. | |
(= gravity* 1.4 timebase* 120 front-threshold* 1) | |
(def frontpage-rank (s (o gravity gravity*)) | |
(/ (- (realscore s) 1) | |
(expt (/ (+ (item-age s) timebase*) 60) gravity))) | |
(def realscore (i) (- i!score i!sockvotes)) | |
(def item-age (i) (minutes-since i!time)) | |
(def user-age (u) (minutes-since (uvar u created))) | |
; Only looks at the 1000 most recent stories, which might one day be a | |
; problem if there is massive spam. | |
(def gen-topstories () | |
(= ranked-stories* (rank-stories 180 1000 (memo frontpage-rank)))) | |
(def save-topstories () | |
(writefile1 (map [_ 'id] (firstn 180 ranked-stories*)) | |
(+ newsdir* "topstories"))) | |
(def rank-stories (n consider scorefn) | |
(bestn n (compare > scorefn) (recent-stories consider))) | |
; The n most recent stories. Use firstn when add virtual lists. | |
(def recent-stories (n (o id maxid*) (o acc nil)) | |
(if (or (< n 1) (< id 1)) | |
(rev acc) | |
(let s (item id) | |
(if (storylike s) | |
(recent-stories (- n 1) (- id 1) (cons s acc)) | |
(recent-stories n (- id 1) acc))))) | |
(def storylike (i) (and i (astory i))) | |
(def adjust-rank (s (o scorefn frontpage-rank)) | |
(insortnew (compare > (memo scorefn)) s ranked-stories*) | |
(save-topstories)) | |
; If something rose high then stopped getting votes, its score would | |
; decline but it would stay near the top. Newly inserted stories would | |
; thus get stuck in front of it. I avoid this by regularly adjusting | |
; the rank of a random top story. | |
(def rerank-random ((o depth 15)) | |
(when ranked-stories* | |
(adjust-rank (ranked-stories* (rand (min depth (len ranked-stories*)))))) | |
(save-topstories)) | |
(def topstories (user n (o threshold front-threshold*)) | |
(firstn-that n | |
[and (>= (realscore _) threshold) (cansee user _)] | |
ranked-stories*)) | |
; If had ip of current request could add clause below to make ignore | |
; tighter better, but wait till need to. | |
(def cansee (user i) | |
(if i!deleted (admin user) | |
i!dead (or (is user i!by) (seesdead user)) | |
; ip of this request is i!ip | |
t)) | |
(def seesdead (user) | |
(or (and user (uvar user showdead) (no (uvar user ignore))) | |
(editor user))) | |
(def visible (user is) | |
(keep [cansee user _] is)) | |
(def cansee-descendant (user c) | |
(or (cansee user c) | |
(some [cansee-descendant user (item _)] | |
c!kids))) | |
(def editor (u) | |
(and u (or (admin u) (> (uvar u auth) 0)))) | |
(def member (u) | |
(and u (or (admin u) (uvar u member)))) | |
; Page Layout | |
(= up-url* "grayarrow.gif" down-url* "graydown.gif" logo-url* "y18.gif") | |
(defopr favicon.ico req favicon-url*) | |
(mac npage (title . body) | |
`(tag html | |
(tag head | |
(prn "<link rel=\"stylesheet\" type=\"text/css\" href=\"news.css\">") | |
(prn "<link rel=\"shortcut icon\" href=\"" favicon-url* "\">") | |
(tag script (pr votejs*)) | |
(tag title (pr ,title))) | |
(tag body | |
(center | |
(tag (table border 0 cellpadding 0 cellspacing 0 width "85%" | |
bgcolor sand) | |
,@body))))) | |
(= pagefns* nil) | |
(mac fulltop (user label title whence . body) | |
(w/uniq (gu gl gt gw) | |
`(with (,gu ,user ,gl ,label ,gt ,title ,gw ,whence) | |
(npage (+ this-site* (if ,gt (+ bar* ,gt) "")) | |
(if (check-procrast ,gu) | |
(do (pagetop 'full ,gl ,gt ,gu ,gw) | |
(hook 'page ,gu ,gl) | |
,@body) | |
(row (procrast-msg ,gu ,gw))))))) | |
(mac longpage (user t1 label title whence . body) | |
(w/uniq (gu gt) | |
`(with (,gu ,user ,gt ,t1) | |
(fulltop ,gu ,label ,title ,whence | |
(trtd ,@body) | |
(trtd (vspace 10) | |
(color-stripe (main-color ,gu)) | |
(br) | |
(center | |
(hook 'longfoot) | |
(admin-bar ,gu (- (msec) ,gt) ,whence))))))) | |
(def admin-bar (user elapsed whence) | |
(when (admin user) | |
(br2) | |
(w/bars | |
(pr (len items*) "/" maxid* " loaded") | |
(pr elapsed " msec") | |
(link "settings" "newsadmin") | |
(hook 'admin-bar user whence)))) | |
(def color-stripe (c) | |
(tag (table width "100%" cellspacing 0 cellpadding 1) | |
(tr (tdcolor c)))) | |
(mac shortpage (user label title whence . body) | |
`(fulltop ,user ,label ,title ,whence | |
(trtd ,@body))) | |
(mac minipage (label . body) | |
`(npage (+ this-site* bar* ,label) | |
(pagetop nil ,label) | |
(trtd ,@body))) | |
; remember to (= caching* 0) or won't see changes | |
(defop news.css req | |
(pr " | |
body { font-family:Verdana; font-size:10pt; color:#828282; } | |
td { font-family:Verdana; font-size:10pt; color:#828282; } | |
.admin td { font-family:Verdana; font-size:8.5pt; color:#000000; } | |
.subtext td { font-family:Verdana; font-size: 7pt; color:#828282; } | |
input { font-family:Courier; font-size:10pt; color:#000000; } | |
input[type=\"submit\"] { font-family:Verdana; } | |
textarea { font-family:Courier; font-size:10pt; color:#000000; } | |
a:link { color:#000000; text-decoration:none; } | |
a:visited { color:#828282; text-decoration:none; } | |
.default { font-family:Verdana; font-size: 10pt; color:#828282; } | |
.admin { font-family:Verdana; font-size:8.5pt; color:#000000; } | |
.title { font-family:Verdana; font-size: 10pt; color:#828282; } | |
.adtitle { font-family:Verdana; font-size: 9pt; color:#828282; } | |
.subtext { font-family:Verdana; font-size: 7pt; color:#828282; } | |
.yclinks { font-family:Verdana; font-size: 8pt; color:#828282; } | |
.pagetop { font-family:Verdana; font-size: 10pt; color:#222222; } | |
.comhead { font-family:Verdana; font-size: 8pt; color:#828282; } | |
.comment { font-family:Verdana; font-size: 9pt; } | |
.dead { font-family:Verdana; font-size: 9pt; color:#dddddd; } | |
.comment a:link, .comment a:visited { text-decoration:underline;} | |
.dead a:link, .dead a:visited { color:#dddddd; } | |
.pagetop a:visited { color:#000000;} | |
.topsel a:link, .topsel a:visited { color:#ffffff; } | |
.subtext a:link, .subtext a:visited { color:#828282; } | |
.subtext a:hover { text-decoration:underline; } | |
.comhead a:link, .subtext a:visited { color:#828282; } | |
.comhead a:hover { text-decoration:underline; } | |
.default p { margin-top: 8px; margin-bottom: 0px; } | |
.pagebreak {page-break-before:always} | |
pre { overflow: hidden; padding: 2px; } | |
pre:hover {overflow:auto} ")) | |
; only need pre padding because of a bug in Mac Firefox | |
; Without setting the bottom margin of p tags to 0, 1- and n-para comments | |
; have different space at the bottom. This solution suggested by Devin. | |
; Really am using p tags wrong (as separators rather than wrappers) and the | |
; correct thing to do would be to wrap each para in <p></p>. Then whatever | |
; I set the bottom spacing to, it would be the same no matter how many paras | |
; in a comment. In this case by setting the bottom spacing of p to 0, I'm | |
; making it the same as no p, which is what the first para has. | |
; supplied by pb | |
;.vote { padding-left:2px; vertical-align:top; } | |
;.comment { margin-top:1ex; margin-bottom:1ex; color:black; } | |
;.vote IMG { border:0; margin: 3px 2px 3px 2px; } | |
;.reply { font-size:smaller; text-decoration:underline !important; } | |
(= votejs* " | |
function byId(id) { | |
return document.getElementById(id); | |
} | |
function vote(node) { | |
var v = node.id.split(/_/); // {'up', '123'} | |
var item = v[1]; | |
// adjust score | |
var score = byId('score_' + item); | |
var newscore = parseInt(score.innerHTML) + (v[0] == 'up' ? 1 : -1); | |
score.innerHTML = newscore + (newscore == 1 ? ' point' : ' points'); | |
// hide arrows | |
byId('up_' + item).style.visibility = 'hidden'; | |
byId('down_' + item).style.visibility = 'hidden'; | |
// ping server | |
var ping = new Image(); | |
ping.src = node.href; | |
return false; // cancel browser nav | |
} ") | |
; Page top | |
(= sand (color 246 246 239) textgray (gray 130)) | |
(def main-color (user) | |
(aif (and user (uvar user topcolor)) | |
(hex>color it) | |
site-color*)) | |
(def pagetop (switch label (o title) (o user) (o whence)) | |
(tr (tdcolor (main-color user) | |
(tag (table border 0 cellpadding 0 cellspacing 0 width "100%" | |
style "padding:2px") | |
(tr (gen-logo) | |
(when (is switch 'full) | |
(tag (td style "line-height:12pt; height:10px;") | |
(spanclass pagetop | |
(tag b (link this-site* "news")) | |
(hspace 10) | |
(toprow user label)))) | |
(if (is switch 'full) | |
(tag (td style "text-align:right;padding-right:4px;") | |
(spanclass pagetop (topright user whence))) | |
(tag (td style "line-height:12pt; height:10px;") | |
(spanclass pagetop (prbold label)))))))) | |
(map [_ user] pagefns*) | |
(spacerow 10)) | |
(def gen-logo () | |
(tag (td style "width:18px;padding-right:4px") | |
(tag (a href parent-url*) | |
(gentag img src logo-url* width 18 height 18 | |
style "border:1px white solid;")))) | |
(= toplabels* '(nil "new" "threads" "comments" "leaders" "*")) | |
(def toprow (user label) | |
(w/bars | |
(toplink "new" "newest" label) | |
(when user | |
(toplink "threads" (threads-url user) label)) | |
(toplink "comments" "newcomments" label) | |
(toplink "leaders" "leaders" label) | |
(hook 'toprow user label) | |
(link "submit") | |
(unless (mem label toplabels*) | |
(tag (font color white) (pr label))))) | |
(def toplink (name dest label) | |
(tag-if (is name label) (span class 'topsel) | |
(link name dest))) | |
(def topright (user whence (o showkarma t)) | |
(when user | |
(link user (user-url user)) | |
(when showkarma (pr " (" (karma user) ")")) | |
(pr " | ")) | |
(if user | |
(rlinkf 'logout (req) | |
(when-umatch/r user req | |
(logout-user user) | |
whence)) | |
(onlink "login" | |
(login-page 'both nil | |
(list (fn (u ip) | |
(ensure-news-user u) | |
(newslog u 'top-login ip)) | |
whence))))) | |
; News-Specific Defop Variants | |
(mac defopt (name parm test msg . body) | |
`(defop ,name ,parm | |
(if (,test (get-user ,parm)) | |
(do ,@body) | |
(login-page 'both (+ "Please log in" ,msg ".") | |
(list (fn (u ip) (ensure-news-user u)) | |
(string ',name (reassemble-args ,parm))))))) | |
(mac defopg (name parm . body) | |
`(defopt ,name ,parm idfn "" ,@body)) | |
(mac defope (name parm . body) | |
`(defopt ,name ,parm editor " as an editor" ,@body)) | |
(mac defopa (name parm . body) | |
`(defopt ,name ,parm admin " as an administrator" ,@body)) | |
(mac opexpand (definer name parms . body) | |
(w/uniq gr | |
`(,definer ,name ,gr | |
(with (user (get-user ,gr) ip (,gr 'ip)) | |
(with ,(and parms (mappend [list _ (list 'arg gr (string _))] | |
parms)) | |
(newslog user ',name ip ,@parms) | |
,@body))))) | |
(= newsop-names* nil) | |
(mac newsop args | |
`(do (pushnew ',(car args) newsop-names*) | |
(opexpand defop ,@args))) | |
(mac adop (name parms . body) | |
(w/uniq g | |
`(opexpand defopa ,name ,parms | |
(let ,g (string ',name) | |
(shortpage user ,g ,g ,g | |
,@body))))) | |
; News Admin | |
(defopa newsadmin req (newsadmin-page (get-user req))) | |
; For emergency, real-time changes. All are reset to the val in the | |
; source code when restart server. | |
(def nad-fields () | |
`((num caching ,caching* t t) | |
(posint front-threshold ,front-threshold* t t) | |
(int legit-threshold ,legit-threshold* t t) | |
(bigtoks url-kill ,url-kill* t t) | |
(bigtoks url-ignore ,url-ignore* t t) | |
(bigtoks comment-kill ,comment-kill* t t) | |
(bigtoks comment-ignore ,comment-ignore* t t) | |
(bigtoks ip-ban ,ip-ban* t t))) | |
; Need a util like vars-form for a collection of variables. | |
; Or could generalize vars-form to think of places (in the setf sense). | |
(def newsadmin-page (user) | |
(newslog user 'newsadmin) | |
(shortpage user "newsadmin" "News Admin Page" "newsadmin" | |
(vars-form user (nad-fields) | |
(fn (name val) | |
(case name | |
caching (= caching* val) | |
front-threshold (= front-threshold* val) | |
legit-threshold (= legit-threshold* val) | |
url-kill (= url-kill* val) | |
url-ignore (= url-ignore* val) | |
comment-kill (= comment-kill* val) | |
comment-ignore (= comment-ignore* val) | |
ip-ban (= ip-ban* val))) | |
(fn () (newsadmin-page user))) | |
(br2) | |
(aform (fn (req) | |
(with (user (get-user req) subject (arg req "id")) | |
(if (profs* subject) | |
(do (killallby subject) | |
(submitted-page user subject)) | |
(if (admin user) (newsadmin-page user))))) | |
(single-input "" 'id 20 "kill all by")))) | |
; Users | |
(newsop user (id) | |
(if (profs* id) | |
(user-page user id) | |
(pr "No such user."))) | |
(def user-page (user subject) | |
(let here (user-url subject) | |
(shortpage user nil (+ "Profile: " subject) here | |
(profile-form user subject) | |
(br2) | |
(when (some astory:item (uvar subject submitted)) | |
(underlink "submissions" (submitted-url subject))) | |
(when (some acomment:item (uvar subject submitted)) | |
(sp) | |
(underlink "comments" (threads-url subject))) | |
(hook 'user user subject)))) | |
(def profile-form (user subject) | |
(let prof (profs* subject) | |
(vars-form user | |
(user-fields user subject) | |
(fn (name val) (= (prof name) val)) | |
(fn () (save-prof subject) | |
(user-page user subject))))) | |
(= topcolor-threshold* 250) | |
(def user-fields (user subject) | |
(withs (e (editor user) | |
a (admin user) | |
w (is user subject) | |
k (and w (> (karma user) topcolor-threshold*)) | |
u (or a w) | |
m (or a (and (member user) w)) | |
p (profs* subject)) | |
`((string user ,subject t nil) | |
(string name ,(p 'name) ,m ,m) | |
(string created ,(text-age:user-age subject) t nil) | |
(string password ,(resetpw-link) ,w nil) | |
(string saved ,(saved-link user subject) ,u nil) | |
(int auth ,(p 'auth) ,e ,a) | |
(yesno member ,(p 'member) ,a ,a) | |
(posint karma ,(p 'karma) t ,a) | |
(yesno ignore ,(p 'ignore) ,e ,e) | |
(num weight ,(p 'weight) ,a ,a) | |
(yesno nodowns ,(p 'nodowns) ,a ,a) | |
(mdtext about ,(p 'about) t ,u) | |
(string email ,(p 'email) ,u ,u) | |
(yesno showdead ,(p 'showdead) ,u ,u) | |
(yesno noprocrast ,(p 'noprocrast) ,u ,u) | |
(string firstview ,(p 'firstview) ,a nil) | |
(string lastview ,(p 'lastview) ,a nil) | |
(posint maxvisit ,(p 'maxvisit) ,u ,u) | |
(posint minaway ,(p 'minaway) ,u ,u) | |
(sexpr keys ,(p 'keys) ,a ,a) | |
(hexcol topcolor ,(or (p 'topcolor) (hexrep site-color*)) ,k ,k)))) | |
(def saved-link (user subject) | |
(when (or (admin user) (is user subject)) | |
(let n (len (voted-stories user subject)) | |
(if (is n 0) | |
"" | |
(tostring (underlink n (saved-url subject))))))) | |
(def resetpw-link () | |
(tostring (underlink "reset password" "resetpw"))) | |
; Main Operators | |
; remember to set caching to 0 when testing non-logged-in | |
(= caching* 0 perpage* 30 maxend* 200) | |
; Limiting that newscache can't take any arguments except the user. | |
; To allow other arguments, would have to turn the cache from a single | |
; stored value to a hash table whose keys were lists of arguments. | |
(mac newscache (name user time . body) | |
(w/uniq gc | |
`(let ,gc (cache (fn () (* caching* ,time)) | |
(fn () (tostring (let ,user nil ,@body)))) | |
(def ,name (,user) | |
(if ,user | |
(do ,@body) | |
(pr (,gc))))))) | |
(newsop news () (newspage user)) | |
(newsop || () (newspage user)) | |
;(newsop index.html () (newspage user)) | |
(newscache newspage user 90 | |
(rerank-random) | |
(listpage user (msec) (topstories user maxend*) nil nil "news" t)) | |
(def listpage (user t1 items label title (o url label) (o number)) | |
(hook 'listpage user) | |
(longpage user t1 label title url | |
(display-items user items label title url 0 perpage* number))) | |
(newsop newest () (newestpage user)) | |
; Note: dead/deleted items will persist for the remaining life of the | |
; cached page. If this were a prob, could make deletion clear caches. | |
(newscache newestpage user 40 | |
(rerank-random) | |
(listpage user (msec) (newstories user maxend*) "new" "New Links" "newest" t)) | |
(def newstories (user n) | |
(firstn-that n [cansee user _] stories*)) | |
(newsop best () (bestpage user)) | |
(newscache bestpage user 1000 | |
(listpage user (msec) (beststories user maxend*) "best" "Top Links" "best" t)) | |
; As no of stories gets huge, could test visibility in fn sent to best. | |
(def beststories (user n) | |
(bestn n (compare > realscore) (visible user stories*))) | |
(newsop bestcomments () (bestcpage user)) | |
(newscache bestcpage user 1000 | |
(listpage user (msec) (bestcomments user maxend*) | |
"best comments" "Best Comments" "bestcomments")) | |
(def bestcomments (user n) | |
(bestn n (compare > realscore) (visible user comments*))) | |
(newsop lists () | |
(longpage user (msec) "lists" "Lists" "lists" | |
(tag table | |
(row "" (hspace 10)) | |
(row (link "best") "" "Highest voted recent links.") | |
(row (link "active") "" "Most active current discussions.") | |
(row (link "bestcomments") "" "Highest voted recent comments.") | |
(when (admin user) | |
(map [row (link _)] | |
'(optimes killed badguys badlogins goodlogins))) | |
(hook 'listspage user)))) | |
(def saved-url (user) (string "saved?id=" user)) | |
(newsop saved (id) | |
(if (profs* id) | |
(savedpage user id) | |
(pr "No such user."))) | |
(def savedpage (user subject) | |
(if (or (is user subject) (admin user)) | |
(listpage user (msec) | |
(sort (compare < item-age) (voted-stories user subject)) | |
"saved" "Saved Links" (saved-url subject) t) | |
(pr "Can't display that."))) | |
(def voted-stories (user subject) | |
(keep [and (astory _) (cansee user _)] | |
(map item (keys:votes* subject)))) | |
; Story Display | |
(def display-items (user items label title whence | |
(o start 0) (o end perpage*) (o number)) | |
(zerotable | |
(let n start | |
(each i (if end (cut items start end) items) | |
(display-item (and number (++ n)) i user whence t) | |
(spacerow (if (acomment i) 15 5)))) | |
(when end | |
(let newend (+ end perpage*) | |
(when (and (<= newend maxend*) (< end (len items))) | |
(spacerow 10) | |
(tr (tag (td colspan (if number 2 1))) | |
(tag (td class 'title) | |
(morelink items label title end newend number)))))))) | |
; This code is inevitably complex because the More fn needs to know | |
; its own fnid in order to supply a correct whence arg to stuff on | |
; the page it generates, like logout and delete links. | |
(def morelink (items label title end newend number) | |
(tag (a href (url-for | |
(afnid (fn (req) | |
(prn) | |
(let user (get-user req) | |
(newslog user 'more label) | |
(longpage user (msec) label title (url-for it) | |
(display-items user items | |
label title (url-for it) | |
end newend number)))))) | |
rel 'nofollow) | |
(pr "More"))) | |
(def display-story (i s user whence) | |
(when (or (cansee user s) (s 'kids)) | |
(tr (display-item-number i) | |
(td (votelinks s user whence)) | |
(titlelink s s!url user)) | |
(tr (tag (td colspan (if i 2 1))) | |
(tag (td class 'subtext) | |
(hook 'itemline s user) | |
(itemline s user) | |
(when (astory s) (commentlink s user)) | |
(editlink s user) | |
(when (admin user) | |
(pr bar*) | |
(w/rlink (do (zap no s!dead) | |
(save-item s) | |
whence) | |
(pr (if (s 'dead) "unkill" "kill")))) | |
(deletelink s user whence))))) | |
(def display-item-number (i) | |
(when i (tag (td align 'right valign 'top class 'title) | |
(pr i ".")))) | |
(= follow-threshold* 5) | |
(def titlelink (s url user) | |
(tag (td class 'title) | |
(if (cansee user s) | |
(do (let toself (blank url) | |
(tag (a href (if toself (item-url s) url) | |
rel (unless (or toself | |
(> (realscore s) follow-threshold*)) | |
'nofollow)) | |
(pr (s 'title)))) | |
(deadmark s user) | |
(awhen (and (valid-url url) (sitename url)) | |
(spanclass comhead | |
(pr " (" it ") ")))) | |
(pr (pseudo-text s))))) | |
(def pseudo-text (i) | |
(if i!deleted "[deleted]" "[dead]")) | |
(def deadmark (i user) | |
(when (and i!dead (seesdead user)) | |
(pr " [dead]")) | |
(when (and i!deleted (admin user)) | |
(pr " [deleted]"))) | |
(= downvote-threshold* 20 downvote-time* 1440) | |
(= votewid* 14) | |
(def votelinks (i user whence (o downtoo)) | |
(center | |
(if (and (cansee user i) | |
(or (no user) | |
(no ((votes* user) i!id)))) | |
(do (votelink i user whence 'up) | |
(if (and downtoo | |
(or (admin user) | |
(< (item-age i) downvote-time*)) | |
(canvote user i 'down)) | |
(do (br) | |
(votelink i user whence 'down)) | |
(tag (span id (string "down_" i!id))))) | |
(is user i!by) | |
(do (tag (font color orange) (pr "*")) | |
(br) | |
(hspace votewid*)) | |
(hspace votewid*)))) | |
(def votelink (i user whence dir) | |
(tag (a id (string dir '_ i!id) | |
onclick (if user "return vote(this)") | |
href (if user | |
(string "vote?by=" user "&for=" i!id "&dir=" dir) | |
(flink (vote-fn i whence dir)))) | |
(gentag img src (case dir up up-url* down down-url*) | |
border 0 vspace 3 hspace 2))) | |
(def vote-fn (i whence dir) | |
(fn (req) | |
(login-page 'both "You have to be logged in to vote." | |
(list (fn (u ip) | |
(ensure-news-user u) | |
(newslog u 'vote-login ip) | |
(vote-for u i dir) | |
(logvote u i)) | |
whence)))) | |
; Not much stricter than whether to generate the arrow. Further tests | |
; applied in vote-for. | |
(def canvote (user i dir) | |
(and user | |
(no ((votes* user) i!id)) | |
(or (is dir 'up) | |
(and (acomment i) | |
(> (karma user) downvote-threshold*) | |
(no (aand i!parent (is user ((item it) 'by)))))))) | |
; Can't use this for links when not logged in, because doesn't know | |
; where to redirect after the login. But that's few fnids anyway | |
; because pages with those links are cached. Now that have Javascript | |
; voting, can use for all other votes because never have to regen | |
; the page. | |
(newsop vote (by for dir) | |
(let dir (saferead dir) | |
(if (isnt by user) | |
(pr "User mismatch.") | |
(aif (safe-item for) | |
(if (and (in dir 'up 'down) (canvote user it dir)) | |
(do (vote-for by it dir) | |
(logvote by it)) | |
(pr "Can't make that vote.")) | |
(pr "No such item."))))) | |
(def itemline (i user) | |
(when (cansee user i) | |
(when (news-type i) | |
(tag (span id (string "score_" i!id)) | |
(pr i!score (plural i!score " point")))) | |
(byline i))) | |
(def byline (i) | |
(pr " by ") | |
(link i!by (user-url i!by)) | |
(pr " " (text-age:item-age i) " ")) | |
(def user-url (user) (+ "user?id=" user)) | |
(def commentlink (i user) | |
(when (cansee user i) (pr bar*)) ; smells like a hack | |
(tag (a href (item-url i)) | |
(let n (- (len (visible user (family i))) 1) | |
(if (> n 0) | |
(pr n (plural n " comment")) | |
(pr "discuss"))))) | |
(def family (i) (cons i (mappend family:item i!kids))) | |
(= user-changetime* 120 editor-changetime* 1440) | |
(= everchange* (table) noedit* (table)) | |
(def canedit (user i) | |
(or (admin user) | |
(and (~noedit* i!type) | |
(editor user) | |
(< (item-age i) editor-changetime*)) | |
(own-changeable-item user i))) | |
(def own-changeable-item (user i) | |
(and (is user i!by) | |
(no i!deleted) | |
(or (everchange* i!type) | |
(< (item-age i) user-changetime*)))) | |
(def editlink (story user) | |
(when (canedit user story) | |
(pr bar*) | |
(link "edit" (edit-url story)))) | |
(def candelete (user story) | |
(or (admin user) (own-changeable-item user story))) | |
(def deletelink (i user whence) | |
(when (candelete user i) | |
(pr bar*) | |
(linkf (if i!deleted "undelete" "delete") (req) | |
(let user (get-user req) | |
(if (candelete user i) | |
(del-confirm-page user i whence) | |
(prn "You can't delete that.")))))) | |
; Undeleting stories could cause a slight inconsistency. If a story | |
; linking to x gets deleted, another submission can take its place in | |
; url->story. If the original is then undeleted, there will be two | |
; stories with equal claim to be in url->story. (The more recent will | |
; win because it happens to get loaded later.) Not a big problem. | |
(def del-confirm-page (user i whence) | |
(minipage "Confirm" | |
(tab | |
(display-item nil | |
i user | |
; never used so not testable but think correct | |
(flink [del-confirm-page (get-user _) i whence])) | |
(spacerow 20) | |
(tr (td) | |
(td (urform user req | |
(do (when (candelete user i) | |
(= i!deleted (is (arg req "b") "Yes")) | |
(save-item i)) | |
whence) | |
(prn "Do you want this to " | |
(if i!deleted "remain" "be") | |
" deleted?") | |
(br2) | |
(but "Yes" "b") (sp) (but "No" "b"))))))) | |
(def permalink (story user) | |
(when (cansee user story) | |
(pr bar*) | |
(link "link" (item-url story)))) | |
(def logvote (user story) | |
(newslog user 'vote (story 'id) (list (story 'title)))) | |
(def text-age (a) | |
(tostring | |
(if (>= a 1440) (let n (trunc (/ a 1440)) | |
(pr n (plural n " day") " ago")) | |
(>= a 60) (let n (trunc (/ a 60)) | |
(pr n (plural n " hour") " ago")) | |
(let n (trunc a) | |
(pr n (plural n " minute") " ago"))))) | |
; Voting | |
; A user needs legit-threshold karma for a vote to count if there has | |
; already been a vote from the same IP address. A new account below both | |
; new- thresholds won't affect rankings, though such votes still affect | |
; scores unless not a legit-user. | |
(= legit-threshold* 0 new-age-threshold* 0 new-karma-threshold* 0) | |
(def legit-user (user) | |
(or (editor user) | |
(> (karma user) legit-threshold*))) | |
(def possible-sockpuppet (user) | |
(or (uvar user ignore) | |
(< (uvar user weight) .5) | |
(and (< (user-age user) new-age-threshold*) | |
(< (karma user) new-karma-threshold*)))) | |
(= recent-votes* nil) | |
; Note: if vote-for by one user changes (s 'score) while s is being | |
; edited by another, the save after the edit will overwrite the change. | |
; Actual votes can't be lost because that field is not editable. Not a | |
; big enough problem to drag in locking. | |
(def vote-for (user i (o dir 'up)) | |
(unless ((votes* user) i!id) | |
(atwiths (ip (logins* user) | |
vote (list (seconds) ip user dir i!score)) | |
(unless (or (and (uvar user ignore) | |
(isnt user i!by)) | |
; prevention of karma-bombing | |
(and (is dir 'down) | |
(or (and (~editor user) (just-downvoted user i!by)) | |
(uvar user nodowns))) | |
(and (no (legit-user user)) | |
(find [is (cadr _) ip] i!votes))) | |
(case dir up (++ i!score) | |
down (-- i!score)) | |
; canvote protects against sockpuppet downvote of comments | |
(when (and (is dir 'up) (possible-sockpuppet user)) | |
(++ i!sockvotes)) | |
(if (storylike i) (adjust-rank i)) | |
; get equal karma for comments | |
(unless (or (is user i!by) | |
(and (is ip i!ip) (~editor user))) | |
(case dir up (++ (karma i!by)) | |
down (-- (karma i!by))) | |
(save-prof i!by))) | |
(push vote i!votes) | |
(push (cons i!id vote) recent-votes*) | |
(save-item i) | |
(= ((votes* user) i!id) vote) | |
(save-votes user)))) | |
(def just-downvoted (user victim (o n 3)) | |
(let prev (firstn n (recent-votes-by user)) | |
(and (is (len prev) n) | |
(all (fn ((id sec ip voter dir score)) | |
(and (is ((item id) 'by) victim) (is dir 'down))) | |
prev)))) | |
; Ugly to pluck out fourth element. Should read votes into a vote | |
; template. They're stored slightly differently in two diff places: | |
; in one with the voter in the car and the other without. | |
(def recent-votes-by (user) | |
(keep [is (_ 3) user] recent-votes*)) | |
; Story Submission | |
(newsop submit () | |
(if user | |
(submit-page user "" "" t) | |
(submit-login-warning "" "" t))) | |
(def submit-login-warning ((o url) (o title) (o showtext) (o text)) | |
(login-page 'both "You have to be logged in to submit." | |
(fn (user ip) | |
(ensure-news-user user) | |
(newslog user 'submit-login ip) | |
(submit-page user url title showtext text)))) | |
(def submit-page (user (o url) (o title) (o showtext) (o text "") (o msg)) | |
(minipage "Submit" | |
(pagemessage msg) | |
(urform user req | |
(process-story (get-user req) | |
(striptags (arg req "u")) | |
(striptags (arg req "t")) | |
showtext | |
(and showtext (md-from-form (arg req "x") t)) | |
(req 'ip)) | |
(tab | |
(row "title" (input "t" title 50)) | |
(if prefer-url* | |
(do (row "url" (input "u" url 50)) | |
(when showtext | |
(row "" "<b>or</b>") | |
(row "text" (textarea "x" 4 50 (only.pr text))))) | |
(do (row "text" (textarea "x" 4 50 (only.pr text))) | |
(row "" "<b>or</b>") | |
(row "url" (input "u" url 50)))) | |
(row "" (submit)) | |
(spacerow 20) | |
(row "" submit-instructions*))))) | |
(= submit-instructions* | |
"Leave url blank to submit a question for discussion. If there is | |
no url, the text (if any) will appear at the top of the comments | |
page. If there is a url, the text will be ignored.") | |
; For use by outside code like bookmarklet. | |
; http://news.domain.com/submitlink?u=http://foo.com&t=Foo | |
; Added a confirm step to avoid xss hacks. | |
(newsop submitlink (u t) | |
(if user | |
(submit-page user u t) | |
(submit-login-warning u t))) | |
(= title-limit* 100 | |
retry* "Please try again." | |
toolong* (string "Please make title < " title-limit* " characters.") | |
bothblank* "The url and text fields can't both be blank. Please | |
either supply a url, or if you're asking a question, | |
put it in the text field.") | |
(def process-story (user url title showtext text ip) | |
(aif (and (~blank url) (live-story-w/url url)) | |
(do (vote-for user it) | |
(item-url it)) | |
(if (no user) | |
(flink [submit-login-warning url title showtext text]) | |
(no (and (or (blank url) (valid-url url)) | |
(~blank title))) | |
(flink [submit-page user url title showtext text retry*]) | |
(len> title title-limit*) | |
(flink [submit-page user url title showtext text toolong*]) | |
(and (blank url) (blank text)) | |
(flink [submit-page user url title showtext text bothblank*]) | |
(atlet s (create-story url (scrubtitle title) text user ip) | |
(ban-test user s ip url url-kill* url-ignore*) | |
(when (uvar user ignore) (kill s)) | |
(push s!id (uvar user submitted)) | |
(save-prof user) | |
(vote-for user s) | |
"newest")))) | |
(= scrubrules* '(("Breaking: " "") ("Exclusive: " ""))) | |
; Note that by deliberate tricks, someone could thus submit a story | |
; with a blank title. | |
(def scrubtitle (str) (multisubst scrubrules* str)) | |
(def live-story-w/url (url) | |
(aand (url->story* url) (check it live))) | |
; Kill means stuff with this substring gets killed. Ignore is stronger, | |
; means that user will be auto-ignored. Eventually this info should | |
; be stored on disk and not in the source code. | |
(= url-kill* nil | |
url-ignore* '("internetisseriousbusiness") | |
comment-kill* nil | |
comment-ignore* '("http://internetisseriousbusiness" | |
"http://www.internetisseriousbusiness") | |
ip-ban* nil) | |
; Kill submissions from banned ips, but don't auto-ignore users from | |
; them, because eventually ips will become legit again. | |
; Note that ban tests are only applied when a link or comment is | |
; submitted, not each time it's edited. This will do for now. | |
(def ban-test (user i ip string kill-list ignore-list) | |
(when (some [posmatch _ string] ignore-list) | |
(assert (uvar user ignore)) | |
(save-prof user)) | |
(when (or (mem ip ip-ban*) (some [posmatch _ string] kill-list)) | |
(kill i))) | |
(def killallby (user) (map kill (submissions user))) | |
; Only called from repl. | |
(def kill-whole-thread (c) | |
(kill c) | |
(map kill-whole-thread:item c!kids)) | |
; Could be stricter. | |
(def valid-url (url) | |
(and (len> url 10) | |
(begins url "http://") | |
(~find [in _ #\< #\> #\"] url))) | |
(def parse-site (url) | |
(rev (tokens (cadr (tokens url [in _ #\/ #\?])) #\.))) | |
(defmemo sitename (url) | |
(let toks (parse-site url) | |
(if (isa (saferead (car toks)) 'int) | |
(tostring (prall toks "" ".")) | |
(let (t1 t2 t3 . rest) toks | |
(if (or (mem t1 multi-tld-countries*) | |
(and t3 (mem t2 long-domains*))) | |
(string t3 "." t2 "." t1) | |
(string t2 "." t1)))))) | |
; Minor bug: can have both google.at and google.co.at. Same for jp. | |
(= multi-tld-countries* '("uk" "jp" "au" "in" "ph" "tr" "za" "my" "nz" "br" | |
"mx" "th" "sg" "id" "pk" "eg" "il" "at")) | |
(= long-domains* '("blogspot" "wordpress" "livejournal" "blogs" "typepad" | |
"weebly" "blog-city" "com")) | |
(def create-story (url title text user ip) | |
(newslog user 'create url (list title)) | |
(let s (inst 'item 'type 'story 'id (new-item-id) | |
'url url 'title title 'text text 'by user 'ip ip) | |
(save-item s) | |
(= (items* s!id) s (url->story* url) s) | |
(push s stories*) | |
s)) | |
; Individual Item Page (= Comments Page of Stories) | |
(def item-url (story) (string "item?id=" (story 'id))) | |
(newsop item (id) | |
(let s (safe-item id) | |
(if (news-type s) | |
(item-page user s) | |
(pr "No such item.")))) | |
(def news-type (s) | |
(and s (or (storylike s) (acomment s)))) | |
(def item-page (user i) | |
(with (title (and (cansee user i) | |
(or i!title (aand i!text (ellipsize (striptags it))))) | |
here (item-url i)) | |
(shortpage user nil title here | |
(tab (display-item nil i user here) | |
(display-item-text i user) | |
(when (and (cansee user i) (live i) (commentable i)) | |
(spacerow 10) | |
(row "" (comment-form i user here)))) | |
(br2) | |
(when (and i!kids (commentable i)) | |
(tab (display-subcomments i user here)))))) | |
(def commentable (i) (in i!type 'comment 'story)) | |
(= displayfn* (table)) | |
(= (displayfn* 'story) (fn (n i user here inlist) | |
(display-story n i user here))) | |
(= (displayfn* 'comment) (fn (n i user here inlist) | |
(display-comment n i user here nil 0 nil inlist))) | |
(def display-item (n i user here (o inlist)) | |
((displayfn* (i 'type)) n i user here inlist)) | |
(def superparent (i) | |
(aif i!parent (superparent:item it) i)) | |
(def display-item-text (s user) | |
(when (and (cansee user s) (astory s) (blank s!url) (~blank s!text)) | |
(spacerow 2) | |
(row "" s!text))) | |
; Edit Item | |
(def edit-url (story) (string "edit?id=" (story 'id))) | |
(newsop edit (id) | |
(let i (safe-item id) | |
(if (and i | |
(cansee user i) | |
(editable-type i) | |
(or (news-type i) (admin user) (is user i!by))) | |
(edit-page user i) | |
(pr "No such item.")))) | |
(def editable-type (i) (fieldfn* i!type)) | |
(= fieldfn* (table)) | |
(= (fieldfn* 'story) | |
(fn (user s) | |
(with (a (admin user) e (editor user) x (canedit user s)) | |
`((string1 title ,(s 'title) t ,x) | |
(url url ,(s 'url) t ,e) | |
(mdtext2 text ,(s 'text) t ,x) | |
(int votes ,(len (s 'votes)) ,a nil) | |
(int score ,(s 'score) t ,a) | |
(int sockvotes ,(s 'sockvotes) ,e ,a) | |
(yesno dead ,(s 'dead) ,e ,e) | |
(yesno deleted ,(s 'deleted) ,a ,a) | |
(string ip ,(s 'ip) ,e nil))))) | |
(= (fieldfn* 'comment) | |
(fn (user s) | |
(with (a (admin user) e (editor user) x (canedit user s)) | |
`((mdtext text ,(s 'text) t ,x) | |
(int score ,(s 'score) t ,a) | |
(int sockvotes ,(s 'sockvotes) ,e ,a) | |
(yesno dead ,(s 'dead) ,e ,e) | |
(yesno deleted ,(s 'deleted) ,a ,a) | |
(string ip ,(s 'ip) ,e nil))))) | |
; Should check valid-url etc here too. In fact make a fn that | |
; does everything that has to happen after submitting a story, | |
; and call it both there and here. | |
(def edit-page (user s) | |
(let here (edit-url s) | |
(shortpage user nil "Edit" here | |
(tab (display-item nil s user here) | |
(display-item-text s user)) | |
(br2) | |
(vars-form user | |
((fieldfn* s!type) user s) | |
(fn (name val) (= (s name) val)) | |
(fn () (save-item s) | |
(if (storylike s) (adjust-rank s)) | |
(edit-page user s))) | |
(hook 'edit user s)))) | |
; Comment Submission | |
(def comment-login-warning (parent whence (o text)) | |
(login-page 'both "You have to be logged in to comment." | |
(fn (u ip) | |
(ensure-news-user u) | |
(newslog u 'comment-login ip) | |
(addcomment-page parent u whence text)))) | |
(def addcomment-page (parent user whence (o text) (o msg)) | |
(minipage "Add Comment" | |
(pagemessage msg) | |
(tab | |
(let here (flink [addcomment-page parent (get-user _) whence text msg]) | |
(display-item nil parent user here)) | |
(spacerow 10) | |
(row "" (comment-form parent user whence text))))) | |
(def comment-form (parent user whence (o text)) | |
(urform user req | |
(process-comment (get-user req) parent (arg req "text") (req 'ip) whence) | |
(textarea "text" 6 60 | |
(aif text (prn (unmarkdown it)))) | |
(br2) | |
(submit (if (astory parent) "add comment" "reply")))) | |
; Have to remove #\returns because a form gives you back "a\r\nb" | |
; instead of just "a\nb". Maybe should just remove returns from | |
; the vals coming in from any form, e.g. in aform. | |
(def process-comment (user parent text ip whence) | |
(if (no user) | |
(flink [comment-login-warning parent whence text]) | |
(empty text) | |
(flink [addcomment-page parent (get-user _) whence text retry*]) | |
(atlet c (create-comment parent (md-from-form text) user ip) | |
(ban-test user c ip text comment-kill* comment-ignore*) | |
(when (uvar user ignore) (kill c)) | |
(push c!id (uvar user submitted)) | |
(save-prof user) | |
(vote-for user c) | |
whence))) | |
(def create-comment (parent text user ip) | |
(newslog user 'comment (parent 'id)) | |
(let c (inst 'item 'type 'comment 'id (new-item-id) | |
'text text 'parent parent!id 'by user 'ip ip) | |
(save-item c) | |
(= (items* c!id) c) | |
(push c!id parent!kids) | |
(save-item parent) | |
(push c comments*) | |
c)) | |
; Comment Display | |
(def display-comment-tree (c user whence (o indent 0) (o initialpar)) | |
(when (cansee-descendant user c) | |
(display-1comment c user whence indent initialpar) | |
(display-subcomments c user whence (+ indent 40)))) | |
(def display-1comment (c user whence indent showpar) | |
(row (tab (display-comment nil c user whence t indent showpar showpar)))) | |
(def display-subcomments (c user whence (o indent 0)) | |
(= c!kids (sort (compare > [frontpage-rank (item _)]) | |
c!kids)) | |
(each k c!kids | |
(display-comment-tree (item k) user whence indent))) | |
(def display-comment (n c user whence (o astree) (o indent 0) | |
(o showpar) (o showon)) | |
(tr (display-item-number n) | |
(when astree (td (hspace indent))) | |
(tag (td valign 'top) | |
(votelinks c user whence t)) | |
(tag (td class 'default) | |
(let parent (and (or (no astree) showpar) (c 'parent)) | |
(spanclass comhead | |
(itemline c user) | |
(permalink c user) | |
(when parent | |
(when (cansee user c) (pr bar*)) | |
(link "parent" (item-url (item parent)))) | |
(editlink c user) | |
(deletelink c user whence) | |
(deadmark c user) | |
(when showon | |
(pr " | on: ") | |
(let s (superparent c) | |
(link (ellipsize s!title 50) | |
(if (empty s!url) (item-url s) s!url))))) | |
(when (or parent (cansee user c)) | |
(br) (vspace 20)) | |
(spanclass comment | |
(if (no (cansee user c)) | |
(pr (pseudo-text c)) | |
(and (no (live c)) (isnt user c!by)) | |
(spanclass dead (pr c!text)) | |
(tag (font color (comment-color c)) | |
(pr c!text)))) | |
(when (and astree (cansee user c) (live c)) | |
(para) | |
(tag (font size 1) | |
(underline (replylink c user whence)))))))) | |
(def replylink (i user whence (o title 'reply)) | |
(linkf title (req) | |
(let user (get-user req) | |
(if user | |
(addcomment-page i user whence) | |
(login-page 'both "You have to be logged in to comment." | |
(fn (u ip) | |
(ensure-news-user u) | |
(newslog u 'comment-login ip) | |
(addcomment-page i u whence))))))) | |
(def comment-color (c) | |
(let s (realscore c) | |
(if (> s 0) black | |
(< s -2) (gray 150) | |
(case s -2 (gray 130) -1 (gray 90) 0 (gray 50))))) | |
; Threads | |
(def threads-url (user) (string "threads?id=" user)) | |
(newsop threads (id) (threads-page user id)) | |
(def threads-page (user subject) | |
(if (profs* subject) | |
(withs (title (string subject "'s comments") | |
label (if (is user subject) "threads" title) | |
here (threads-url subject)) | |
(longpage user (msec) label title here | |
(awhen (keep [and (cansee user _) (no (subcomment _))] | |
(comments subject perpage*)) | |
(tab (each c it | |
(display-comment-tree c user here 0 t)))))) | |
(prn "No such user."))) | |
(def submissions (user (o limit)) | |
(map item (firstn limit (uvar user submitted)))) | |
(def comments (user (o limit)) | |
((afn (ids count) | |
(if (or (no ids) (is count limit)) | |
nil | |
(let c (item (car ids)) | |
(consif (and (acomment c) c) | |
(self (cdr ids) | |
(+ count (if (acomment c) 1 0))))))) | |
(uvar user submitted) 0)) | |
(def subcomment (c) | |
(some [and (acomment _) (is _!by c!by) (no _!deleted)] | |
(ancestors c))) | |
(def ancestors (i) | |
(accum a (trav i!parent | |
a:item | |
[self ((item _) 'parent)]))) | |
; Submitted | |
(def submitted-url (user) (string "submitted?id=" user)) | |
(newsop submitted (id) (submitted-page user id)) | |
(def submitted-page (user subject) | |
(if (profs* subject) | |
(with (label (string subject "'s submissions") | |
here (submitted-url subject)) | |
(longpage user (msec) label label here | |
(if (or (no (uvar subject ignore)) | |
(is user subject) | |
(seesdead user)) | |
(aif (keep [and (astory _) (cansee user _)] | |
(submissions subject)) | |
(display-items user it label label here 0 perpage* t))))) | |
(pr "No such user."))) | |
; RSS | |
(newsop rss () (rsspage nil)) | |
(newscache rsspage user 90 | |
(rss-stories (firstn 25 ranked-stories*))) | |
(def rss-stories (stories) | |
(tag (rss version "2.0") | |
(tag channel | |
(tag title (pr this-site*)) | |
(tag link (pr site-url*)) | |
(tag description (pr site-desc*)) | |
(each s stories | |
(tag item | |
(let comurl (+ site-url* (item-url s)) | |
(tag title (pr (eschtml s!title))) | |
(tag link (pr (if (blank s!url) comurl (eschtml s!url)))) | |
(tag comments (pr comurl)) | |
(tag description | |
(cdata (link "Comments" comurl))))))))) | |
; User Stats | |
(newsop leaders () (leaderspage user)) | |
(= nleaders* 20) | |
(newscache leaderspage user 180 | |
(longpage user (msec) "leaders" "Leaders" "leaders" | |
(zerotable | |
(let i 0 | |
(each u (firstn nleaders* (leading-users)) | |
(tr (tdright (pr (++ i) ".")) | |
(td (hspace 7)) | |
(td (underlink u (user-url u))) | |
(td (hspace 10)) | |
(tdright (pr (karma u)))) | |
(if (is i 10) (spacerow 30))))))) | |
(def leading-users () | |
(sort (compare > [karma _]) | |
(users [and (> (karma _) 1) (~admin _)]))) | |
(adop editors () | |
(tab (each u (users [is (uvar _ auth) 1]) | |
(row (link u (user-url u)))))) | |
; Comment Analysis | |
; Instead of a separate active op, should probably display this info | |
; implicitly by e.g. changing color of commentlink or by showing the | |
; no of comments since that user last looked. | |
(newsop active () (active-page user)) | |
(newscache active-page user 90 | |
(listpage user (msec) (actives user) "active" "Active Threads" "active" t)) | |
(def actives (user (o n maxend*) (o consider 2000)) | |
(visible user (rank-stories n consider (memo active-rank)))) | |
(= active-threshold* 1500) | |
(def active-rank (s) | |
(apply + (map [max 0 (- active-threshold* (item-age _))] | |
(cdr (family s))))) | |
(newsop newcomments () (newcomments-page user)) | |
(newscache newcomments-page user 60 | |
(listpage user (msec) (visible user (firstn maxend* comments*)) | |
"comments" "New Comments" "newcomments")) | |
; Doc | |
(defop formatdoc req | |
(minipage "Formatting Options" | |
(spanclass admin | |
(center (widtable 500 formatdoc*))))) | |
(= formatdoc-url* "formatdoc") | |
(= formatdoc* | |
"Blank lines separate paragraphs. | |
<p> Text after a blank line that is indented by two or more spaces is | |
reproduced verbatim. (This is intended for code.) | |
<p> Text surrounded by asterisks is italicized, if the character after the | |
first asterisk isn't whitespace. | |
<p> Urls become links, except in the text field of a submission.<br><br>") | |
; Noprocrast | |
(def check-procrast (user) | |
(or (no user) | |
(no (uvar user noprocrast)) | |
(let now (seconds) | |
(when (no (uvar user firstview)) | |
(reset-procrast user)) | |
(or (when (< (/ (- now (uvar user firstview)) 60) | |
(uvar user maxvisit)) | |
(= (uvar user lastview) now) | |
(save-prof user) | |
t) | |
(when (> (/ (- now (uvar user lastview)) 60) | |
(uvar user minaway)) | |
(reset-procrast user) | |
t))))) | |
(def reset-procrast (user) | |
(= (uvar user lastview) (= (uvar user firstview) (seconds))) | |
(save-prof user)) | |
(def procrast-msg (user whence) | |
(let m (+ 1 (trunc (- (uvar user minaway) | |
(/ (since (uvar user lastview)) 60)))) | |
(pr "<b>Get back to work!</b>") | |
(para "Sorry, you can't see this page. Based on the anti-procrastination | |
parameters you set in your profile, you'll be able to use the site | |
again in " m (plural m " minute") ".") | |
(para "(If you got this message after submitting something, don't worry, | |
the submission was processed.)") | |
(para "To change your anti-procrastination settings, go to your profile | |
by clicking on your username. If <tt>noprocrast</tt> is set to | |
<tt>yes</tt>, you'll be limited to sessions of <tt>maxvisit</tt> | |
minutes, with <tt>minaway</tt> minutes between them.") | |
(para) | |
(w/rlink whence (underline (pr "retry"))) | |
(hspace 20) | |
(w/rlink (do (reset-procrast user) whence) | |
(underline (pr "override"))) | |
(br2))) | |
; Reset PW | |
(defopg resetpw req (resetpw-page (get-user req))) | |
(def resetpw-page (user (o msg)) | |
(minipage "Reset Password" | |
(if msg | |
(pr msg) | |
((orf no blank) (uvar user email)) | |
(do (pr "Before you do this, please add your email address to your ") | |
(underlink "profile" (user-url user)) | |
(pr ". Otherwise you could lose your account if you mistype | |
your new password."))) | |
(br2) | |
(uform user req (try-resetpw user (arg req "p")) | |
(single-input "New password: " 'p 20 "reset" t)))) | |
(def try-resetpw (user newpw) | |
(if (len< newpw 4) | |
(resetpw-page user "Passwords should be a least 4 characters long. | |
Please choose another.") | |
(do (set-pw user newpw) | |
(newspage user)))) | |
; Abuse Analysis | |
(adop badips () | |
(let (bads goods) (badips) | |
(tab | |
(row "IP" "Dead" "Live") | |
(each ip (sort (compare > (memo [len (bads _)])) | |
(rem [len< (bads _) 3] (keys bads))) | |
(tr (td ip) | |
(tdright | |
(w/link (listpage user (msec) (bads ip) | |
(string "dead from " ip) nil "badips" t) | |
(pr (len (bads ip))))) | |
(tdright | |
(w/link (listpage user (msec) (goods ip) | |
(string "live from " ip) nil "badips" t) | |
(pr (len (goods ip)))))))))) | |
; Sort by time, instead of putting stories before comments? | |
(def badips () | |
(with (bads (table) goods (table)) | |
(each s (+ stories* comments*) | |
(if (s 'dead) | |
(push s (bads (s 'ip))) | |
(push s (goods (s 'ip))))) | |
(ontable k v bads (zap rev (bads k))) | |
(ontable k v goods (zap rev (goods k))) | |
(list bads goods))) | |
(adop killed () | |
(let deads (fn (items) (firstn maxend* (keep [_ 'dead] items))) | |
(display-items user (deads stories*) nil nil "killed") | |
(vspace 35) | |
(color-stripe textgray) | |
(vspace 35) | |
(display-items user (deads comments*) nil nil "killed"))) | |
(adop badguys () | |
(sptab (each user (sort (compare > [uvar _ created]) | |
(users [uvar _ ignore])) | |
(row (link user (user-url user)))))) | |
(adop badlogins () (logins-page bad-logins*)) | |
(adop goodlogins () (logins-page good-logins*)) | |
(def logins-page (source) | |
(sptab (each (time ip user) (firstn 100 (rev (qlist source))) | |
(row time ip user)))) | |
; Stats | |
(adop optimes () | |
(sptab (each name (sort < newsop-names*) | |
(tr (td name) | |
(td (hspace 10)) | |
(tdright (pr (aand (qlist (optimes* name)) | |
(num (avg it) 2 t)))))))) | |
(defop topcolors req | |
(minipage "Custom Colors" | |
(tab | |
(each c (dedup (map downcase (trues [uvar _ topcolor] (keys profs*)))) | |
(tr (td c) (tdcolor (hex>color c) (hspace 30))))))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment