Changeset 842 for trunk

Show
Ignore:
Timestamp:
12/16/11 02:37:52 (5 months ago)
Author:
FeepingCreature
Message:

Stuff

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/idc/dice.d

    r723 r842  
    289289    res = stuple(res, times) /apply/ (typeof(res) dg, typeof(times) times) { 
    290290      auto tv = times(); 
     291      if (tv.sum > 128) throw new Exception("Cannot roll more than 128 dice. "); 
    291292      Result r;  
    292293      // r.text = Format(tv.info, "x ["); 
  • trunk/idc/dsss.conf

    r648 r842  
    55# [dice.d] 
    66# target=dice 
     7[pad/cgi.d] 
     8target=cgi 
     9buildflags=-g -L-lbz2 -J. 
     10[pad/console.d] 
     11target=console 
     12buildflags=-g -L-lbz2 -J. 
  • trunk/idc/idc.d

    r723 r842  
    11module idc; // Internet delay chat! Yay! 
    22 
    3 import tools.fixed_socket, tools.base, std.string
     3import tools.fixed_socket, tools.base
    44import tools.log, tools.threads, tools.threadpool; 
    55import tools.time; 
    66 
    77import irc; 
    8 import std.process; 
    9 alias std.process.system system; 
    108 
    119import cah; 
     10 
     11extern(C) void exit(int); 
     12 
     13alias irc.tolower tolower; 
    1214 
    1315string characters(string s) { 
     
    1921 
    2022string clean(string s) { 
    21   return s.tolower().replace("'", "").replace("_", ""); 
     23  auto res = s.tolower().replace("'", "").replace("_", ""); 
     24  if (res == "127.0.0.1") res = "teentropers"; 
     25  return res; 
    2226} 
    2327 
     
    7680 
    7781import std.file; 
     82import std.process; 
     83alias std.process.system system; 
     84 
    7885void reload(Query query) { 
    7986  if (system("sh -c \"make >/dev/null 2> /tmp/idc_errors || exit 1\"") != 0) { 
     
    105112  if (site) q = "site:"~site~" "~q; 
    106113  auto hash = "http://www.google.com/uds/?file=search&v=1.0".download().between("JSHash = '", "'"); 
    107   auto url = "http://www.google.com/uds/GwebSearch?context=0&callback=google.search.WebSearch.RawCompletion&hl=en&v=1.0&q="~q.urlencode(); 
     114  auto url = "http://www.google.com/uds/GwebSearch?context=0&callback=GwebSearch.RawCompletion&hl=en&v=1.0&q="~q.urlencode(); 
    108115  // if (site) url ~= "&as_sitesearch="~site; 
    109   auto res = url.download().between("unescapedUrl\":\"", "\"").replace("\\u003d", "\u003d"); 
    110   if (!res.length) throw new Exception("No results! "); 
     116  auto sitedata = url.download(); 
     117  auto res = sitedata.between("unescapedUrl\":\"", "\"").replace("\\u003d", "\u003d"); 
     118  if (!res.length) 
     119    if (sitedata.length < 132) throw new Exception(Format("No results! '", sitedata, "'")); 
     120    else throw new Exception(Format("No results! ", sitedata.length)); 
    111121  // if (!res.length) throw new Exception("Cannot identify Google result for "~url); 
    112122  if (!res.startsWith("http://")) res = "http://"~res; 
     
    212222bool pwn_site(string url, ref string[string] urls, ref string src) { 
    213223  if (url.find("youtube.com") != -1) { 
    214     if (!src) src = url.download(); 
    215     // Youtube is in the sucking-of-ass business. This won't work. 
    216     /*if (src.find(`id="verify-age"`) != -1) { 
    217       url = "http://www.youtube.com/verify_age?action_confirm=Confirm Birth Date&next_url="~url.between("youtube.com", "").urlencode(); 
    218       src = url.download(); 
    219     }*/ 
    220     urls["Sorry"] = "Youtube downloading appears broken by a recent update. "; 
    221     return false; 
    222     auto 
    223       hd = src.between("isHDAvailable = ", ";") == "true", 
    224       t = src.between(`"t": "`, `"`), id = src.between(`"video_id": "`, `"`); 
    225     auto hqurl = url.followLink("/get_video?fmt=22&video_id="~id~"&t="~t); 
    226     auto mp4url = url.followLink("/get_video?fmt=18&video_id="~id~"&t="~t); 
    227     if (hd) urls["hd"] = hqurl.mktiny(); 
    228     urls["mp4"] = mp4url.mktiny(); 
     224    string[int] fmt_map; 
     225    bool[int] checked; 
     226    void check(string url, int fmt = 0) { 
     227      if (fmt) { 
     228        if (fmt in checked) return; 
     229        checked[fmt] = true; 
     230        url ~= Format("&fmt=", fmt); 
     231      } 
     232      auto src = url.download(); 
     233      auto swfhtml = src.between("swfHTML", "</script"); 
     234      auto str = swfhtml.between("? \"", "noembed>\";").replace("\\\"", "\"").unescape(); 
     235      auto videos = str.between("fmt_url_map=", "&csi_page_type=").split(",") /map/ (string s) { return stuple(s.slice("|").atoi(), s); }; 
     236      bool matched; 
     237      foreach (video; videos) { 
     238        if (video._0 == fmt) 
     239          matched = true; 
     240        fmt_map[video._0] = video._1; 
     241        checked[video._0] = true; 
     242      } 
     243    } 
     244     
     245    check(url); 
     246    foreach (type; [5, 17, 18, 22, 34, 35, 37, 43, 45]) { 
     247      if (type in checked) continue; 
     248      check(url, type); 
     249    } 
     250     
     251    string[int] id_map = [ 
     252      5: "flv/h.263 240p"[], 34: "flv/h.264 360p", 35: "flv/h.264 480p", 22: "mp4 720p", 37: "mp4 1080p", 18: "mp4 480x360", 
     253      43: "WebM 480p", 45: "WebM 720p", 17: "3gp 176x144", 
     254      0: "flv/h.263 320x240", 6: "flv/h.263 480x360", 13: "3gp 176x144" 
     255    ]; 
     256    foreach (key, value; fmt_map) { 
     257      if (key in id_map) 
     258        urls[id_map[key]] = value.mktiny(); 
     259      else 
     260        urls[Format(key)] = value.mktiny(); 
     261    } 
    229262    return true; 
    230263  } 
     
    254287  } 
    255288  void do_megavideo(string hit) { 
    256     if (hit.find("/v/") != -1) { hit = hit.replace("/v/", "/?v="); } 
    257     if (hit.find("megavideo.com/?v=") == -1) { 
    258       string redir; 
    259       hit.download_first(&redir); 
    260       if (redir) hit = redir ~ "&"; // hackaround: make .between(foo, "&") work 
    261     } else hit ~= "&"; 
     289    string redir; 
     290    hit.download_first(&redir); 
     291    if (redir) hit = redir ~ "&"; // hackaround: make .between(foo, "&") work 
     292    else hit ~= "&"; 
    262293    auto link = hit.between("v=", "&"); 
    263294    if (link.find("\"") != -1) link = link.between("", "\""); 
     
    304335} 
    305336 
    306 extern(C) { 
    307   int pipe(int* fd); 
    308   int close(int); 
    309   FILE *fdopen(int fd, char* mode); 
    310 
    311  
    312 string readStream(InputStream IS) { 
    313   string res; 
    314   ubyte[512] buffer; 
    315   int i; 
    316   do { 
    317     i = IS.read(buffer); 
    318     if (i < 0) throw new Exception(Format("Read error: ", i)); 
    319     res ~= cast(string) buffer[0 .. i]; 
    320   } while (i); 
    321   return res; 
    322 
    323  
    324 string readback(string cmd) { 
    325   int[2] fd; // read end, write end 
    326   if (-1 == pipe(fd.ptr)) throw new Exception(Format("Can't open pipe! ")); 
    327   scope(exit) close(fd[0]); 
    328   auto cmdstr = Format(cmd, " >&", fd[1], " &"); 
    329   // throw new Exception("test: "~cmdstr); 
    330   system(cmdstr); 
    331   close(fd[1]); 
    332   scope fs = new CFile(fdopen(fd[0], "r"), FileMode.In); 
    333   return readStream(fs); 
    334 
     337import readback; 
    335338 
    336339void gun(Query query) { 
    337   auto gs = readback("echo $(fwoosh/fwoosh)"); 
     340  auto gs = readBack("echo $(fwoosh/fwoosh)"); 
    338341  query.answer(gs); 
    339342} 
    340343 
    341344void fwoosh_upd(Query query) { 
    342   query.answer(readback("cd fwoosh; ./scriptfile.sbcl |wc -l")); 
     345  query.answer(readBack("cd fwoosh; ./scriptfile.sbcl |wc -l")); 
    343346} 
    344347 
    345348import std.utf; 
    346349string htmlFormat(string s) { 
    347   wstring res; 
     350  dstring res; 
    348351  s.glomp_parse([ 
    349     "<sup>"[]: (string pre, ref string post) { res ~= pre.toUTF16(); }, 
     352    "<sup"[]: (string pre, ref string post) { res ~= pre.toUTF32(); post.slice(">"); }, 
    350353    "</sup>": (string pre, ref string post) { 
    351354      bool onlyAsciiHighables = true, onlyUnicodeHighables = true; 
    352355      foreach (ch; pre) { 
    353356        if (ch < '1' || ch > '3') onlyAsciiHighables = false; 
    354         if (ch /notin/ Range['0' .. '9'].endIncl && ch != '-' && ch != '+' && ch != ' ') onlyUnicodeHighables = false; 
     357        if (ch /notin/ Range['0' .. '9'].endIncl && " +-=[]()Nn".find(ch) == -1) onlyUnicodeHighables = false; 
    355358      } 
    356359      if (onlyAsciiHighables) { 
     
    360363            case '2': res ~= '²'; break; 
    361364            case '3': res ~= '³'; break; 
    362             case ' ': res ~= ' '; break; 
    363365            default: assert(false); 
    364366          } 
     
    375377            case '+': res ~= '⁺'; break; 
    376378            case '-': res ~= '⁻'; break; 
     379            case '=': res ~= '⁌'; break; 
     380            case '[', '(': res ~= '⁜'; break; 
     381            case ']', ')': res ~= ' '; break; 
     382            case 'N', 'n': res ~= 'ⁿ'; break; 
    377383            default: assert(false); 
    378384          } 
    379385        } 
    380       } else res ~= ("^"~pre).toUTF16(); 
     386      } else res ~= ("^"~pre~"^").toUTF32(); 
     387    }, 
     388    "<sub": (string pre, ref string post) { res ~= pre.toUTF32(); post.slice(">"); }, 
     389    "</sub>": (string pre, ref string post) { 
     390      bool onlyUnicodeLowables = true; 
     391      foreach (ch; pre) { 
     392        if (ch /notin/ Range['0' .. '9'].endIncl && " +-=[]()AaEeOoXx".find(ch) == -1) onlyUnicodeLowables = false; 
     393      } 
     394      if (onlyUnicodeLowables) { 
     395        foreach (ch; pre) { 
     396          if (ch in Range['0' .. '9'].endIncl) { res ~= "₀₁₂₃₄₠
     397₆₇₈₉"w[ch - '0']; continue; } 
     398          switch (ch) { 
     399            case ' ': res ~= ' '; break; 
     400            case '+': res ~= '₊'; break; 
     401            case '-': res ~= '₋'; break; 
     402            case '=': res ~= '₌'; break; 
     403            case '[', '(': res ~= '₍'; break; 
     404            case ']', ')': res ~= '₎'; break; 
     405            case 'A', 'a': res ~= 'ₐ'; break; 
     406            case 'E', 'e': res ~= 'ₑ'; break; 
     407            case 'O', 'o': res ~= 'ₒ'; break; 
     408            case 'X', 'x': res ~= 'ₓ'; break; 
     409            default: assert(false); 
     410          } 
     411        } 
     412      } else res ~= ("_"~pre~"_").toUTF32(); 
    381413    }, 
    382414    "<font": (string pre, ref string post) { 
    383       res ~= pre.toUTF16(); 
    384       post = post[post.find(">")+1 .. $]
     415      res ~= pre.toUTF32(); 
     416      post.slice(">")
    385417    }, 
    386     "</font>": (string pre, ref string post) { res ~= pre.toUTF16(); } 
    387   ], (string rest) { res ~= rest.toUTF16(); }); 
    388   return res.toUTF8().replace("&#215;", "×"); 
     418    "</font>": (string pre, ref string post) { res ~= pre.toUTF32(); }, 
     419    "<img": (string pre, ref string post) { 
     420      res ~= pre.toUTF32(); 
     421      post.slice(">"); 
     422    }, 
     423    "</img>": (string pre, ref string post) { } 
     424  ], (string rest) { res ~= rest.toUTF32(); }); 
     425  return res.toUTF8().replace("&#215;", "×").replace("&#160;", "  "); 
    389426} 
    390427 
     
    419456  string res; 
    420457  const string exclude = "-intitle:\"Discussion\" -inurl:editors.php -inurl:inboundcount.php -inurl:posts.php"; 
    421   try 
     458  try { 
     459    if (j.length) throw new Exception("mew"); // lal 
    422460    res = (exclude~" \"pmwiki.php/main/"~q~"\"").googleQuery("tvtropes.org"); 
    423   catch (Exception ex) try 
     461  } catch (Exception ex) try { 
     462    sleep(2); 
    424463    res = (exclude~" \""~q~"\"").googleQuery("tvtropes.org"); 
    425   catch (Exception ex) 
     464  } catch (Exception ex) { 
     465    sleep(2); 
    426466    res = (exclude~" "~q).googleQuery("tvtropes.org"); 
     467  } 
    427468  auto alt = res.between("", "?"); 
    428469  query.answer(alt?alt:res); 
     
    701742  } 
    702743  unref; 
    703   res = res /qsort/ ex!("fn -> a, b -> fn(a.count, b.count)")(&cmp!(int)); 
     744  res = res /qsort/ ex!("a, b -> a.count < b.count"); 
    704745  if (!res[$-1].count) { 
    705746    return query.say("Time's up and nobody voted! Sorry. Better luck next time!"); 
     
    941982  (ref int[string] stats, ref string[string] namestats, string user, string host) { 
    942983    string country; 
     984    if (host.endsWith(".hmsk")) return;; 
    943985    auto parts = host.split("."); 
    944986    while (parts.length > 1) { 
     
    948990        country = GeoIP.lookup(ip); 
    949991        break; 
    950       } catch (Exception ex) { parts = parts[1 .. $]; } 
     992      } catch (Exception ex) { logln("fail: ", ex); parts = parts[1 .. $]; } 
    951993    } 
    952994    if (!country) return; 
     
    9601002    } 
    9611003  }, stuple(stats, namestats, query) /apply/ (ref int[string] stats, ref string[string] namestats, Query query) { 
     1004    if (!stats.length) return query.answer("No country stats available. This is probably due to hostmasking. "); 
    9621005    auto rstats = invert(stats), top = rstats.keys.sort; 
    9631006    string[][int] res; 
     
    10101053 
    10111054import std.stream; 
    1012 File[string] bridget
     1055File[string] readers, writers
    10131056 
    10141057void bridge(Query query) { 
     
    10221065    // query.say("Setting up read pipe on |", params[2], "|"); 
    10231066    bridge_read = new File(params[2], FileMode.In); 
     1067    readers[ch] = bridge_read; 
    10241068  } 
    10251069  void setupWrite() { 
    10261070    // query.say("Setting up write pipe on |", params[3], "|"); 
    1027     bridget[ch] = new File(params[3], FileMode.Out); 
     1071    writers[ch] = new File(params[3], FileMode.Out); 
    10281072  } 
    10291073  if (order == "first") { 
     
    10411085} 
    10421086 
     1087void listen(Query query) { 
     1088  if (query.name != root_user) return query.answer("You are not authorized to set up a listen pipe! "); 
     1089  auto file = new File(query.param, FileMode.In); 
     1090  readers["<main>"] = file; 
     1091  while (true) { 
     1092    auto cmd = file.readLine(); 
     1093    query.connection.raw_sendln(IRCFormat(cmd)); 
     1094  } 
     1095} 
     1096 
    10431097void wbOnJoin(Query query) { 
    10441098//   sleep(1); 
     
    10471101//   q2.notice("No it hasn't. STFU GreetBot you tard. "); 
    10481102  // query.answer(Format("wbOnJoin(", query, ")")); 
     1103  if (query.channel == "#yackfest") return; 
    10491104  auto ch = query.channel.replace("#", ""), name = cast(string) query.name; 
    10501105  if (!wbfile.has(ch.clean(), name)) return; 
     
    10641119    // else q.say(": not in file"); 
    10651120     
    1066     notes ~= notefile.get!(Stuple!(string, typeof(µsec()), string)[])("global", name, null); 
     1121    auto privnotes = notefile.get!(Stuple!(string, typeof(µsec()), string)[])("global", name, null); 
    10671122    if (notefile.has("global", name)) notefile.del("global", name); 
    10681123     
    1069     foreach (note; notes) q.answer(note._2.replace("$WHEN", timediff(µsec() - note._1))); 
    1070   } 
     1124    string countercheck(string msg) { 
     1125      if (auto rest = msg.startsWith("CHECKED ")) { 
     1126        msg = rest; 
     1127        auto name = msg.between("", " left"); 
     1128        auto notes = notefile.get!(Stuple!(string, typeof(µsec()), string)[])("global", name, null); 
     1129        notes ~= stuple(cast(string) name.tolower(), µsec(), Format("Your note to ", .nick(host), " was delivered $WHEN. ")); 
     1130        notefile.set("global", name, notes); 
     1131      } 
     1132      return msg; 
     1133    } 
     1134     
     1135    foreach (note; notes) { 
     1136      q.answer(countercheck(note._2).replace("$WHEN", timediff(µsec() - note._1))); 
     1137    } 
     1138    foreach (note; privnotes) { 
     1139      q.notice(countercheck(note._2).replace("$WHEN", timediff(µsec() - note._1))); 
     1140    } 
     1141  } 
     1142
     1143 
     1144void onNickChange(IRCconn ic, string channel, hostmask from, hostmask to) { 
     1145  seenfile.set(channel.clean(), (cast(string) .nick(from)).tolower(), stuple("\x00NICK "~cast(string) .nick(to), µsec())); 
    10711146} 
    10721147 
    10731148void dbg(Query q) { 
    1074   q.notice("Users: ", q.connection.users); 
     1149  q.notice("This does nothing currently. "); 
     1150  return; 
     1151  // q.notice("Users: ", q.connection.users); 
    10751152} 
    10761153 
    10771154void addnote(Query q) { 
    1078   bool global
     1155  bool global, checked
    10791156  if (auto rest = q.param.startsWith("global ")) { 
    10801157    q.param = rest; 
    10811158    global = true; 
    10821159  } 
    1083   if (!q.param.length) return q.answer("Usage: addnote <target name> <message>"); 
     1160  if (auto rest = q.param.startsWith("/checked ")) { 
     1161    q.param = rest; 
     1162    checked = true; 
     1163  } 
     1164  if (!q.channel.startsWith("#")) global = true; // privmsg notes are always global 
     1165  if (!q.param.length) return q.answer("Usage: note <target name> <message>"); 
    10841166  // if (!q.connection.registered(q.name)) { 
    10851167  //   return q.answer("You have to be registered to do that! "); 
     
    10991181    foreach (note; notes) 
    11001182      if (note._0 == cast(string) q.name.tolower()) count++; 
    1101     if (count >= 3) return q.answer("Cannot leave more than three messages for somebody! "); 
    1102     notes ~= stuple(cast(string) q.name.tolower(), µsec(), Format(q.name, " left a note $WHEN: ", q.param)); 
     1183    if (a == "global") { 
     1184      if (count >= 12) return q.answer("Cannot leave more than twelve global messages for somebody! "); 
     1185    } else { 
     1186      if (count >= 3 && a != "#teentropers" && a != "#127.0.0.1"&& q.name != root_user) 
     1187        return q.answer("Cannot leave more than three messages for somebody! "); 
     1188    } 
     1189    notes ~= stuple(cast(string) q.name.tolower(), µsec(), Format(checked?"CHECKED ":"", q.name, " left a note $WHEN: ", q.param)); 
    11031190    notefile.set(a, b, notes); 
    11041191    return q.notice("Note saved! "); 
     
    11221209  } 
    11231210  auto ch = query.channel.replace("#", ""); 
    1124   auto nick = query.param.slice(" ")
     1211  auto nick = query.param.slice(" ").strip()
    11251212  if (nick.startsWith("<")) { 
    11261213    query.param = nick ~ " " ~ query.param; // part of message 
    11271214    nick = nick[1 .. $]; 
    1128     nick = nick.slice(">"); 
    1129     while (nick.length && "%@+-*".find(nick[0]) != -1) nick = nick[1 .. $]; 
    1130     if (!nick.length) return query.answer("Invalid nickname! "); 
    1131   } 
     1215    nick = nick.slice(">").strip(); 
     1216  } 
     1217  if (nick.startsWith("(")) { 
     1218    nick = nick[1 .. $]; 
     1219    nick = nick.slice(")").strip(); 
     1220    query.param = "<" ~ nick ~ "> " ~ query.param; 
     1221  } 
     1222  if (nick.startsWith("*")) { 
     1223    query.param = nick ~ " " ~ query.param; // dito 
     1224    nick = nick[nick.find("*")+1 .. $]; 
     1225  } 
     1226  while (nick.length && "%@+-*".find(nick[0]) != -1) nick = nick[1 .. $]; 
     1227  if (!nick.length) return query.answer("Invalid nickname: "~nick~"!"); 
    11321228  if (cast(nickname) nick == query.name && query.name != root_user) return query.answer("Please don't selfquote! "); 
    11331229  last_added[ch.clean() ~ "\x00" ~ cast(string) query.name] = nick; 
     
    12231319  } 
    12241320  auto text = std.string.join(parts, "\n  ==========\n"); 
     1321  if (text.length > 64000) { 
     1322    query.answer("Hosting locally. "); 
     1323    auto fn = "quotes_"~ch~".txt"; 
     1324    auto localfn = "/home/mathis/public_html/"~fn; 
     1325    localfn.write(text); 
     1326    system("chmod a+r "~localfn); 
     1327    query.answer("http://demented.no-ip.org/~feep/"~fn); 
     1328    return; 
     1329  } 
     1330  query.answer("Uploading ", text.length, "b .. "); 
    12251331  return query.answer(pastepost(text, query.connection.nick, "d")); 
    12261332} 
     
    12391345  temp[] flat; 
    12401346  foreach (key, value; answer) flat ~= temp(value, key); 
    1241   flat = flat.qsortfn(ex!("fn -> a, b -> fn(a.key, b.key)")(&cmp!(int))); 
     1347  flat = flat.qsortfn(ex!("a, b -> a.key < b.key")); 
    12421348  query.answer("WB stats: ", std.string.join(flat /map/ ex!("a -> a.val"), "; ")); 
    12431349} 
     
    12481354struct PixivSession { 
    12491355  string session_id; 
    1250   const User = "qqqwwwqqq", Pass = "qqqwwwqqq"; 
     1356  const User = "guest1234", Pass = "bugmenot"; 
    12511357  string relogin() { 
    12521358    logln("Acquiring new session id"); 
     
    12991405string getTitle(string url, string src = null) { 
    13001406  try return _getTitle(url, src); 
    1301   catch (Exception ex) return Format("[", ex, "]"); 
     1407  catch (Exception ex) { 
     1408    auto res = Format("[", ex, "]"); 
     1409    if (res.startsWith ("[SSL/TLS is not")) return null; 
     1410    return res; 
     1411  } 
    13021412} 
    13031413 
     
    13341444  if (!src) src = url.download_first(); 
    13351445  if (!src) src = url.download(); 
    1336   if (src.startsWith("module")) return "D code. "
     1446  if (src.startsWith("module")) return null
    13371447   
    13381448  // return first paragraph 
     
    13401450     
    13411451    string para = src; 
    1342     if (string box = para.between("<table", "infobox", "</table")) 
     1452    if (string box = para.between("<table", "infobox", "</table>")) 
    13431453      para = para.after(box); 
     1454    if (string nav = para.between("<table", "navbox", "</table>")) 
     1455      para = para.after(nav); 
    13441456    para = para.between("<p>", "</p>"); 
     1457     
     1458    para = htmlFormat(para); 
    13451459     
    13461460    void rmTag(string tag) { 
    13471461      while (true) { 
    1348         auto betw = para.between_incl("<"~tag, ">"); 
     1462        auto betw = para.between_incl("<"~tag~" ", ">"); 
    13491463        if (!betw) break; 
    13501464        para = para.remove(betw); 
    13511465      } 
     1466      para = para.replace("<"~tag~">", ""); 
    13521467      para = para.replace("</"~tag~">", ""); 
    13531468    } 
    13541469    rmTag("a"); 
     1470    rmTag("abbr"); 
    13551471    rmTag("sup"); 
    13561472    rmTag("span"); 
    13571473    rmTag("small"); 
     1474    rmTag("big"); 
     1475    para = para.replace("<br />", "").replace("<br/>", ""); 
    13581476    bool rmDot; 
    13591477    auto Limit = 384 - url.length - 32; // safety margin, magic number! yay. 
     
    13991517  if (pwn_site(url, extras, src)) { 
    14001518    foreach (key, value; extras) 
    1401       extra ~= Format("[", key, " ", value, "]"); 
    1402   } 
    1403   if (url.ifind("escapistmagazine.com/videos/view/zero") != -1) { 
     1519      extra ~= Format("[", key, " ", value, " ]"); 
     1520  } 
     1521  if (url.ifind("escapistmagazine.com/videos/view/") != -1) { 
    14041522    src = src.replace("&quot;", "\""); 
    14051523    auto js_url = src.between("flashvars=\"config=", "\""); 
     
    14201538  } 
    14211539  mistake: // Wait, my mistake. Not a ZP video after all. 
     1540  if (title.length > 128) title = title[0..128] ~ "[snip]"; 
    14221541  auto res = title.replaceEntities().replace("\n", "").strip() ~ extra; 
    14231542  bool booru = 
     
    14541573 
    14551574void ip(Query query) { 
    1456   query.answer("I'm online from ", "whatismyip.com/automation/n09230945.asp".download()); 
     1575  query.answer("I'm online from ", "automation.whatismyip.com/n09230945.asp".download()); 
    14571576} 
    14581577 
    14591578void control_char(Query query) { 
     1579  if (query.name != root_user) return query.answer("You are not authorized to do that."); 
    14601580  setCtrl(query.channel, query.param); 
    1461   if (query.name != root_user) return query.answer("You are not authorized to do that."); 
    14621581  query.answer("Control character is now `", CTRL(query.channel), "'."); 
    14631582} 
     
    14901609void seen(Query query) { 
    14911610  if (!query.param.length) return query.answer("Usage: "~CTRL(query.channel)~"seen <nick>"); 
    1492   if (!seenfile.has(query.channel.clean(), query.param.tolower())) return query.answer("I have not seen "~query.param~" in this channel!"); 
    1493   auto entry = seenfile.get!(Stuple!(string, typeof(µsec())))(query.channel.clean(), query.param.tolower(), { fail; return stuple("", µsec()); }()); 
     1611  bool talking; 
     1612  if (auto rest = query.param.strip().startsWith("talking ")) { talking = true; query.param = rest; } 
     1613  if (auto time = query.param.strip().startsWith("within ")) { 
     1614    typeof(µsec()) limit; 
     1615    if (auto t = time.endsWith("d")) limit = t.atoi() * 3600L * 24L * 1_000_000L; 
     1616    else if (auto t = time.endsWith("h")) limit = t.atoi() * 3600L * 1_000_000L; 
     1617    else { 
     1618      query.answer("Unknown time format: ", time); 
     1619    } 
     1620    auto cur = µsec(); 
     1621    auto strlist = seenfile.section(query.channel.clean()); 
     1622    Stuple!(string, typeof(µsec()))[string] list; 
     1623    foreach (key, value; seenfile.section_map(query.channel.clean())) { 
     1624      try list[key] = deserialize!(Stuple!(string, typeof(µsec()))) (value); 
     1625      catch (Exception ex) { } 
     1626    } 
     1627    bool[string] names; 
     1628    bool similar(string a, string b) { 
     1629      if (a.length > b.length) swap(a, b); 
     1630      auto rest1 = b.startsWith(a), rest2 = b.endsWith(a); 
     1631      if (rest1 && "_|[".find(rest1[0]) != -1) return true; 
     1632      if (rest2 && "_|]".find(rest2[$-1]) != -1) return true; 
     1633      return false; 
     1634    } 
     1635    string shorter(string a, string b) { 
     1636      if (a.length < b.length) return a; 
     1637      return b; 
     1638    } 
     1639    outer:foreach (key, value; list) { 
     1640      auto dist = cur - value._1; 
     1641      if (dist < limit && (!talking || value._0 != "JOIN")) { 
     1642        auto name = key.dup; 
     1643        auto keys = names.keys.dup; // ouch 
     1644        foreach (key2; keys) { 
     1645          if (similar(key2, name)) { 
     1646            key2 = key2.dup; 
     1647            names.remove(key2); 
     1648            names[shorter(name, key2)] = true; 
     1649            continue outer; 
     1650          } 
     1651        } 
     1652        names[name] = true; 
     1653      } 
     1654    } 
     1655    query.notice("Seen within the last ", time, ": ", std.string.join(names.keys, ", ")); 
     1656    return; 
     1657  } 
     1658  Stuple!(string, typeof(µsec())) entry; 
     1659  string extra = " "; 
     1660  if (auto rest = query.param.strip().startsWith("global ")) { 
     1661    query.param = rest; 
     1662    auto ids = seenfile.sections(); 
     1663    typeof(µsec()) highest_time = 0; 
     1664    bool anyHit; 
     1665    foreach (id; ids) { 
     1666      bool hit = true; 
     1667      auto ent = seenfile.get!(Stuple!(string, typeof(µsec())))(id, query.param.tolower(), { hit = false; return stuple("", µsec()); }()); 
     1668      if (!hit) continue; 
     1669      anyHit = true; 
     1670      if (ent._1 > highest_time) { 
     1671        highest_time = ent._1; 
     1672        entry = ent; 
     1673        extra = format(" in ", id, " "); 
     1674      } 
     1675    } 
     1676    if (!anyHit) return query.answer("I have not seen anybody named \""~query.param~"\" anywhere at all! "); 
     1677  } else { 
     1678    if (!seenfile.has(query.channel.clean(), query.param.tolower())) return query.answer("I have not seen anybody named \""~query.param~"\" in this channel!"); 
     1679    entry = seenfile.get!(Stuple!(string, typeof(µsec())))(query.channel.clean(), query.param.tolower(), { fail; return stuple("", µsec()); }()); 
     1680  } 
    14941681  string mesg; 
    1495   if (auto rest = entry._0.startsWith("\x01ACTION ")) { 
     1682  if (auto rest = entry._0.startsWith("\x00NICK ")) { 
     1683    mesg = Format(" changing nick to ", rest); 
     1684  } else if (auto rest = entry._0.startsWith("\x01ACTION ")) { 
    14961685    rest = rest[0 .. $-1]; 
    14971686    mesg = Format(" doing \" * ", query.param, " ", rest, "\"."); 
    14981687  } else mesg = Format(" saying \"", entry._0, "\"."); 
    1499   query.answer("I have last seen ", query.param, " ", timediff(µsec() - entry._1), mesg); 
     1688  query.answer("I have last seen ", query.param, extra, timediff(µsec() - entry._1), mesg); 
     1689
     1690 
     1691void peak(Query query) { 
     1692  if (query.param.length) return query.answer("Usage: "~CTRL(query.channel)~"peak"); 
     1693  if (!IRCconfig.has(query.connection.host, "peak_"~query.channel.clean())) return query.answer("No data."); 
     1694  auto entry = IRCconfig.get!(Stuple!(int, typeof(µsec()), string)) (query.connection.host, "peak_"~query.channel.clean(), { fail; return stuple(0, µsec(), ""); }()); 
     1695  query.answer("User peak was ", timediff(µsec() - entry._1), " when ", entry._2, " brought the total to ", entry._0, "."); 
    15001696} 
    15011697 
     
    15301726      return; 
    15311727    } catch (Exception ex) { 
     1728      logln("Closing pipes. "); 
     1729      foreach (reader; readers) reader.close; 
     1730      foreach (writer; writers) writer.close; 
    15321731      logln(ex, "! Restarting in 10s, resetting resume. "); 
    15331732      foreach (i, arg; args) if (arg == "--resume") { args = args[0 .. i]; break; } 
     
    15631762  New(tgtp, Threadpool.GROW); 
    15641763  threadgens["ps"] = (nickname nick) { 
    1565     auto conduit = new MessageMultiChannel!(Query, true, false); 
     1764    auto conduit = new MessageMultiChannel!(Query, false, false); 
    15661765    tp.addTask(stuple(conduit, nick) /apply/ (typeof(conduit) conduit, nickname nick) { 
    1567       start: scope(exit) goto start; 
    15681766      Query last; 
    15691767      string getCmd() { last = conduit.get(); return last.param; } 
     
    15901788        int[6] dest = points; 
    15911789        foreach (part; parts) { 
     1790          part = part.strip(); 
    15921791          if (auto rest = part.startsWith("add " /or/ "put ")) { 
    15931792            auto name = rest.slice(" "), value = rest; 
     
    19812180} 
    19822181 
     2182void dcode(Query query) { 
     2183  auto code = query.param.strip(); 
     2184  if (!code.length) { query.answer("Usage: <D program>"); return; } 
     2185  auto response = ("POST=lang=D&code="~urlencode(code)~"&run=True&submit=Submit http://codepad.org/").download(); 
     2186  auto output = response.between("<span class=\"heading\">Output:", "</table>").between("</pre>", "</pre>").between("<pre>", "").strip().replace("\n", "\\"); 
     2187  if (output.length > 512) output = output[0 .. 512] ~ Format("[...] ", output.length - 512, " omitted. "); 
     2188  while (true) { 
     2189    auto pos1 = output.find("<a href"); 
     2190    if (pos1 != -1) { 
     2191      output = output[0 .. pos1] ~ output.between("<a href", "").between(">", ""); 
     2192      continue; 
     2193    } 
     2194    auto pos2 = output.find("</a>"); 
     2195    if (pos2 != -1) { 
     2196      output = output[0 .. pos2] ~ output.between("</a>", ""); 
     2197      continue; 
     2198    } 
     2199    break; 
     2200  } 
     2201  query.answer(output.replace("&quot;", "\"")); 
     2202} 
     2203 
     2204void magic(Query q) { 
     2205  if (!q.param.length) { 
     2206    return q.answer("Usage: magic <card name>"); 
     2207  } 
     2208  auto site = googleQuery("site:magiccards.info -inurl:query "~q.param).download(); 
     2209  auto name = site.between("<title>", "</title>").strip(); 
     2210  auto type = site 
     2211    .between("0 0 0.5", "").between("<p>", "</p>").replace("\n", ""); 
     2212  auto pricelink = site.between("href=\"", "magic.tcgplayer.com/db", "\""); 
     2213  string priceinfo; 
     2214  if (!pricelink) priceinfo = "No price info. "; 
     2215  else { 
     2216    auto pricesite = pricelink.download(); 
     2217    auto pricedata = pricesite.between("#D9FCD1", "</div").betweens("<B>", "</b>"); // lol 
     2218    assert(pricedata.length == 3); 
     2219    priceinfo = Format("L ", pricedata[2], ", M ", pricedata[1], ", H ", pricedata[0]); 
     2220  } 
     2221  return q.answer(name, ", ", type, ": ", priceinfo, " ", mktiny(pricelink)); 
     2222} 
     2223 
     2224import neatcode; 
     2225 
    19832226import vars, loli, maid, std.date: getUTCtime, dateToString = toString; 
    19842227// Organically grown. 
     
    20112254  anypos["trope"] = anypos["roll"] = true; 
    20122255  bool mask_rp; 
     2256  auto d_imports = "import std.stdio, std.file, std.stream, std.string, std.math; "; 
    20132257  foreach (key, value; [ 
    20142258    "roll"[]: &roll /todg, "dice": &roll /todg, "google": &google /todg, "gcalc": &gcalc /todg, 
    20152259    "anidb": (Query q) { return anidb(q, 0, "", false); }, "decide": &decide /todg, 
    2016     "vote": &vote /todg, "rr": &rr /todg, "rps": &rps /todg, 
     2260    /*"vote": &vote /todg, */"rr": &rr /todg, "rps": &rps /todg, 
    20172261    "more": &more /todg, "join": &join /todg, "part": &part /todg, 
    20182262    "help": &help, "cstats": &cstats /todg, "cfind": &cfind /todg, "vstats": &vstats /todg, 
     
    20302274    "note": &addnote /todg, "dbg": &dbg /todg, "overlap": &overlap /todg, 
    20312275    "fwoosh": &gun /todg, "fwoosh_up": &fwoosh_upd /todg, 
    2032     "markov": &markov /todg, 
     2276    "markov": &markov /todg, "peak": &peak /todg, 
     2277    "dsource": (Query q) { q.answer("http://downforeveryoneorjustme.com/dsource.org".getTitle()); }, 
     2278    "down": (Query q) { q.answer(("http://downforeveryoneorjustme.com/"~q.param).getTitle()); }, 
    20332279    "rp": (Query q) { 
    20342280      bool mask = true; 
     
    20382284      q.channel = "#fetishfuel"; q.answer(mask?"#ffsa masked for RP. ":"#ffsa unmasked. "); 
    20392285      q.channel = "#ffsa";       q.answer(mask?"#fetishfuel masked for RP. ":"#fetishfuel unmasked. "); 
    2040     }, 
     2286    }, "forcequit": (Query q) { if (q.name != root_user) return q.answer("Unauthorized access! "); exit(0); }, 
    20412287    "woof": (Query q) { 
    20422288      if (rand()%100>95) return q.answer("I'm a doggie!"); 
     
    20512297      if (q.param.length) q.answer("<b>", q.param.getTitle(), "</b>"); 
    20522298      else synchronized(last_url_sync) if (auto p = q.channel in last_url) q.answer("<b>", (*p).getTitle(), "</b>"); 
    2053     } 
     2299    }, 
     2300    "dcode": &dcode /todg, 
     2301    "dstmt": (Query q) { q.param = d_imports ~ "void main() { "~q.param~" }"; dcode(q); }, 
     2302    "dexpr": (Query q) { q.param = d_imports ~ "void main() { auto value = "~q.param~"; writefln(\"%s\", value); }"; dcode(q); }, 
     2303    "neat": &neat /todg, 
     2304    "magic": &magic /todg, "listen": &listen /todg 
    20542305  ]) commands[key] = value; 
    2055   safelist ~= ["roll"[], "dice", "wp", "trope", "seen", "df", "woof", "title", "decide", "google"]; 
    2056   auto conn = new IRCconn(args[0], cast(nickname) args[1], 6667, resume_handle); 
     2306  safelist ~= ["roll"[], "dice", "wp", "trope", "seen", "df", "woof", "title", "decide", "google", "magic"]; 
     2307  short port = 6667; 
     2308  if (args[0].find(":") != -1) { 
     2309    string portstr = args[0]; 
     2310    args[0] = portstr.slice(":"); 
     2311    port = cast(short) portstr.atoi(); 
     2312  } 
     2313  auto conn = new IRCconn(args[0], cast(nickname) args[1], port, resume_handle); 
    20572314  conn.onFind = conn /apply/ &onFind; 
     2315  conn.onNickChange = conn /apply/ &onNickChange; 
    20582316  auto aborted = new bool; 
    20592317  scope(exit) *aborted = true; 
     
    20772335  // int[hostmask] strikes; 
    20782336  int freebird; auto lastbird = sec(); 
    2079   with (conn) { 
    2080     defaultChanHandler = (string channel, hostmask host, string msg) { 
     2337  conn.defaultChanHandler = stuple(conn, channel_guards, justJoined) /apply/ (typeof(conn) conn, ref typeof(channel_guards) channel_guards, typeof(justJoined) justJoined, string channel, hostmask _host, string msg) { 
     2338    with (conn) { 
     2339      auto host = _host; 
    20812340      if (!channel.length && notice_cb) { notice_cb(host, msg); return true; } 
    20822341      logln("< ", msg); 
     
    20892348        if (!netsplit) wbOnJoin(q); 
    20902349        justJoined[host] = true; 
     2350        auto entry = IRCconfig.get!(Stuple!(int, typeof(µsec()), string)) (conn.host, "peak_"~channel.clean(), { return stuple(0, µsec(), ""); }()); 
     2351        if (auto p = channel in conn.users) { 
     2352          auto users = p.length; 
     2353          if (users >= entry._0) 
     2354            IRCconfig.set(conn.host, "peak_"~channel.clean(), stuple(conn.users[channel].length, µsec(), cast(string) .nick(host))); 
     2355        } 
    20912356      } else { 
    20922357        if (host in justJoined) { 
     
    21122377      if (channel.tolower() == "#himitsu" && msg.startsWith("!list" /or/ "!new")) { 
    21132378        tp.addTask(stuple(Query(conn, .nick(host), channel), conn) /apply/ (Query q, IRCconn ic) { 
    2114           if (ic.exists(cast(nickname) "Himitsu_Millenium_Robot")) return; 
     2379          if (ic.exists(cast(nickname) "HimitsuBot")) return; 
    21152380          q.notice("Our bot isn't here right now. Perhaps try the archive bots in #lurk? "); 
    21162381        }); 
     
    21202385      seenfile.set(channel.clean(), (cast(string) .nick(host)).tolower(), stuple(msg, µsec())); 
    21212386      bool permit = true; 
     2387      permit &= .nick(host) != cast(nickname) "retardedFaggot"; 
    21222388      // if (channel.endsWith("tropers")) permit = false; // fucking bot nazis 
    21232389      if (channel.tolower().endsWith("anime")) permit = false; // moar nazis 
     2390      if (channel.tolower().endsWith("lesswrong")) permit = false; // nice people 
    21242391      permit &= .nick(host) != cast(nickname) "TropeBot"; 
    21252392      // echo is a silly feature 
     
    21422409      */ 
    21432410      // uncomment for awesome 
    2144       /*if (msg.ifind("freebird") != -1) { 
     2411      if (msg.ifind("__freebird") != -1) { 
    21452412        auto q = Query(conn, .nick(host), channel); 
    21462413        if (freebird == -1 && sec() - lastbird > 300) { 
     
    21572424          else freebird ++; 
    21582425        } 
    2159       }*/ 
     2426      } 
    21602427      if (false && msg.ifind("stop") != -1 && isPoking()) { 
    21612428        auto r = rand(); 
     
    21662433        cooldown = sec(); 
    21672434      } 
    2168       auto animes = msg.ifind("animes"); 
     2435      if (msg == "\x01VERSION\x01") { 
     2436        Query(conn, .nick(host), channel).notice("\x01VERSION feepbot\x01"); 
     2437        return true; 
     2438      } 
     2439      if (msg.startsWith("\x01PING")) { 
     2440        Query(conn, .nick(host), channel).notice(msg); 
     2441        return true; 
     2442      } 
     2443      /*auto animes = msg.ifind("animes"); 
     2444      auto msg2 = msg.tolower(); 
     2445      if (msg2.between("", "animes").endsWith("the ")) animes = -1; 
    21692446      const vocals="aeiouAEIOUqQ"; 
    2170       if (animes != -1 && (animes+6 == msg.length || vocals.find(msg[animes+6]) == -1)) { 
    2171         Query(conn, .nick(host), channel).answer( 
     2447      if (animes != -1 && (animes+6 == msg.length || vocals.find(msg[animes+6]) == -1) && channel.ifind("yackfest") == -1) { 
     2448        auto answer =  
    21722449          ["Anime."[], "It's anime.", "Anime is its own plural. ", "Anime.", "Anime >_>", "Just \"anime\"."] 
    2173           [rand()%$] 
    2174         ); 
     2450          [rand()%$]; 
     2451        Query(conn, .nick(host), channel).answer(answer); 
    21752452        return true; 
    2176       } 
     2453      }*/ 
    21772454      if (onFind) onFind(channel, host); 
    21782455      auto name = .nick(host); 
     
    21812458      auto lpos = msg.find("http://"); 
    21822459      if (auto rest = msg.startsWith(CTRL(channel))) { 
    2183         try roll2(Query(conn, name, channel, rest), true); 
    2184         catch (Exception ex) { 
    2185           Query(conn, name, channel, rest).say("Error: ", ex); 
    2186         } 
     2460        auto q = Query(conn, name, channel, rest); 
     2461        tp.addTask(q /apply/ (Query q) { 
     2462          try roll2(q, true); 
     2463          catch (Exception ex) { 
     2464            // q.say("Error: ", ex); 
     2465          } 
     2466        }); 
    21872467      } 
    21882468      if (auto rest = msg.startsWith("> ")) { 
     
    21982478        } 
    21992479        bool flooded; 
    2200         if (channel /notin/ channel_guards) 
    2201           channel_guards[channel] = floodguard!(false)(6, 3, { flooded = true; }, { flooded = false; }); 
    2202         channel_guards[channel](1); 
     2480        synchronized(SyncObj!(channel_guards)) { 
     2481          if (channel /notin/ channel_guards) 
     2482            channel_guards[channel] = floodguard!(false)(6, 3, { flooded = true; }, { flooded = false; }); 
     2483          channel_guards[channel](1); 
     2484        } 
    22032485        auto query = Query(conn, name, channel, cmd); 
    22042486        if (flooded && (name != root_user)) query.notice("You have triggered my flood guard prevention. Please wait a bit."); 
     
    22202502        } 
    22212503        permit &= link.ifind("scp-wiki.wikidot.com") == -1; 
    2222         permit &= channel.tolower() != "#d" /or/ "#d.gdc"; 
     2504        permit &= channel.tolower() != 
     2505          "#d" /or/ "#d.gdc" /or/ "#yackfest" /or/ "#mcdevs" /or/ "#tgchan" 
     2506           /or/ "#c++"; 
     2507        permit &= .nick(host) != cast(nickname) "CIA-110"; 
     2508        permit &= .nick(host) != cast(nickname) "CIA-81"; 
     2509        permit &= .nick(host) != cast(nickname) "gh-clutter"; 
    22232510        if (permit) { 
    22242511          tp.addTask(stuple(link, channel, Query(conn, name, channel, ""), lpos + link.length + 2) 
    22252512            /apply/ (string link, string channel, Query query, int ll) { 
    22262513            auto title = link.getTitle(); 
     2514            if (!title) return; 
    22272515            if (channel.endsWith("tropers")) { 
    22282516              string rss; 
     
    22592547            string f; 
    22602548            if (channel.endsWith("tropers")) f = "b"; 
     2549            else if (channel.endsWith("lets-read")) f = "u"; 
    22612550            if (title.length) { 
    22622551              if (f) query.say(/*foo, */"<", f, ">", title, "</", f, ">"); 
     
    22692558        if (key.strip() /notin/ anypos) continue; 
    22702559        if (match != -1) { 
    2271           tp.addTask(&runQuery /fix/ Query(conn, name, channel, msg[match+1 .. $], 0, true)); 
     2560          if (.nick(host) == cast(nickname) "Lymia") { Query(conn, .nick(host), channel).say("Fuck you, ", .nick(host), "."); return true; } 
     2561          else { 
     2562            tp.addTask(&runQuery /fix/ Query(conn, name, channel, msg[match+1 .. $], 0, true)); 
     2563          } 
    22722564          wasCommand = true; 
    22732565        } 
     
    22822574      } 
    22832575      if (!wasCommand && !(mask_rp && (channel == "#fetishfuel" /or/ "#ffsa"))) { 
    2284         if (auto fdp = channel in bridget) { 
    2285           auto mname = name[0]~""~cast(string) name[1..$]; 
    2286           if (auto rest = msg.startsWith("\x01ACTION ")) { 
    2287             fdp.writeLine(Format("* ", mname, " ", rest[0 .. $-1])); 
    2288           } else if (msg == IRCconn.JOIN) { 
    2289             fdp.writeLine(Format("* ", mname, " has joined. ")); 
    2290           } else if (msg == IRCconn.PART) { 
    2291             fdp.writeLine(Format("* ", mname, " has left. ")); 
    2292           } else { 
    2293             fdp.writeLine(Format("<", mname, "> ", msg)); 
     2576        if (auto fdp = channel in writers) { 
     2577                                                   // non-breaking space 
     2578          string mname = name; 
     2579          int last_vocal = -1; 
     2580          foreach_reverse(i, ch; mname) if ("aeiouAEIOU".find(ch) != -1) last_vocal = i; 
     2581          if (last_vocal != -1) mname = mname[0 .. last_vocal] ~ mname[last_vocal] ~ mname[last_vocal .. $]; 
     2582          try { 
     2583            if (auto rest = msg.startsWith("\x01ACTION ")) { 
     2584              fdp.writeLine(Format("* ", mname, " ", rest[0 .. $-1])); 
     2585            } else if (msg == IRCconn.JOIN) { 
     2586              fdp.writeLine(Format("* ", mname, " has joined. ")); 
     2587            } else if (msg == IRCconn.PART) { 
     2588              fdp.writeLine(Format("* ", mname, " has left. ")); 
     2589            } else { 
     2590              fdp.writeLine(Format("<", mname, "> ", msg)); 
     2591            } 
     2592          } catch (Exception ex) { 
     2593            logln(ex, ". Ignoring. "); 
    22942594          } 
    22952595        } 
    22962596      } 
    22972597      return false; 
    2298     }; 
     2598    } 
     2599  }; 
     2600  with (conn) { 
    22992601    privmsg = defaultChanHandler /fix/ ""; 
    23002602    tp.addTask(aborted /apply/ (ref bool aborted) { 
    2301       sleep (10); 
     2603      sleep(2); 
     2604      auto onstart = onStart(args[0]); 
     2605      if (onstart.length) { 
     2606        auto mesg1 = cast(hostmask) (cast(string) root_user ~ "!@"), mesg2 = onstart.take(); 
     2607        logln("first mesg ", mesg1, ", ", mesg2); 
     2608        privmsg(mesg1, mesg2); 
     2609      } 
     2610      sleep (6); 
    23022611      if (aborted) return; 
    23032612      join(args[2], defaultChanHandler); 
    2304       foreach (line; onStart(args[0])) { 
     2613      foreach (line; onstart) { 
    23052614        privmsg(cast(hostmask) (cast(string) root_user ~ "!@"), line); 
    23062615      } 
     
    23212630        message(channel, ex.toString~". Time to die. :("); 
    23222631      }*/ 
     2632      conn.invalidateHandlers(); 
    23232633      logln(ex, " Rethrowing. "); 
    23242634      throw ex; 
  • trunk/idc/irc.d

    r723 r842  
    88 
    99import tools.time; 
     10 
     11string tolower(string s) { 
     12  s = s.dup; 
     13  foreach (ref ch; s) { if (ch >= 'A' && ch <= 'Z') ch = 'a' + (ch - 'A'); } 
     14  return s; 
     15} 
    1016 
    1117template GuardTypes(bool B) { 
     
    3743        while (sum > calls) { 
    3844          cleanup(); 
    39           lock.Unsynchronized = { slowyield(); }; 
     45          lock.Unsynchronized = { usleep(200_000); }; 
    4046        } 
    4147      } else cleanup(); 
     
    8894  void delegate(int) guard; 
    8995  void delegate(string channel, hostmask host) onFind; 
     96  void delegate(string channel, hostmask from, hostmask to) onNickChange; 
    9097  void delegate(string) onJoin; 
     98  void invalidateHandlers() { 
     99    onFind = null; 
     100    onJoin = null; 
     101    channels = null; 
     102    onNickChange = null; 
     103  } 
    91104  bool registered(nickname n) { 
    92105    auto waiter = new Semaphore; 
     
    114127  this(string host, nickname nick, int port = 6667, uint resume_handle = -1) { 
    115128    this.host = host; 
    116     guard = floodguard!(true)(100_000, 60); // 100 kb per 60
     129    guard = floodguard!(true)(1_000, 5); // 1 kB / 5
    117130    if (resume_handle == -1) { 
    118131      auto dpos = host.find(":"); 
     
    189202      logln(chan, " -> users: ", users[chan]); 
    190203    } 
    191     if (auto rest = line.startsWith("JOIN")) { 
    192       auto channel = rest[2 .. $].tolower(); // "JOIN :[channel]" 
     204    if (auto rest = line.startsWith("JOIN ")) { 
     205      auto channel = rest.tolower(); 
     206      if (channel.startsWith(":")) channel = channel[1 .. $]; 
    193207      if (onFind) onFind(channel, host); 
    194208      if (channel /notin/ channels) 
     
    209223      return; 
    210224    } 
    211     if (auto rest = line.startsWith("NICK")) { 
     225    if (auto rest = line.startsWith("NICK :")) { 
    212226      outer:foreach (channel, value; users) { 
    213227        foreach (ref person; value) { 
    214228          if (person == cast(string) .nick(host)) { 
    215229            if (onFind) onFind(channel, cast(hostmask) host.replace(person, rest)); 
     230            if (onNickChange) onNickChange(channel, host, cast(hostmask) host.replace(person, rest)); 
    216231            break outer; 
    217232          } 
     
    222237    if (auto rest = line.startsWith("433")) { 
    223238      if (line.find("already in use") != -1) { 
    224         throw new Exception("Nick already in use! "); 
     239        throw new Exception("Nick already in use! '"~line~"'"); 
    225240      } 
    226241    } 
     
    321336} 
    322337 
    323 string IRCFormat(T...)(T t) { 
     338struct FormatContext { 
     339  int bold_depth, ul_depth, inv_depth; 
    324340  string res; 
    325   int bold_depth, ul_depth, inv_depth; 
    326   void delegate(string, ref string)[string] commands = 
    327   ["<b>"[]: (string pre, ref string post) { 
    328     if (!bold_depth) res ~= pre~"\x02"; 
    329     bold_depth++; 
    330   }, "</b>": (string pre, ref string post) { 
    331     bold_depth--; 
    332     if (!bold_depth) res ~= pre~"\x02"; 
    333   }, "<u>": (string pre, ref string post) { 
    334     if (!ul_depth) res ~= pre~"\x1F"; 
    335     ul_depth++; 
    336   }, "</u>": (string pre, ref string post) { 
    337     ul_depth--; 
    338     if (!ul_depth) res ~= pre~"\x1F"; 
    339   }, "<i>": (string pre, ref string post) { 
    340     if (!inv_depth) res ~= pre~"\x16"; 
    341     inv_depth++; 
    342   }, "</i>": (string pre, ref string post) { 
    343     inv_depth--; 
    344     if (!inv_depth) res ~= pre~"\x16"; 
    345   }, "<br>": (string pre, ref string post) { 
    346     res ~= pre~"\n"; 
     341  void openBold(string pre, ref string post) { 
     342    if (!bold_depth) res ~= pre ~ "\x02"; 
     343    bold_depth ++; 
     344  } 
     345  void closeBold(string pre, ref string post) { 
     346    bold_depth --; 
     347    if (!bold_depth) res ~= pre ~ "\x02"; 
     348  } 
     349  void openUnderline(string pre, ref string post) { 
     350    if (!ul_depth) res ~= pre ~ "\x1F"; 
     351    ul_depth ++; 
     352  } 
     353  void closeUnderline(string pre, ref string post) { 
     354    ul_depth --; 
     355    if (!ul_depth) res ~= pre ~ "\x1F"; 
     356  } 
     357  void openItalic(string pre, ref string post) { 
     358    if (!inv_depth) res ~= pre ~ "\x16"; 
     359    inv_depth ++; 
     360  } 
     361  void closeItalic(string pre, ref string post) { 
     362    inv_depth --; 
     363    if (!inv_depth) res ~= pre ~ "\x16"; 
     364  } 
     365  void insertNewline(string pre, ref string post) { 
     366    res ~= pre ~ "\n"; 
    347367    // recreate formatting 
    348368    if (bold_depth) res ~= "\x02"; 
    349369    if (ul_depth) res ~= "\x1F"; 
    350370    if (inv_depth) res ~= "\x16"; 
    351   }]; 
     371  } 
     372  void delegate(string, ref string)[string] map; 
     373  void setupMap() { 
     374    map["<b>"] = &openBold; 
     375    map["</b>"] = &closeBold; 
     376    map["<u>"] = &openUnderline; 
     377    map["</u>"] = &closeUnderline; 
     378    map["<i>"] = &openItalic; 
     379    map["</i>"] = &closeItalic; 
     380    map["<br>"] = &insertNewline; 
     381  } 
     382
     383 
     384TLS!(FormatContext) foco; 
     385 
     386static this() { New(foco, { auto res = new FormatContext; res.setupMap(); return res; }); } 
     387 
     388string IRCFormat(T...)(T t) { 
    352389  auto base = Format(t); 
    353390  auto nfpos = base.find("<noformat/>"); 
    354391  if (nfpos != -1) return IRCFormat(base[0 .. nfpos]) ~ base[nfpos + 8+3 .. $]; 
    355   base.glomp_parse(commands, (string rest) { res ~= rest; }); 
    356   return res; 
     392  auto ctx = foco.ptr(), map = ctx.map; 
     393  ctx.res = null; 
     394  base.glomp_parse(map, (string rest) { ctx.res ~= rest; }); 
     395  return ctx.res; 
    357396} 
    358397 
     
    369408  } 
    370409  void notice(T...)(T t) { 
    371     connection.raw_sendln(IRCFormat("NOTICE ", name, " :", t)); 
     410    output((string line) { connection.raw_sendln(IRCFormat("NOTICE ", name, " :", line)); }, t); 
    372411  } 
    373412  void answer(T...)(T t) { say(cast(string) name, ": ", t); } 
    374   void say(T...)(T t) { 
     413  void say(T...)(T t) { output((string line) { connection.message(channel, line); }, t); } 
     414  void output(T...)(void delegate(string) dg, T t) { 
    375415    auto msg = Format(t); 
    376416    string pre_line; 
     
    408448    else breakIt(msg); 
    409449    auto lines = IRCFormat(pre_line).split("\n"); 
    410     foreach (line; lines) 
    411       connection.message(channel, line); 
     450    foreach (line; lines) dg(line); 
    412451  } 
    413452  void act(T...)(T t) { connection.message(channel, "\x01"~Format("ACTION ", t).IRCFormat()~"\x01"); } 
  • trunk/idc/pad/cgi.d

    r719 r842  
    7272    else res ~= "<"~ent~">"; 
    7373  } 
    74   foreach (ent; Tuple!("br", "b", "/b", "i", "/i")) { 
     74  foreach (ent; Tuple!("br", "b", "/b", "i", "/i", "/p", "/a")) { 
    7575    ent_map["&lt;"~ent~"&gt;"] = ent.dup /apply/ &fun; 
    7676  } 
     
    7979    auto imgbody = post.slice("&gt;"); 
    8080    if (imgbody.find(" on") != -1) throw new Exception("JavaScript is a security risk, and not supported. "); 
    81     res ~= "<img"~imgbody~">"; 
     81    res ~= "<img"~imgbody.replace("&quot;", "\"")~">"; 
    8282  }; 
    8383  ent_map["&lt;p"] = (string pre, ref string post) { 
     
    8686    if (peabody.find(" on") != -1) throw new Exception("JavaScript is a security risk, and not supported. "); 
    8787    res ~= "<p"~peabody~">"; 
     88  }; 
     89  ent_map["&lt;a"] = (string pre, ref string post) { 
     90    res ~= pre; 
     91    auto abody = post.slice("&gt;"); 
     92    if (abody.find(" on") != -1) throw new Exception("JavaScript is a security risk, and not supported. "); 
     93    res ~= "<a"~abody.replace("&quot;", "\"")~">"; 
    8894  }; 
    8995  ent_map["http://"] = (string pre, ref string post) { 
     
    159165  if (back && lines.length) lines = lines[0 .. $-1]; 
    160166  string default_input; 
     167  string[] input_lines = lines; 
    161168  if (auto lstr = "backToLine" in chunks) { 
    162169    auto line = (*lstr).atoi(); 
     
    193200    output = null; 
    194201    ex.msg = ex.msg.replace("<", "&lt;").replace(">", "&gt;"); 
    195     error = Format("<h3>An error has occurred. </h3><h4>", ex, "</h4><a href=\"javascript:history.go(-1); \">Back</a>"); 
     202    error = Format("<h3>An error has occurred. </h3><h4>", ex, "</h4><a href=\"javascript:history.go(-1); \">Back</a>"/*<br> 
     203      The commands leading up to this were: ", Format(input_lines).replace(", ", ", <br>")*/, " "); 
    196204  } 
    197205  auto needed = sec() - start; 
     
    214222    postText = "</div>"; 
    215223  } 
     224  string res; 
    216225  "Content-type: text/html; charset=utf-8\r\n 
    217226<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" 
     
    220229<head> 
    221230  <meta http-equiv=\"Content-type\" content=\"text/html;charset=UTF-8\" /> 
    222   <title>IF Web Interface - generated in %TIME%s + %TIME2%s, %MEM%MB used</title> 
     231  <title>IF Web Interface - generated in %TIME%s + %TIME2%s for I/O, %MEM%MB used</title> 
    223232</head> 
    224233<body> 
     
    254263</body> 
    255264</html> 
    256   ".dynReplace((string s) { printf("%.*s", s); }, 
     265  ".dynReplace((string s) { printf("%.*s", s); res ~= s; }, 
    257266    "%PREVENCODED%", encoded, 
    258267    "%PREV_REENCODED%", encoded.entitiesEncode(), 
     
    273282    "%MEM%", { GCStats gs = void; getStats(gs); return Format(gs.poolsize / 1_000_000f); }() 
    274283  ); 
     284  "/tmp/lastpage.html".write(res); 
    275285} 
    276286 
  • trunk/idc/pad/engine.d

    r719 r842  
    3737    } 
    3838  } 
    39   foreach (ch; cast(ubyte[]) predata) data ~= map[ch]; 
    40   if (sub_bin) sub_bin ~= "."; 
    41   // auto url = "POST=parent_pid=&format=text&code2="~data~"&poster=&paste=Send&expiry="~expiry~"&email= " 
    42   //   "http://"~sub_bin~"pastebin.com/pastebin.php"; 
    43   // Pastebin v2 .. 
    44   auto url = "POST=submit=submit&paste_parent_key=&paste_subdomain="~sub_bin~ 
    45     "&paste_code="~data~"&paste_format=1&paste_expire_date="~expiry~"&paste_private=1&paste_name=&paste_remember=0&submit=submit " 
    46     "http://"~sub_bin~"pastebin.com/post.php"; 
    47   string redir; 
    48   url.download(&redir); 
    49   if (redir.startsWith("COOKIE=")) redir.slice(" "); 
    50   return redir; 
     39  foreach (ch; cast(ubyte[]) predata) { 
     40    if (ch > 'a' && ch < 'z' || ch > 'A' && ch < 'Z') data ~= ch; 
     41    else data ~= map[ch]; 
     42  } 
     43  // Pastebin public api .. 
     44  auto url = "POST=submit=submit&paste_subdomain="~sub_bin~ 
     45    "&paste_code="~data~"&paste_format=text&paste_expire_date="~expiry~"&paste_private=1&paste_name=&paste_remember=0 " 
     46    "http://pastebin.com/api_public.php"; 
     47  // auto url = "POST=api=1&code="~data~"&chk_private http://www.webpaste.net/"; 
     48  return url.download(); 
    5149} 
    5250 
     
    202200  auto res = s2.take(matched); 
    203201  if (!res.length) { 
    204     if (mode != TokenMode.Normal || !s2.length) { *failtext.ptr() = "Invalid token "~st.strip().next_text(); return false; } 
     202    if (mode != TokenMode.Normal || !s2.length) { 
     203      *failtext.ptr() = *failtext.ptr() ~ Format("Invalid token ", st.strip().next_text(), ", matched ", matched, ", mode ", mode, ". "); 
     204      return false; 
     205    } 
    205206    st = s2; s = st.take(1); // single-symbol token 
    206207    return true; 
     
    408409                 | bool          | int         | string               | float   | Scope 
    409410      -----------+---------------+-------------+----------------------+---------+---------- 
    410       Boolean    | b             | b           | b?q{true}p:q{false}p | Þ       | Þ 
    411       Integer    | i != 0        | i           | Format(i)            | i       | Þ 
    412       String     | s == q{true}p | atoi(s)     | s                    | atof(s) | Þ 
    413       Float      | Þ             | cast(int) f | Format(f)            | f       | Þ 
    414       ScopeRef   | !!sr          | Þ           | (sr?sr.fqn:q{(null:r)}p) | Þ       | sr 
     411      Boolean    | b             | b           | b?q{true}p:q{false}p |         |  
     412      Integer    | i != 0        | i           | Format(i)            | i       |  
     413      String     | s == q{true}p | atoi(s)     | s                    | atof(s) |  
     414      Float      |               | cast(int) f | Format(f)            | f       |  
     415      ScopeRef   | !!sr          |             | (sr?sr.fqn:q{(null:r)}p) |     | sr 
    415416      ScopeValue | sr.value().to!(T) | sr.value().to!(T) | sr.value().to!(T) | sr.value().to!(T) | sr`; 
    416417    mixin(ctTableUnrollColMajor(Table, 
     
    422423      else `, 
    423424      `case FlatType.$ROW: 
    424         static if (q{$CELL}p == "Þ"
     425        static if (!q{$CELL}p.length
    425426          throw new Exception(q{Cannot convert $ROW to $COL: }p~to!(string)~q{! }p); 
    426         else return $CELL; 
     427        else { return $CELL; } 
    427428      ` 
    428429    ).litstring_expand() ~ `static assert(false, "Unsupported type: "~T.stringof); `); 
     
    12241225      } else break; 
    12251226    } 
    1226     if (!accept(s2, "}")) return false; 
     1227    if (!accept(s2, "}")) { 
     1228      throw new Exception(Format("Expected closing bracket at ", s2.next_text())); 
     1229    } 
    12271230    s = s2; 
    12281231    return true; 
     
    15321535  static void getThis(ref string s, out Scope sc) { 
    15331536    try _getThis(s, sc); 
    1534     catch (Exception ex) throw new Exception(Format("Failed to parse Scope: ", ex, ". ")); 
     1537    catch (Exception ex) throw new Exception(Format("Failed to parse Scope '", sc?sc.name:"", "': ", ex, ". ")); 
    15351538  } 
    15361539  static void _getThis(ref string s, out Scope sc) { 
     
    15751578        scp.replant(this); 
    15761579    } 
     1580  } 
     1581  bool noStmts() { 
     1582    foreach (statement; statements) 
     1583      if (auto sc = castToScope(statement)) { 
     1584        if (sc.name == "default") return false; // SORT of a statement 
     1585      } else return false; 
     1586    return true; 
    15771587  } 
    15781588  // breaks on gdc with final 
     
    16821692  Scope lookdown(string var, ref RecursionCheck visited, bool userquery = false, bool below = false) { 
    16831693    if (!var.length) { 
    1684       if (userquery && "failLookup" in this) return null; 
     1694      if (userquery && ("failLookup" in this || noStmts)) return null; 
    16851695      return this; 
    16861696    } 
     
    17211731      } 
    17221732      return cur.lookdown(var, visited, userquery, below); 
    1723     } else if (!below && isNamed(first)) return lookdown(var, visited, userquery, below); 
     1733    } else if (!below && isNamed(first) && !parent) return lookdown(var, visited, userquery, below); // only do this if we're the root!! 
    17241734    else { 
    17251735      Scope[] fallbacks; 
     
    17311741          // pure recursion 
    17321742          if (!mst.checkingRightNow) // a scope can't depend on itself 
    1733             if (auto res = mst.src.lookdown(var_backup, visited, userquery, below)) return res; 
     1743            if (auto res = mst.src.lookdown(var_backup, visited, userquery, below)) 
     1744              if (!(userquery && res.noStmts)) return res; 
    17341745        } 
    17351746        if (auto ast = cast(AliasStatement) entry) { 
     
    17611772          } 
    17621773          if (sc.isNamed(first)) { 
    1763             if (auto res = sc.lookdown(var, visited, userquery)) return res; 
     1774            if (auto res = sc.lookdown(var, visited, userquery)) 
     1775              if (!(userquery && res.noStmts)) return res; 
    17641776          } 
    17651777        } 
     
    17671779      // Multiple fallbacks because instances might introduce more than one "any" block 
    17681780      if (fallbacks) { 
    1769         foreach (fallback; fallbacks) if (auto res = fallback.lookdown(var, visited, userquery))
     1781        foreach (fallback; fallbacks) if (auto res = fallback.lookdown(var, visited, userquery)) if (!(userquery && res.noStmts))
    17701782          fallback.value = Value(first); 
    17711783          return res; 
     
    17971809      auto from = re.from(sc).to!(string), to = re.to(sc).to!(string); 
    17981810      auto alt = regex_sub(name, from, to); 
     1811      // throw new Exception(Format("|", from, "| |", to, "| |", name, "| |", alt, "|")); 
    17991812      if (alt != name) { 
    18001813        auto res = lookup(alt, userquery, vp); 
     
    18071820        if (sc.global) { 
    18081821          auto rest = name, first = rest.slice("."); 
    1809           if (auto res = sc.lookdown(first, *vp, userquery)) { 
    1810             if (!res.local) { 
     1822          { 
     1823            auto res = sc.isNamed(first)?sc:null; 
     1824            if (!res) res = sc.lookdown(first, *vp, !rest.length /* don't block if there's more to come */); 
     1825            if (res && !res.local) { 
    18111826              if (auto res2 = res.lookdown(rest, *vp, userquery)) return stuple(res2, true); 
    18121827            } 
  • trunk/idc/pad/mainloop.d

    r715 r842  
    11module pad.mainloop; 
    22 
    3 import pad.engine, pad.utils, tools.log, tools.compat: string
     3import pad.engine, pad.utils, tools.log, tools.compat
    44static import tools.compat; 
    55 
     
    7171  return runLoop(src, "", write, read, readFS, dontSave, stopstart); 
    7272} 
     73 
     74import tools.rd: next_text; 
    7375 
    7476const bool ReadFS = true, DontSave = true; 
     
    8890      Scope.getThis(s2, root); 
    8991    } 
     92    if (s2.strip().length) { 
     93      throw new PadException(Format("Unknown/leftover text at `", s2.strip().next_text(), "'")); 
     94    } 
    9095    if (full || !stream) stream = new SaveStream; 
    9196    if (!root) return; 
     
    9398    location = root.eval(initial, root); 
    9499    if (!location) { 
    95       throw new PadException("Evaluating root didn't generate a redirect! "); 
     100      throw new PadException("Evaluating root didn't generate a redirect! "/* ~ data*/); 
    96101    } 
    97102    if (verbose) write(initial.collate); 
     
    108113    } 
    109114    while (line) { 
    110       auto chunk = line.slice(";")
     115      auto chunk = line.slice(";").strip()
    111116      auto parts = chunk.split(" "); 
    112117      if (parts.length == 1) cmd = parts[0]; 
     
    139144    write("Forwarded. "); 
    140145  } else reset(true); 
    141   string lastLoaded; 
     146  string lastLoaded = src; 
     147  int lines; 
    142148  while (true) { 
    143149    auto line = read(); 
     150    lines ++; 
    144151    if (line == "quit" || line == "q") return write("Bye! "); 
    145152    if (line == "forcequit") return; 
     
    162169        auto url = pastepost(postdata); 
    163170        write(Format("Saved ", postdata.length, " (", predata.length, ") to ", url)); 
     171        continue; 
     172      } 
     173      if (line == "info") { 
     174        write(Format("You are playing ", lastLoaded, ", ", lines, " lines in. ")); 
    164175        continue; 
    165176      } 
     
    191202      continue; 
    192203    } 
    193     if (auto from = line.startsWith("open ")) { 
     204    if (!src) if (auto from = line.startsWith("open ")) { 
    194205      if (from.find(" ") != -1) throw new Exception("Invalid URL: \""~from~"\". "); // No POST hacking! Bad user! 
    195206      if (stopstart) stopstart(); 
  • trunk/idc/pad/utils.d

    r713 r842  
    2222      string data2; 
    2323      foreach (entry; data.betweens("<pre class=\"code\">", "<")) 
    24         data2 ~= entry; 
     24        data2 ~= entry 
     25          .replace("&quot;", "\"") 
     26          .replace("&lt;", "<") 
     27          .replace("&gt;", ">") ~ "\n"; 
    2528      data = data2; 
     29    } 
     30    if (data.find("/ep/pad/view/") != -1) { // EtherPad 
     31      auto id = data.between(`href="`, `" id="docbarslider`, GLOMP_RIGHT); 
     32      auto redir = addr.followLink(id)~"?pt=1"; 
     33      data = redir.download();       
    2634    } 
    2735    if (data.find("%PAD START%") != -1) { 
     
    3947      if (inTarget) 
    4048        data ~= line.between("content=\"", "")[0 .. $-1] 
    41           .replace(`\n`, "\n").replace(`\"`, "\"").replace(`\\`, "\\"); 
     49          .replace(`\n`, "\n") 
     50          .replace(`\r`, "\r") 
     51          .replace(`\t`, "\t") 
     52          .replace(`\"`, "\"") 
     53          .replace(`\\`, "\\"); 
    4254      if (line.endsWith("title=\""~addr~"\"")) inTarget = true; 
    4355    } 
     
    97109  string res; 
    98110  int nest_level = 0; 
    99   void eatToBreak(ref string s) { 
     111  void eatToNewline(ref string s) { 
    100112    auto pos = s.find("\n"); 
    101113    if (pos == -1) return; 
     
    104116  text.glomp_parse([ 
    105117    "\""[]: (string pre, ref string post) { if (!nest_level) { res ~= pre ~ "\"" ~ getLiteral(post, true) ~ "\""; } }, 
    106     "//": (string pre, ref string post) { if (!nest_level) { res ~= pre; post.eatToBreak(); } }, 
     118    "//": (string pre, ref string post) { if (!nest_level) { res ~= pre; post.eatToNewline(); } }, 
    107119    "/*": (string pre, ref string post) { if (!nest_level) res ~= pre; nest_level ++; }, 
    108120    "*/": (string pre, ref string post) { if (!nest_level) throw new Exception("Too many */"); nest_level --; }