MetaFor besteht aus einem Frontend und einem Backend. Dazwischen vermittelt ein Modul, das gleichzeitig einen sehr einfachen Cache implementiert.
<erl> out(A=#arg{headers=#headers{user_agent="Convera"++_}}) -> [{status, 403}, {content, "text/plain", "You are stupid. Go away."}, break]; out(A) -> ok. </erl><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <meta http-equiv="Content-Script-Type" content="text/javascript"> <title>MetaFor</title> <link rel="alternate stylesheet" type="text/css" href="http://www.fu-mathe-team.de/fumt-css.yaws" title="FU Mathe Team"> <link rel="stylesheet" type="text/css" href="http://laufen-in-wuppertal.de/drsl/dat/drsl.css" title="drsl"> <link rel="icon" href="http://www.laufen-in-wuppertal.de/drsl/drsl.ico" type="image/x-icon"> <style type="text/css"> table{border-collapse:collapse; margin:3px} td,th{border:1px solid #999; padding:0.2ex 0.3em; vertical-align:middle} img{border:0} .even {} .odd {background-color:#e8f0ff} </style> </head> <body> <script type="text/javascript"><!-- function openext() {w=window.open('about:blank','mf-ext'); w.focus();} // --> </script> <h1 style="text-align:center"> <img src="http://www.laufzentrale.de/bilder/metafor.gif" alt="MetaFor" width=170 height=104> </h1> <p><b>Da dieser Service schon lange nicht mehr gepflegt wurde und kaum noch funktioniert hat, wurde er eingestellt.</b> <p> Die unten aufgeführten Beiträge sind in den genannten Foren und Newsgruppen erschienen, zu denen wir in keinerlei Beziehung stehen. Die Verweise führen dorthin. <p> Wir haben jetzt auch einen <a href="metafor-rss.yaws">RSS-Feed</a>. Da er die Texte der Artikel nicht enthält, ist er nur von eingeschränkter Nützlichkeit, eignet sich aber zum Beispiel für die Live Bookmarks von <a target="_top" href="http://www.mozilla.org/products/firefox/">Firefox 1.0</a>. Teilt uns Eure Meinung mit! <p><a href="metafor-source.yaws" >Hinweise zur Implementierung</a> <p> Kleine Bemerkung: Die externen Seiten werden prinzipiell bei jedem Aufruf neu geladen, höchstens aber alle drei Minuten. <hr> <erl> -record(forum, {short, long, url, pic, hpurl, fid}). -record(article, {author, subject, date, url}). -record(thread, {first, last, url, nanswers, tid, vid}). -define(sel_cookie, "metaforlist"). -define(default_selection, ["drslv", "drslm", "LA25", "LA23", "LA19", "BM21", "BM3", "Tip7","Tip8", "LT67211", "LT67219"]). get_selection(A) -> case (A#arg.req)#http_request.method of 'POST' -> get_sel_from_post(A); _ -> {[], get_sel_from_cookie(A)} end. get_sel_from_post(A) -> {Save, List} = lists:foldl(fun acc_list/2, {false, []}, yaws_api:parse_post(A)), {case Save of true -> yaws_api:setcookie(?sel_cookie, encode_sel_cookie(List), "/", "Fri, 01-Jan-2010 00:00:00 GMT"); false -> [] end, List}. acc_list({"check", X}, {B, L}) -> {B, [X|L]}; acc_list({"save", _}, {_, L}) -> {true, L}; acc_list(_, X) -> X. get_sel_from_cookie(A) -> case yaws_api:find_cookie_val(?sel_cookie, (A#arg.headers)#headers.cookie) of [$:|C] -> case lists:member($\s, C) of true -> % old cookie ?default_selection; false -> decode_sel_cookie(C) end; _ -> ?default_selection end. decode_sel_cookie(C) -> decode_sel_cookie([], [], C). decode_sel_cookie(X, Xs, []) -> [lists:reverse(X)|Xs]; decode_sel_cookie(X, Xs, [$:|Cs]) -> decode_sel_cookie([], [lists:reverse(X)|Xs], Cs); decode_sel_cookie(X, Xs, [C|Cs]) -> decode_sel_cookie([C|X], Xs, Cs). encode_sel_cookie([]) -> []; encode_sel_cookie([H|T]) -> [$:|H] ++ encode_sel_cookie(T). out(A) -> []; % No more MetaFor out(A=#arg{headers=#headers{user_agent="Convera"++_}}) -> [{status, 403}, {content, "text/plain", "You are stupid. Go away."}, break]; out(A) -> {Out, Selection} = get_selection(A), [{header, {cache_control, "max-age=180"}}, {header, {"Vary", "Cookie"}}, Out, main(A, Selection)]. main(A, Selection) -> Data = metafor:get_threads(Selection), success(A, Data, Selection). error() -> {ehtml, [{p}, "Leider ist ein Fehler aufgetreten", {hr}]}. na() -> {em, [], "n/a"}. f_string(undefined) -> na(); f_string(S) -> {pre_html, S}. f_integer(undefined) -> na(); f_integer(I) -> integer_to_list(I). f_date(undefined) -> na(); f_date({{Y,M,D},{H,Min,_S}}) -> [integer_to_list(D),$.,integer_to_list(M),$.,two_d(Y),$ , integer_to_list(H),$:,two_d(Min)]. two_d(N) -> D0 = N rem 10, D1 = (N div 10) rem 10, [$0 + D1, $0 + D0]. f_url(undefined, _) -> na(); f_url(URL, Text) -> foreign_a(URL, Text). base_name(Cs) -> base_name(Cs, []). base_name([], A) -> lists:reverse(A); base_name([$.|T], A) -> lists:reverse(A); base_name([C|T], A) -> base_name(T, [C|A]). format_thread(N, {T,F}) -> First = T#thread.first, Last = T#thread.last, DoLastSubject = case Last of #article{url=LU,subject=LS} -> (LU =/= undefined) or (LS =/= undefined); undefined -> false end, ThreadURL = case T#thread.url of undefined -> First#article.url; TURL -> TURL end, LastArt = case Last of A=#article{} -> A; undefined -> T#thread.first end, S = case DoLastSubject of false -> fun (X) -> X end; true -> fun(X) -> [{rowspan,2}|X] end end, RowClass = row_class(N), [{tr, [RowClass], [{td, S([]), case F#forum.pic of undefined -> []; Pic -> foreign_a(case F#forum.hpurl of undefined -> F#forum.url; URL -> URL end, {img, [{src,["icons/",Pic]}, {alt, base_name(Pic)}, {width, 28}, {height,28}]}) end}, {td, S([]), foreign_a(F#forum.url, f_string(F#forum.short))}, {td, S([]), [f_date(LastArt#article.date),{br},f_string(LastArt#article.author)]}, {td, [], foreign_a(case First#article.url of undefined -> T#thread.url; U -> U end, F#forum.url, f_string(First#article.subject))}, case Last of undefined -> {td, S([{align, center}]), na()}; _ -> {td, S([]), [f_date(First#article.date), {br}, f_string(First#article.author)]} end, {td, S([]), foreign_a(T#thread.url, f_integer(T#thread.nanswers))}, {td, S([]), case T#thread.vid of undefined -> {a, [{href, ["http://www.lg-w.de/events" "/put_vid.php?" %% "tid=", %% yaws_api:url_encode(F#forum.fid), $:, %% yaws_api:url_encode(T#thread.tid), %% "&" "tname=", yaws_api:url_encode(First#article.subject), "&url=", yaws_api:url_encode( olzo_url(ThreadURL))]}], {img, [{src, "/icons/pen.gif"}, {alt, "neu"}, {title, "einer Veranstaltung zuordnen"}, {width, 18}, {height,15}] }}; VID -> {a, [{href, ["http://www.lg-w.de/events" "/events.php?vid=", integer_to_list(VID)]}], {img, [{src, "http://laufen-in-wuppertal.de/pix/star.gif"}, {alt, VID}, {title, "zur Veranstaltung"}, {width, 15}, {height,15}]}} end} ]}, case DoLastSubject of false -> []; true -> [{tr, [RowClass], {td, [], foreign_a(Last#article.url, F#forum.url, f_string(Last#article.subject))}}] end ]. olzo_url("news:"++MID) -> "http://www.drsl.de/?mid=" ++ MID; olzo_url(URL) -> URL. foreign_a(undefined, _, T) -> T; foreign_a("news:"++MID, "news:"++Group, T) -> foreign_a(msg_url(MID, Group), T); foreign_a(URL, _, T) -> foreign_a(URL, T). foreign_a(undefined, T) -> T; foreign_a(URL, T) -> {a, [{target, "_top"}, %{target, "mf-ext"}, %{onclick, "openext();return true;"}, {href, mangleURL(URL)}], T}. mangleURL("news:"++X) -> case lists:member($@, X) of true -> news_msg_url(X); false -> news_group_url(X) end; mangleURL(URL) -> yaws_api:htmlize(URL). news_msg_url(MID) -> ["http://drsl.de/?mid=", yaws_api:url_encode(MID)]. %news_group_url(Name) -> % ["http://groups-beta.google.com/group/", yaws_api:htmlize(Name)]. news_group_url(Name) -> ["http://www.newsoffice.de/groups/index.php?action=thread&group=", yaws_api:htmlize(Name)]. msg_url(MID, Group) -> ["http://www.newsoffice.de/groups/index.php?action=article&id=", %% "%3C", yaws_api:url_encode(MID), "%3E", http_base_64:encode("<"++MID++">"), "&group=", Group]. recent_date(#thread{first=F, last=L}) -> case a_date(L) of undefined -> a_date(F); D -> D end. a_date(undefined) -> undefined; a_date(#article{date=D}) -> D. row_class(N) -> {class, if N rem 2 == 0 -> "even"; true -> "odd" end}. format_forum(N, Selection, {#forum{long=Name,url=URL,short=Abbrev,fid=FID},TS}) -> InSelection = lists:member(FID, Selection), {tr, [row_class(N)], [{td, [], foreign_a(URL, Name)}, {td, [], Abbrev}, {td, [{align, "right"}], case InSelection of true -> integer_to_list(length(TS)); false -> na() end}, {td, [], {input, case InSelection of true -> [checked]; false -> [] end ++ [{type, "checkbox"}, {name,"check"}, {value, FID}]} } ]}. success(A, Data, Selection) -> Data0 = [{T,F} || {F,TS} <- Data, T <- TS ], {ehtml, [{p}, {a, [{href, "#forums"}], "Zur Forenübersicht und den Einstellungen."}, threads(A, Data0), forums(A, Data, Selection)] }. forums(A, Data, Selection) -> [{h2, [], {a, [{name, "forums"}], "Foren"}}, {p}, {form, [{action, A#arg.server_path}, {method, "post"}], [{table, [{border,1}], [{tr, [], [{th, [], "Name"}, {th, [], "Kürzel"}, {th, [], "Threads"}, {th, [], "Aktiv"}]} | numbered_map(fun(N, D) -> format_forum(N, Selection, D) end, Data)]}, {input, [{type, "submit"}, {name, "change"}, {value, "Auswahl ändern"}]}, " und in Cookie speichern ", {input, [checked, {type, "checkbox"}, {name, "save"}, {value, "ok"}]} ] }]. take_while(F, []) -> []; take_while(F, [H|T]) -> case F(H) of true -> [H|take_while(F,T)]; _ -> [] end. numbered_map(F, Rs) -> numbered_map(F, 0, Rs, []). numbered_map(F, N, [], A) -> lists:reverse(A); numbered_map(F, N, [R|T], A) -> numbered_map(F, N+1, T, [F(N, R)|A]). %% could be improved remove_dups([]) -> []; remove_dups([X|T]) -> remove_dups(X, T, []). remove_dups(X, [], A) -> lists:reverse([X|A]); remove_dups(X={#thread{last=undefined,first=#article{url=ID}},_}, [Y={#thread{last=undefined,first=#article{url=ID}},_}|T], A) -> remove_dups(X, T, A); remove_dups(X, [Y|T], A) -> remove_dups(Y, T, [X|A]). from_date(A) -> case queryvar(A, "from_date") of {ok, Date} -> case io_lib:fread("{{~d,~d,~d},{~d,~d,~d}}", Date) of {ok, [Y,M,D,H,Min,S], []} -> {{Y,M,D},{H,Min,S}}; _ -> undefined end; _ -> undefined end. threads(A, Data0) -> TLimit = 50, Data1 = remove_dups( lists:sort(fun({X,_},{Y,_}) -> recent_date(X)>recent_date(Y) end, Data0)), L = length(Data1), {Threads, StripComment} = case from_date(A) of undefined -> case L > TLimit of true -> {lists:sublist(Data1, TLimit), [{p},"Die ",integer_to_list(TLimit), " aktuellsten folgen."]}; false -> {Data1, []} end; FromDate -> {take_while(fun({X,_}) -> recent_date(X) >= FromDate end, Data1), [{p}, "Artikel ab ", f_date(FromDate), " folgen."]} end, [{h2, [], {a, [{name, "threads"}], "Diskussionen"}}, {p}, {a, [{href, [A#arg.server_path, "?from_date=", yaws_api:url_encode(lists:flatten( f("~w",[calendar:local_time()]))) ]} %%,{rel, "nofollow"} ], "Neue Artikel holen."}, {p}, f("~p Threads wurden gefunden und nach Aktualität sortiert.", [L]), StripComment, {table, [{border,1}], [{tr, [], [{th, [{colspan,2}], "Forum"}, {th, [], "Letzter Beitrag"}, {th, [], "Thema"}, {th, [], "Erster Beitrag"}, {th, [], "Antw."}, {th, [], "VID"}]} | numbered_map(fun format_thread/2, Threads)]}]. </erl> <p id="seitenfuss">Diese Seite ist Teil eines Framesets. <br>Startseite: <a href="http://www.drsl.de/metafor/" target="_top">http://www.drsl.de/metafor/</a></p> </body> </html>
-module(metafor). -export([get_threads/1, main_loop/1, start/1]). -define(cache_timeout, 180000). -record(forum, {short, long, url, pic, hpurl, fid}). start(_SC) -> Server=proc_lib:spawn_link(fun()->main_loop([]) end), register(metafor_server, Server). main_loop(Data) -> receive {PID, metafor_req, Forums} -> Data1 = update_data(Data, Forums), Ans = prepare_answer(Data1, Forums), PID ! {metafor_ans, Ans}, ?MODULE:main_loop(Data1); _ -> ?MODULE:main_loop(Data) end. get_threads(Forums) -> metafor_server ! {self(), metafor_req, Forums}, receive {metafor_ans, Ans} -> Ans end. %% Cache format: %% [{Forum, {TimeStamp, Data} | undefined}}] get_q(_FID, _, []) -> true; get_q(FID, Now, [{#forum{fid=FID}, FData}|_]) -> case FData of undefined -> true; {TS, _} -> timer:now_diff(Now, TS) > ?cache_timeout * 1000 end; get_q(FID, Now, [_|T]) -> get_q(FID, Now, T). merge_with_cache(Xs, Cache, Now) -> lists:map(fun({F,[]}) -> mwc(F#forum.fid, F, Cache); ({F,D}) -> {F, {Now, D}} end, Xs). mwc(FID, F, [{#forum{fid=FID}, DC}|_]) -> {F, DC}; mwc(FID, F, [_|T]) -> mwc(FID, F, T); mwc(_FID, F, []) -> {F, undefined}. update_data(Data, Forums) -> Now=now(), ForumsToGet = lists:filter( fun(F) -> get_q(F, Now, Data) end, Forums), %%io:format("ForumsToGet:~n~p~n", [ForumsToGet]), if (ForumsToGet == []) and (Data /= []) -> %%io:format("serving completely from cache~n", []), Data; true -> %%io:format("opening port~n", []), Port = open_port({spawn, "/home/carsten/cgi/metafor1 +RTS -H10m -M20m"}, [{cd, "/tmp"}, exit_status, {packet, 4}, binary]), port_command(Port, term_to_binary(ForumsToGet)), %%io:format("data sent~n", []), receive {Port, {data, Bin}} when binary(Bin) -> %%io:format("data received~n", []), Now1 = now(), merge_with_cache(binary_to_term(Bin), Data, Now1); {Port, _} -> Data after 30000 -> Data end end. prepare_answer(Data, Forums) -> lists:map(fun({F, undefined}) -> {F, []}; ({F, {_TS, D}}) -> case lists:member(F#forum.fid, Forums) of true -> {F, D}; false -> {F, []} end end, Data).