Created
January 6, 2015 03:55
-
-
Save anonymous/14f9a5a64e7e0c16d164 to your computer and use it in GitHub Desktop.
idc.d
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
module idc; // Internet delay chat! Yay! | |
import tools.fixed_socket, tools.base; | |
import tools.log, tools.threads, tools.threadpool; | |
import tools.time; | |
import readback; | |
string download(string url, bool first = false) { | |
if (url.find("://") == -1) url = "http://"~url; | |
string cmd = "wget -T 5 -t 5 --user-agent=\"Mozilla/5.0 (Windows NT 5.2; rv:2.0.1) Gecko Firefox\" -q -O- \""~urlencode(url, true)~"\""; | |
if (first) cmd ~= " | head -c 10K"; | |
logln("$ "~cmd); | |
string res = read_cmd(cmd); | |
logln("? ", res.length, "b"); | |
return res; | |
} | |
import irc, strophe; | |
import cah; | |
extern(C) void exit(int); | |
alias irc.tolower tolower; | |
string characters(string s) { | |
string res; | |
foreach (ch; s) | |
if (ch in Range['a' .. 'z'].endIncl /or/ Range['A' .. 'Z'].endIncl) res ~= ch; | |
return res; | |
} | |
string clean(string s) { | |
auto res = s.tolower().replace("'", "").replace("_", ""); | |
if (res == "127.0.0.1") res = "teentropers"; | |
return res; | |
} | |
void statline(Query query) { | |
int[6] buckets; | |
foreach (ref b; buckets) b = 3; | |
void rolldie() { | |
while (true) { | |
auto die = rand() % 6; | |
if (buckets[die] >= 18) continue; | |
buckets[die] ++; | |
break; | |
} | |
} | |
for (int i = 0; i < 57; ++i) { | |
rolldie; | |
} | |
query.answer("Rolled 57d6 + 3, distribution: ", buckets); | |
} | |
extern(C) string CTRL(string channel) { | |
return IRCconfig.get!(string)(channel.clean(), "ControlChar", "."); | |
} | |
void setCtrl(string channel, string value) { | |
IRCconfig.set(channel.clean(), "ControlChar", value); | |
} | |
string[] onStart(string server) { | |
return IRCconfig.get!(string[])(server, "onStartup", cast(string[]) null); | |
} | |
void addOnStart(Query query) { | |
if (!auth(query)) return query.answer("Unauthorized access! "); | |
auto serv = query.connection.host; | |
IRCconfig.set(serv, "onStartup", onStart(serv) ~ query.param); | |
query.answer("Command added. "); | |
} | |
string exec; string[] args; | |
extern(C) nickname root_user, reload_nick; | |
bool force_auth; // during startup | |
bool auth(Query q) { | |
return q.name == root_user && (force_auth || q.connection.registered(q.name)); | |
} | |
uint socket_handle; | |
int count = 1; | |
void woof(Query q) { | |
if (rand()%100>95) return q.answer("I'm a doggie!"); | |
if (rand()%100>90) return q.answer(":pounce:"); | |
else if (rand()%100>90) return q.answer(":tailwag:"); | |
else if (rand()%100>90) return q.answer(":facelick:"); | |
else if (rand()%100>50) return q.answer("Woof! "); | |
else if (rand()%100>50) return q.answer("Growl. Woof! "); | |
else return q.answer("Woof! Woof!! "); | |
} | |
void rr(Query query) { | |
auto channel = query.channel; | |
void reload(int rounds = 6) { | |
int roll; | |
roll = rand() % rounds; | |
IRCconfig.set(channel.clean(), "RR", roll); | |
// query.answer("The gun has been reloaded. "); | |
} | |
if (query.param == "load") return reload(); | |
auto value = IRCconfig.get!(int)(channel.clean(), "RR", 0); | |
if (value) { | |
query.answer("Click. "); | |
IRCconfig.set(channel.clean(), "RR", value-1); | |
} else { | |
void qdie(string msg, int num) { | |
query.answer(msg~"You lose at life. Reloading."); | |
reload(num); | |
} | |
if (rand()%100>70) return qdie("You blow a hole through your head. Chunks of your brain, mixed with blood, splatter on the wall behind you. ", 6); | |
if (rand()%100>70) return qdie("The shotgun embeds its load deep in your cranium. Brain function is disrupted. Your skull now looks like a showerhead. You die immediately. Good job! ", 2); | |
if (rand()%100>70) return qdie("As the bullet penetrates your skull, you have a last, fleeting instant to ask yourself - was this really worth it? Aaand it's over. ", 7); | |
if (rand()%100>70) return qdie("Clickclickclickclickclickclickbooom. You don't even hear the shot that ends you. Playing Russian Roulette with an automatic weapon was not your brightest idea in life, champ. Somewhere far away, Darwin smiles a knowing smile. ", 30); | |
if (rand()%100>70) return qdie("What's there to say? A shot, a kill. Nothing accomplished, nothing of any value lost. The world spins on as before. Nobody cares but the crows. -", 6); | |
if (rand()%100>70) return qdie("Let's be serious for a minute. You are imitating suicide on IRC. You need help. Go seek help. Not from feepbot. It cannot help you. It is not programmed for fixing your fucked up issues. It is just a simple bot. PS ", 6); | |
if (rand()%100>70) { reload(); return woof(query); } | |
return qdie("Boom! You're dead. ", 6); | |
} | |
} | |
string[] grepv(string[] s, string t) { | |
string[] res; | |
foreach (str; s) | |
if (str.find(t) == -1) res ~= str; | |
return res; | |
} | |
import std.file; | |
import std.process; | |
alias std.process.system system; | |
void reload(Query query) { | |
if (system("sh -c \"make >/dev/null 2> /tmp/idc_errors || exit 1\"") != 0) { | |
query.answer("Error occured while rebuilding: "~"/tmp/idc_errors".read().castLike("").split("\n").grepv("EXPER")[0]); | |
return; | |
} else logln("Build complete."); | |
logln("Asked to reload by ", query.name, " in ", query.channel); | |
if (auth(query)) { | |
execv(exec, exec~args~["--resume"[], toString(socket_handle), | |
query.channel.length?query.channel:cast(string) query.name, cast(string) query.name, toString(count)]); | |
} else query.answer("Unauthorized!"); | |
} | |
string urlencode(string q, bool full_url = false) { | |
string res; | |
string specials = "._-"; | |
if (full_url) specials = "._-/?&:= "; | |
int skip; | |
foreach (ch; q) { | |
if (skip--) { res ~= ch; continue; } | |
if (ch == ' ') res ~= "+"; | |
else if (ch == '%') { res ~= ch; skip = 2; continue; } | |
else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')) res ~= ch; | |
else if (specials.find(ch) != -1) res ~= ch; | |
else { | |
const hex = "0123456789ABCDEF"; | |
res ~= "%" ~ hex[ch/16] ~ hex[ch%16]; | |
} | |
} | |
return res; | |
} | |
string googleQuery(string q, string site = null) { | |
if (site) q = "site:"~site~" "~q; | |
// auto hash = "http://www.google.com/uds/?file=search&v=1.0".download().between("JSHash = '", "'"); | |
auto url = "http://www.google.com/uds/GwebSearch?context=0&callback=GwebSearch.RawCompletion&hl=en&v=1.0&q="~q.urlencode(); | |
// if (site) url ~= "&as_sitesearch="~site; | |
auto sitedata = url.download(); | |
auto res = sitedata.between("unescapedUrl\":\"", "\"").replace("\\u003d", "\u003d"); | |
if (!res.length) | |
if (sitedata.length < 132) throw new Exception(Format("No results! '", sitedata, "'")); | |
else throw new Exception(Format("No results! ", sitedata.length)); | |
// if (!res.length) throw new Exception("Cannot identify Google result for "~url); | |
if (res.find("://") == -1) res = "http://"~res; | |
res = res.replace("\\u0026", "\u0026"); | |
return res; | |
} | |
string mktiny(string url) { | |
return ("http://api.bit.ly/v3/shorten?login=feepingcreature&apiKey=R_135cd22b3c6bef4071c3c0b7e738189c&longUrl="~url.urlencode()) | |
.download() | |
.between("\"url\": \"", "\"") | |
.replace("\\/", "/"); | |
// auto tinydata = ("POST=url="~url.urlencode()~"&submit=Make+TinyURL%21&alias= http://tinyurl.com/create.php").download(); | |
// return "http://tiny"~tinydata.between("<b>http://tiny", "</b>"); | |
} | |
class AbortException : Exception { this() { super("Bad mojo. "); } } | |
string download_first(string url, string* redir = null) { | |
return url.download(true); | |
/*string res; | |
try url.streamload((string chunk) { | |
res ~= chunk; | |
if (res.length > 50_000) | |
throw new AbortException; | |
}, redir); | |
catch (AbortException) return res; | |
return res;*/ // Geez. | |
} | |
ubyte hexdecode(char c) { | |
if (c >= '0' && c <= '9') return c - '0'; | |
if (c >= 'a' && c <= 'f') return c - 'a' + 10; | |
if (c >= 'A' && c <= 'F') return c - 'A' + 10; | |
throw new Exception("'"~c~"' is not a hex! >_<"); | |
} | |
ubyte hexdecode(string s) { | |
assert(s.length == 2); | |
return (hexdecode(s[0]) << 4) + hexdecode(s[1]); | |
} | |
char hexencode(ubyte ub) { | |
if (ub >= 16) throw new Exception(Format(ub, " too large to encode in single char! ")); | |
if (ub >= 10) return 'a' + ub - 10; | |
return '0' + ub; | |
} | |
string unescape(string s) { | |
string res; | |
while (s.length) { | |
auto ch = s.take(); | |
if (ch != '%') res ~= ch; | |
else res ~= cast(char) s.take(2).hexdecode(); | |
} | |
return res; | |
} | |
// Happily ripping off http://userscripts.org/scripts/review/44261 .. | |
string decrypt(string un, int k1, int k2) { | |
string bin; | |
foreach (ch; un) { | |
auto ub = hexdecode(ch); | |
foreach (b; [8, 4, 2, 1]) { | |
if (ub & b) bin ~= "1"; else bin ~= "0"; | |
ub &= ~b; | |
} | |
} | |
auto keys = new ubyte[384]; | |
foreach (ref val; keys) { | |
k1 = (k1 * 11 + 77213) % 81371; | |
k2 = (k2 * 17 + 92717) % 192811; | |
val = (k1 + k2) % 128; | |
} | |
for (int i = 256; i >= 0; --i) { | |
swap(bin[keys[i]], bin[i%128]); | |
} | |
for (int i = 0; i < 128; ++i) { | |
bin[i] ^= keys[i+256] & 1; | |
} | |
string res; | |
for (int i = 0; i < bin.length; i += 4) { | |
auto data = bin[i .. i+4]; | |
int val = ((data[0] == '1') << 3) + ((data[1] == '1') << 2) + ((data[2] == '1') << 1) + (data[3] == '1'); | |
res ~= hexencode(val); | |
} | |
return res; | |
} | |
bool works(void delegate()[] dgs...) { | |
try foreach (dg; dgs) dg(); | |
catch (Exception ex) return false; | |
return true; | |
} | |
void attempt(lazy void v) { | |
try v(); | |
catch (Exception ex) { } // So sue me. | |
} | |
bool pwn_site(string url, ref string[string] urls, ref string src) { | |
if (url.find("youtube.com") != -1) { | |
string[int] fmt_map; | |
bool[int] checked; | |
void check(string url, int fmt = 0) { | |
if (fmt) { | |
if (fmt in checked) return; | |
checked[fmt] = true; | |
url ~= Format("&fmt=", fmt); | |
} | |
auto src = url.download(); | |
auto swfhtml = src.between("swfHTML", "</script"); | |
auto str = swfhtml.between("? \"", "noembed>\";").replace("\\\"", "\"").unescape(); | |
auto videos = str.between("fmt_url_map=", "&csi_page_type=").split(",") /map/ (string s) { return stuple(s.slice("|").atoi(), s); }; | |
bool matched; | |
foreach (video; videos) { | |
if (video._0 == fmt) | |
matched = true; | |
fmt_map[video._0] = video._1; | |
checked[video._0] = true; | |
} | |
} | |
check(url); | |
foreach (type; [5, 17, 18, 22, 34, 35, 37, 43, 45]) { | |
if (type in checked) continue; | |
check(url, type); | |
} | |
string[int] id_map = [ | |
5: "flv/h.263 240p"[], 34: "flv/h.264 360p", 35: "flv/h.264 480p", 22: "mp4 720p", 37: "mp4 1080p", 18: "mp4 480x360", | |
43: "WebM 480p", 45: "WebM 720p", 17: "3gp 176x144", | |
0: "flv/h.263 320x240", 6: "flv/h.263 480x360", 13: "3gp 176x144" | |
]; | |
foreach (key, value; fmt_map) { | |
if (key in id_map) | |
urls[id_map[key]] = value.mktiny(); | |
else | |
urls[Format(key)] = value.mktiny(); | |
} | |
return true; | |
} | |
if (url.find("vimeo.com") != -1) { | |
auto id = url.between("vimeo.com/", ""); | |
if (!id.atoi()) return false; | |
auto tmp = ("http://www.vimeo.com/moogaloop/load/clip:"~id).download(); | |
auto | |
sign = tmp.between("request_signature>", "<"), | |
expiry = tmp.between("signature_expires>", "<"); | |
auto | |
murl = "http://www.vimeo.com/moogaloop/play/clip:"~id~"/"~sign~"/"~expiry~"/?q=!Q!&type=local&embed_location=", | |
hd = murl.replace("!Q!", "hd"), | |
sd = murl.replace("!Q!", "sd"); | |
if (works(hd.download_first())) | |
urls["HD"] = hd.mktiny(); | |
// if (works(sd.download_first())) // assumed to always work | |
urls["SD"] = sd.mktiny(); | |
return true; | |
} | |
bool res; | |
if (!src) src = url.download(); | |
auto megacheck = src; | |
if (auto codes = megacheck.between("write(unescape('", "'")) { | |
// Tricky bastards. | |
megacheck = codes.unescape(); | |
} | |
void do_megavideo(string hit) { | |
string redir; | |
hit.download_first(&redir); | |
if (redir) hit = redir ~ "&"; // hackaround: make .between(foo, "&") work | |
else hit ~= "&"; | |
auto link = hit.between("v=", "&"); | |
if (link.find("\"") != -1) link = link.between("", "\""); | |
auto sesame = ("http://www.megavideo.com/xml/videolink.php?v="~link~"&id=").download(); | |
auto diamond = sesame.between("un=\"", "\""), k1 = sesame.between("k1=\"", "\"").atoi(), k2 = sesame.between("k2=\"", "\"").atoi(); | |
attempt = urls["mp4"] = ("http://www"~sesame.between(" s=\"", "\"")~".megavideo.com/files/"~diamond.decrypt(k1, k2)~"/").mktiny(); | |
res = true; | |
} | |
if (url.find("megavideo.com") != -1) do_megavideo(url); | |
if (auto hit = megacheck.between("\"", "megavideo.com", "\"")) do_megavideo(hit); | |
if (auto hit = megacheck.between("'", "megavideo.com", "'")) do_megavideo(hit); | |
if (auto hit = src.between("value='", "www.yuvitube.com", "'").between("vid=", "")) { | |
urls["yuvitube/flv"] = Format("http://www.yuvitube.com/uploads/flvideo/", hit, ".flv"); | |
res = true; | |
} | |
if (auto hit = src.between("src='", "movshare.net", "'")) { | |
auto mshem = hit.download(); | |
urls["movshare/divx"] = mshem.between("<embed", "video/divx", ">").between("src=\"", "\"").mktiny(); | |
res = true; | |
} | |
if (auto hit = src.between("value=", "thexvid.com", " ")) { | |
string reloc; | |
hit.download_first(&reloc); | |
reloc = reloc.unescape().between("url:'", "flvideo", "'"); | |
urls["thexvid/flv"] = reloc.mktiny(); | |
res = true; | |
} | |
if (auto hit = src.between("src=\"", "joecool6101.com", "\"")) { // iframe .. | |
url = hit; | |
src = url.download(); | |
} | |
if (auto hit = src.between("src=\"", "zshare.net/videoplayer", "\"")) { // another iframe .. | |
url = hit; | |
src = url.download(); | |
} | |
return res; | |
} | |
import tools.downloader; | |
void google(Query query) { | |
auto q = query.param.googleQuery(); | |
query.answer(q ~ " " ~ q.getTitle()); | |
} | |
import readback; | |
void gun(Query query) { | |
auto gs = read_cmd("echo $(fwoosh/fwoosh)"); | |
query.answer(gs); | |
} | |
void fwoosh_upd(Query query) { | |
query.answer(read_cmd("cd fwoosh; ./scriptfile.sbcl |wc -l")); | |
} | |
void imitate(Query query) { | |
if (query.channel == "#lesswrong") return; | |
foreach (ch; query.param) { | |
if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9') continue; | |
if ("_-.[]|".find(ch) != -1) continue; | |
query.answer("invalid character in nickname"); | |
return; | |
} | |
auto logfile = "../../logs/####_"~query.channel~".log"; | |
bool test(string network) { | |
auto l2 = logfile.replace("####", network); | |
if (l2.exists()) { logfile = l2; return true; } | |
return false; | |
} | |
if (!test("freenode") && !test("esper") && !test("rizon") && !test("stormbit") | |
&& !test("synirc")) { | |
query.answer("sorry, no channel log found for ", query.channel); | |
return; | |
} | |
query.say(read_cmd("./imitate "~logfile~" \""~query.param~"\"")); | |
} | |
import std.utf; | |
string htmlFormat(string s) { | |
dstring res; | |
s.glomp_parse([ | |
"<sup"[]: (string pre, ref string post) { res ~= pre.toUTF32(); post.slice(">"); }, | |
"</sup>": (string pre, ref string post) { | |
bool onlyAsciiHighables = true, onlyUnicodeHighables = true; | |
foreach (ch; pre) { | |
if (ch < '1' || ch > '3') onlyAsciiHighables = false; | |
if (ch /notin/ Range['0' .. '9'].endIncl && " +-=[]()Nn".find(ch) == -1) onlyUnicodeHighables = false; | |
} | |
if (onlyAsciiHighables) { | |
foreach (ch; pre) { | |
switch (ch) { | |
case '1': res ~= '¹'; break; | |
case '2': res ~= '²'; break; | |
case '3': res ~= '³'; break; | |
default: assert(false); | |
} | |
} | |
} else if (onlyUnicodeHighables) { | |
foreach (ch; pre) { | |
if (ch in Range['4' .. '9'].endIncl) { res ~= '⁴' + (ch - '4'); continue; } | |
switch (ch) { | |
case '0': res ~= '⁰'; break; | |
case '1': res ~= '¹'; break; | |
case '2': res ~= '²'; break; | |
case '3': res ~= '³'; break; | |
case ' ': res ~= ' '; break; | |
case '+': res ~= '⁺'; break; | |
case '-': res ~= '⁻'; break; | |
case '=': res ~= '⁼'; break; | |
case '[', '(': res ~= '⁽'; break; | |
case ']', ')': res ~= '⁾'; break; | |
case 'N', 'n': res ~= 'ⁿ'; break; | |
default: assert(false); | |
} | |
} | |
} else res ~= ("^"~pre~"^").toUTF32(); | |
}, | |
"<sub": (string pre, ref string post) { res ~= pre.toUTF32(); post.slice(">"); }, | |
"</sub>": (string pre, ref string post) { | |
bool onlyUnicodeLowables = true; | |
foreach (ch; pre) { | |
if (ch /notin/ Range['0' .. '9'].endIncl && " +-=[]()AaEeOoXx".find(ch) == -1) onlyUnicodeLowables = false; | |
} | |
if (onlyUnicodeLowables) { | |
foreach (ch; pre) { | |
if (ch in Range['0' .. '9'].endIncl) { res ~= "₀₁₂₃₄₅₆₇₈₉"w[ch - '0']; continue; } | |
switch (ch) { | |
case ' ': res ~= ' '; break; | |
case '+': res ~= '₊'; break; | |
case '-': res ~= '₋'; break; | |
case '=': res ~= '₌'; break; | |
case '[', '(': res ~= '₍'; break; | |
case ']', ')': res ~= '₎'; break; | |
case 'A', 'a': res ~= 'ₐ'; break; | |
case 'E', 'e': res ~= 'ₑ'; break; | |
case 'O', 'o': res ~= 'ₒ'; break; | |
case 'X', 'x': res ~= 'ₓ'; break; | |
default: assert(false); | |
} | |
} | |
} else res ~= ("_"~pre~"_").toUTF32(); | |
}, | |
"<font": (string pre, ref string post) { | |
res ~= pre.toUTF32(); | |
post.slice(">"); | |
}, | |
"</font>": (string pre, ref string post) { res ~= pre.toUTF32(); }, | |
"<img": (string pre, ref string post) { | |
res ~= pre.toUTF32(); | |
post.slice(">"); | |
}, | |
"</img>": (string pre, ref string post) { } | |
], (string rest) { res ~= rest.toUTF32(); }); | |
return res.toUTF8().replace("×", "×").replace(" ", " "); | |
} | |
void gcalc(Query query) { | |
auto backup = agent_override; | |
scope(exit) agent_override = backup; | |
agent_override = "Mozilla/5.0 (X11; Linux x86_64; rv:5.0) Gecko/20100101 Firefox/5.0"; | |
auto url = "http://www.google.com/complete/search?output=toolbar&hl=en&q="~query.param.urlencode(); | |
auto page = url.download(); | |
auto res = page.between("data=\"", "\"/>"); | |
if (!res) return query.answer("No GCalc result."); | |
return query.answer("<b>"~res.htmlFormat()~"</b>"); | |
} | |
void wp(Query query) { | |
query.param = "site:wikipedia.org "~query.param; | |
return google(query); | |
} | |
void lw(Query query) { | |
query.param = "site:lesswrong.com "~query.param; | |
return google(query); | |
} | |
void trope(Query query) { | |
auto words = query.param.split(" "); | |
int wordcount; string[] altlist; | |
auto backup = words; | |
foreach (word; words) { | |
word = word.strip(); | |
if (!word.length) continue; | |
if (wordcount >= 1) { if (word.length && word[0] in Range['a' .. 'z'].endIncl) { words = altlist; break; } } | |
else if (word.length && word[0] in Range['a' .. 'z'].endIncl) break; | |
wordcount ++; | |
altlist ~= word; | |
} | |
auto j = std.string.join(words, " ").strip(); | |
string q = query.param; | |
if (j.length) q = j; | |
string res; | |
const string exclude = "-intitle:\"Discussion\" -inurl:editors.php -inurl:inboundcount.php -inurl:posts.php"; | |
try { | |
if (j.length) throw new Exception("mew"); // lal | |
res = (exclude~" \"pmwiki.php/main/"~q~"\"").googleQuery("tvtropes.org"); | |
} catch (Exception ex) try { | |
sleep(2); | |
res = (exclude~" \""~q~"\"").googleQuery("tvtropes.org"); | |
} catch (Exception ex) { | |
sleep(2); | |
res = (exclude~" "~q).googleQuery("tvtropes.org"); | |
} | |
auto alt = res.between("", "?"); | |
query.answer(alt?alt:res); | |
} | |
void df(Query query) { | |
string q = query.param, res; | |
const string site = "dwarffortresswiki.org"; | |
try res = ("intitle:\""~q~"\" -intitle:Talk").googleQuery(site); | |
catch (Exception ex) try res = ("intitle:\""~q~"\" -inurl:Talk -inurl:User_talk").googleQuery(site); | |
catch (Exception ex) try res = ("\""~q~"\"").googleQuery(site); | |
catch (Exception ex) res = q.googleQuery(site); | |
query.answer(res); | |
} | |
import std.regexp: regex_sub = sub; | |
void tropesample(Query query) { | |
auto site = ("-discussion "~query.param).googleQuery("tvtropes.org").download(); | |
auto samples = site.between("<h2>", "").betweens("<li>", "</li>") | |
/map/ (string s) { | |
return s.regex_sub("<[^>]*>", "", "g"); | |
} /select/ (string s) { return s.length < 128; }; | |
if (!samples.length) return query.answer("No short samples found. "); | |
query.answer("Sample for ", site.between("<title>", " - "), ":", samples[rand() % $]); | |
} | |
void delegate(Query)[nickname] nexts; // trigger the next action for a nick | |
Object nexts_sync; | |
static this() { New(nexts_sync); } | |
string stripTags(string s) { | |
string res = ""; | |
bool inTag = false; | |
foreach (ch; s) { | |
if (inTag) { | |
if (ch == '>') inTag = false; | |
} else { | |
if (ch == '<') inTag = true; | |
else if (ch == '>') { | |
res = ""; | |
} else res ~= ch; | |
} | |
} | |
return res; | |
} | |
string cleanup(string s) { | |
string res = ""; | |
bool hadSpace = false; | |
foreach (ch; s) { | |
if (ch == '\n' || ch == '\t') continue; | |
if (hadSpace) { | |
if (ch != ' ') { | |
hadSpace = false; | |
res ~= ch; | |
} | |
} else { | |
if (ch == ' ') hadSpace = true; | |
res ~= ch; | |
} | |
} | |
return res; | |
} | |
class Formatter { | |
string buffer; | |
int maxcols; | |
void delegate(string) dg; | |
mixin This!("dg, maxcols=80"); | |
void flush() { dg(buffer); buffer = ""; } | |
string last_section; | |
import std.utf; | |
void append(string text, string sep="") { | |
auto test = text ~ buffer ~ sep; | |
if (test.toUTF32().length > maxcols) { | |
flush; | |
if (last_section.length) buffer ~= "(cont.) "; | |
} else if (last_section.length) buffer ~= sep; | |
buffer ~= text; | |
} | |
void print(string section, string info, string sep="") { | |
if (buffer.length) { | |
if (section == last_section) { | |
append(info, sep); | |
} else { | |
if (last_section.length) { | |
last_section = ""; | |
append(" | "); | |
} | |
append(section); | |
append(info); | |
last_section = section; | |
} | |
} else { | |
if (section.length) append(section); | |
last_section = section; | |
append(info); | |
} | |
} | |
} | |
void anidb(Query query, int offset, string pagebuf, bool rewrote = false) { | |
string address = "http://anidb.net/perl-bin/animedb.pl?show=animelist&adb.search="~query.param.urlencode(); | |
string redirect, page; | |
if (!query.param.strip().length && !offset) { | |
query.answer("anidb <search term>. Performs an AniDB lookup."); | |
return; | |
} | |
if (atoi(query.param) && !offset) { | |
redirect = "http://anidb.net/perl-bin/animedb.pl?show=anime&aid="~toString(atoi(query.param)); /* sanitize */ | |
} // else if (pagebuf.length) page = pagebuf; else page = address.download(&redirect); | |
if (redirect.length) { | |
if (!redirect.startsWith("http://")) redirect = "http://"~redirect; | |
page = redirect.download(); | |
auto fm = new Formatter((string s) { query.answer(s); }); | |
scope(exit) fm.flush(); | |
auto offc = page.betweens("Official Title</th>", "</td>") | |
/map/ (string s) { return s.between("<label>", "</label>"); }; | |
string type = page.between("Type</th", "</td").between("value\">", ""); | |
string year = page.between("Year</th", "</td").between("value\">", ""); | |
string cats = page.between("Categories</a></th", "- <a").stripTags().cleanup(); | |
string rats = page.between("Rating</th>", "</td").stripTags().cleanup(); | |
fm.print("", page.between("Main Title</th", "<a").between("value\">", "\n")); | |
fm.print("", ": "); | |
if (offc.length) | |
foreach (o; offc) fm.print("official title ", o, "/"); | |
if (type.length) fm.print("Type: ", type); | |
if (year.length) fm.print("Year: ", year); | |
if (cats.length) fm.print("Categories: ", cats); | |
if (rats.length) fm.print("Rating: ", rats); | |
fm.print("", redirect); | |
} else { | |
auto entries = page.betweens("class=\"thumb anime\"", "</tr"); | |
int count = 3; | |
if (offset) { | |
entries = entries[offset .. $]; | |
address = ""; // shouldn't be needed | |
if (query.param.length) count = query.param.atoi(); | |
} | |
if (!entries.length) { | |
query.answer("no results. Sorry."); | |
return; | |
} | |
bool[int] already_seen; int doubles; | |
typeof(entries) n_entries; | |
foreach (i, entry; entries) { | |
auto name = entry.between("<a", ""); | |
if (name.find("<img") == -1) name = name.between(">", "</a>"); | |
else name = entry.between("<td", "").between("<a", "").between(">", "</a"); | |
/+if (!rewrote && !offset && name.tolower().startsWith(query.param.tolower())) { | |
/* Perfect match; rewrite */ | |
auto np = entry.between("href=\"", "\"").between("aid=", ""); | |
if (np.atoi()) { | |
auto q = query; | |
q.param = np; | |
return anidb(q, 0, null, true); | |
} | |
}+/ | |
auto id = entry.between("href=\"", "\"").between("aid=", "").atoi(); | |
if (id in already_seen) { doubles++; continue; } | |
already_seen[id] = true; | |
n_entries ~= entry; | |
} | |
entries = n_entries; | |
foreach (i, entry; entries) { | |
if (i >= count) break; | |
auto name = entry.between("<a", ""); | |
if (name.find("<img") == -1) name = name.between(">", "</a>"); | |
else name = entry.between("<td", "").between("<a", "").between(">", "</a"); | |
auto altname = entry.between("class=\"main\">", "<").strip(); | |
auto id = entry.between("href=\"", "\"").between("aid=", "").strip(); | |
query.answer(Format(i + 1 + offset, | |
"(", id, ") - ", name, | |
altname.length?" ":"", altname, | |
", rated ", entry.between("rating weighted\">", "\n").strip(), " weighted, ", entry.between("rating avg\">", "\n").strip(), " average", | |
", aired from ", entry.between("date airdate\">", "<").strip(), | |
" to ", entry.between("date enddate\">", "<").strip() | |
)); | |
} | |
if (entries.length > count) { | |
query.answer(Format(entries.length - count, " omitted.")); | |
synchronized (nexts_sync) nexts[query.name] = stuple(page, offset+count) /apply/ (string page, int offset, Query q) { | |
return anidb(q, offset, page, false); | |
}; | |
} | |
} | |
} | |
import tools.functional; | |
void decide(Query query) { | |
auto choices = query.param.split("|") /map/ &strip; | |
if (choices.length <= 1) query.answer("decide a|b|c -> a, or b, or c"); | |
else { | |
auto choice = choices[rand() % $]; | |
if (!choice.length) choice = ["What?"[], "Eh?", "Whuh?", " ... ?"][rand() % $]; | |
query.answer(choice); | |
} | |
} | |
import tools.time: sec; import tools.base: yield; | |
// waits for condition to become true | |
bool timeout(ref double left, lazy bool condition) { | |
auto start = sec(); | |
double elapsed() { return sec() - start; } | |
double end() { return start + left; } | |
scope(exit) left -= elapsed(); | |
logln("start: ", start, ", left: ", left, ", end ", end(), ", elapsed ", elapsed()); | |
while (sec() < end()) { | |
if (condition()) return false; | |
yield(); | |
} | |
return true; | |
} | |
MessageMultiChannel!(Query, false, false)[string] votes; | |
void vote(Query query) { | |
typeof(votes[""]) my_chan; | |
struct Alternative { | |
int count; | |
string name; | |
} | |
Alternative[] res; double secs; | |
synchronized(SyncObj!(votes)) { | |
if (auto ch = query.channel in votes) | |
return ch.put(query); | |
secs = query.param.slice(" ").atof(); | |
auto choices = query.param.split("|") /map/ &strip; | |
if (choices.length <= 1) return query.answer("vote <timeout> a|b|c"); | |
if (secs !>= 1) return query.answer("Timeout cannot be null or smaller than one."); | |
// if (secs > 600) return query.answer("Don't you think that's a tad excessive?"); | |
res.length = choices.length; | |
foreach (i, choice; choices) res[i].name = choice; | |
New(my_chan); | |
votes[query.channel] = my_chan; | |
} | |
string floatclean(float f) { | |
auto i = cast(int) (f * 100); | |
auto rest = i % 100; | |
return Format(i/100, ".", (rest<10)?"0":"", i%100); | |
} | |
string[] temp; | |
bool[string] voted; | |
foreach (i, vote; res) temp ~= Format(i+1, ": ", vote.name); | |
query.say("Channel vote! The choices are: ", temp, ". You have ", floatclean(secs), "s to decide. Go. "); | |
Query votequery; | |
auto start = sec(), total_secs = secs; // timeout modifies secs | |
double left() { return start - sec() + total_secs; } | |
void unref() { | |
synchronized(SyncObj!(votes)) votes.remove(query.channel); | |
} | |
while (!timeout(secs, my_chan.try_get(votequery))) { | |
if (auth(votequery) && votequery.param == "abort") { | |
unref; | |
return votequery.answer("admin override: vote aborted"); | |
} | |
auto index = votequery.param.atoi(); | |
if (!index) { | |
votequery.answer("Your choices are: ", temp, ". Please vote by using \"vote <number>\"."); | |
continue; | |
} | |
if (index > res.length) { | |
votequery.answer("That is not a valid choice."); | |
continue; | |
} | |
if (votequery.name in voted) { | |
votequery.answer("You have already voted!"); | |
continue; | |
} | |
voted[votequery.name] = true; | |
res[index-1].count++; | |
string[] nutemp; | |
foreach (vote; res) nutemp ~= Format(vote.name, ": ", vote.count); | |
votequery.say("Current score: ", nutemp, ". ", floatclean(left), " seconds left. "); | |
} | |
unref; | |
res = res /qsort/ ex!("a, b -> a.count < b.count"); | |
if (!res[$-1].count) { | |
return query.say("Time's up and nobody voted! Sorry. Better luck next time!"); | |
} | |
if (res.length > 1 && res[$-1].count == res[$-2].count) { | |
auto ties = res /select/ ex!("cmp -> x -> x.count == cmp")(res[$-1].count) /map/ ex!("x -> x.name"); | |
return query.say("Time's up aaand .. it's a tie between ", ties, " with ", res[$-1].count, " votes! Thanks for voting. "); | |
} else with (res[$-1]) | |
return query.say("Time's up! The winner is ", name, " with ", count, (count>1)?" votes":" vote", "! Thanks for voting. "); | |
} | |
MessageMultiChannel!(Query, false, false)[string] padgame; | |
// Thanks stacy! | |
string filterComments(string text) { | |
string res; | |
int nest_level = 0; | |
text.glomp_parse([ | |
"/*": (string pre, ref string post) { if (!nest_level) res ~= pre; nest_level ++; }, | |
"*/": (string pre, ref string post) { if (!nest_level) throw new Exception("Too many */"); nest_level --; } | |
], (string rest) { res ~= rest; }); | |
string res2; | |
foreach (i, line; res.split("\n")) { | |
if (i) res2 ~= "\n"; | |
res2 ~= line.cutOff("//"); | |
} | |
return res2; | |
} | |
Lock padlock; | |
static this() { New(padlock); } | |
string lastPadURL(string id) { | |
return IRCconfig.get!(string)(id, "lastpad", ""); | |
} | |
void setLastPadURL(string id, string v) { | |
IRCconfig.set(id, "lastpad", v); | |
} | |
void repad(Query query) { | |
auto id = query.connection.host ~ query.channel; | |
auto lpu = lastPadURL(id); | |
if (!lpu.length) return query.answer("No previous command recorded"); | |
Query foo = query; | |
foo.param = lpu ~ " " ~ query.param; | |
padfn(foo); | |
} | |
import pad.mainloop; | |
void padfn(Query query) { | |
typeof(padgame[""]) my_chan; | |
// auto mykey = (query.channel~"!"~cast(string)query.name).tolower(); | |
auto mykey = query.channel.tolower(); | |
query.param = query.param.replace("\x02", "").replace("\x1f", "").replace("\x16", ""); | |
string url; | |
bool inpmode; | |
query.param = query.param.strip(); | |
if (auto rest = query.param.startsWith("> ")) { query.param = rest.strip(); inpmode = true; } | |
bool abort; | |
padlock.Synchronized = { | |
if (auto ch = mykey in padgame) { | |
if (query.param.startsWith("http://" /or/ "gobby://")) { | |
auto q2 = query; q2.param = "forcequit"; | |
ch.put(q2); | |
// wait for other instance to forcequit | |
while (mykey in padgame) padlock.Unsynchronized = { slowyield(); }; | |
} else { | |
ch.put(query); | |
abort = true; return; | |
} | |
} | |
if (inpmode) { abort = true; return; }; // Do not give info if no game is running | |
url = query.param.slice(" "); | |
if (!url.length) { abort = true; return query.answer("Paste Adventure: ", CTRL(query.channel), "pad <pastebin url> <initial commands>"); } | |
New(my_chan); | |
padgame[mykey] = my_chan; | |
}; | |
if (abort) return; | |
setLastPadURL(query.connection.host ~ query.channel, url); | |
scope(exit) padlock.Synchronized = { padgame.remove(mykey); }; | |
runLoop(url, query.param, (string s) { | |
auto chunks = s.split("<br>"); | |
foreach (ref chunk; chunks) { | |
if (chunk.length > 256) chunk = chunk[0 .. 256] ~ "[...] EXCESS HERE"; | |
} | |
if (!chunks.length) return query.answer("-- "); | |
query.answer(chunks[0]); | |
chunks[1 .. $] /map/ &query.say!(string); | |
}, { query = my_chan.get(); return query.param; }); | |
} | |
MessageMultiChannel!(Query, false, false)[string] rps_game; | |
void rps(Query query) { | |
typeof(rps_game[""]) my_chan; | |
string[] players; | |
auto id = query.channel ~ " _ " ~ cast(string) query.name; | |
synchronized(SyncObj!(rps_game)) { | |
if (auto ch = id in rps_game) return ch.put(query); | |
players = [cast(string) query.name, query.param.strip()].dup; | |
if (players[0] == players[1]) return query.answer("Your other self says hi! Also don't worry about the dead body. "); | |
if (players[1].find(" ") != -1 || !query.connection.exists(cast(nickname) players[1])) | |
return query.answer("rps <other player>"); | |
New(my_chan); | |
auto id1 = query.channel ~ " _ " ~ players[0], id2 = query.channel ~ " _ " ~ players[1]; | |
if (id1 in rps_game) return query.answer(players[0], " is already playing!"); | |
if (id2 in rps_game) return query.answer(players[1], " is already playing!"); | |
rps_game[id1] = rps_game[id2] = my_chan; | |
} | |
int[] genMap() { | |
auto map = ([0, 1, 2]).dup; | |
swap(map[0], map[rand() % 3]); | |
swap(map[1], map[1+(rand() % 2)]); | |
return map; | |
} | |
auto map1 = genMap(), map2 = genMap(); | |
auto q1 = query, q2 = query; q1.name = cast(nickname) players[0]; q2.name = cast(nickname) players[1]; | |
query.say("A game of RPS between ", q1.name, " and ", q2.name, " is running!"); | |
q1.notice(Format("Your parameters: ", map1[0], ": Rock |", map1[1], ": Paper |", map1[2], ": Scissors")); | |
q2.notice(Format("Your parameters: ", map2[0], ": Rock |", map2[1], ": Paper |", map2[2], ": Scissors")); | |
Query getOnce() { | |
while (true) { | |
auto merp = my_chan.get(); | |
if (merp.param.atoi() /notin/ Range[0..3]) continue; | |
synchronized(SyncObj!(rps_game)) { | |
auto id = merp.channel ~ " _ " ~ cast(string) merp.name; | |
if (id in rps_game) { rps_game.remove(id); return merp; } | |
} | |
} | |
} | |
auto move1 = getOnce(), move2 = getOnce(); | |
logln("Got both merp!"); | |
if (cast(string) move1.name == players[1]) swap(move1, move2); | |
int resolve(int player, int move) { | |
auto map = player?map2:map1; | |
foreach (i, entry; map) if (entry == move) return i; | |
throw new Exception("Brain breakage: Invalid move"); | |
} | |
auto id1 = resolve(0, move1.param.atoi()), id2 = resolve(1, move2.param.atoi()); | |
auto choices = ["Rock"[], "Paper", "Scissors"], challenge = Format(choices[id1], " vs. ", choices[id2]); | |
if (id1 == id2) return query.say(challenge, ": It's a tie!"); | |
if (((id1 + 1) % 3) == id2) return query.say(challenge, ": ", players[1], " wins!"); | |
return query.say(challenge, ": ", players[0], " wins!"); | |
} | |
void join(Query query) { | |
if (auth(query)) { | |
with (query.connection) join(query.param, defaultChanHandler); | |
} else query.answer("unauthorized join: ", query.param); | |
} | |
void part(Query query) { | |
if (auth(query)) { | |
query.connection.part(query.param); | |
} else query.answer("unauthorized part: ", query.param); | |
} | |
void more(Query query) { | |
synchronized (nexts_sync) { | |
if (!(query.name in nexts)) { | |
query.answer("I don't know more."); | |
} else { | |
auto task = nexts[query.name]; | |
nexts.remove(query.name); | |
check_ex(task /fix/ query, query); | |
} | |
} | |
} | |
void say(Query query) { | |
if (auto rest = query.param.startsWith("\x01")) { | |
if (!rest.startsWith("ACTION ")) { | |
query.answer("arbitrary ctcp denied!"); | |
return; | |
} | |
} | |
bool authd(nickname n) { | |
return query.connection.registered(n) && ((n == root_user) || (n == cast(nickname) "Lazy_Zefiris") || (n == cast(nickname) "Meow_Zefiris")); | |
} | |
query.param = query.param.strip(); | |
if (query.param.startsWith("!" /or/ "]" /or/ "ljrbot")) { | |
if (!authd(query.name)) { | |
query.answer("You can't make me do that!"); | |
return; | |
} | |
} | |
auto cutpos = query.param.ifind("candlejack"); | |
if (cutpos != -1) { | |
cutpos += 10; | |
if (cutpos < query.param.length) { | |
cutpos += rand() % (query.param.length - cutpos); | |
} | |
query.param = query.param[0 .. cutpos] ~ "-"; | |
} | |
query.say("<b></b>", query.param.htmlFormat()); | |
} | |
class GeoIP { | |
import std.file; | |
static { | |
struct entry { | |
uint start, end; | |
string country; | |
} | |
entry[] list; | |
string lookup(uint what) { | |
auto res = _lookup(what); | |
if (!res.strip().length) | |
throw new Exception(Format("No search hit for ", what)); | |
return res; | |
} | |
string _lookup(uint what) { | |
auto range = list; | |
while (range.length > 1) { | |
auto center = range[$/2]; | |
if (what >= center.start && what <= center.end) return center.country; | |
if (what >= center.start) range = range[$/2 .. $]; | |
else range = range[0 .. $/2]; | |
} | |
if (what < range[0].start || what > range[0].end) return "unknown"; | |
return range[0].country; | |
} | |
} | |
static this() { | |
std.gc.disable(); | |
scope(exit) std.gc.enable(); // what | |
auto arr = std.string.split("/home/mathis/GeoIPCountryWhois.csv".read().castLike("").dup, "\n"); | |
arr /map/ (string line) { | |
line = line.strip(); | |
if (!line.length) return; | |
auto parts = line[1 .. $-1].split("\",\""); // remove ""s | |
auto ip = parts[0]; | |
auto ip_end = parts[1]; | |
entry e; | |
// why parse IPs? Let the system do it for us. | |
e.start = (new ActuallyWorkingInternetAddress(ip, 0)).addr(); | |
e.end = (new ActuallyWorkingInternetAddress(ip_end, 0)).addr(); | |
e.country = parts[$-1]; | |
list ~= e; // yes they're ordered | |
}; | |
} | |
} | |
mixin(ExSource); | |
void cstats(Query query) { | |
auto stats = &(new Stuple!(int[string]))._0; | |
auto namestats = &(new Stuple!(string[string]))._0; | |
tp.addTask(&query.connection.whoare /fix/ stuple(query.channel, stuple(stats, namestats, query) /apply/ | |
(ref int[string] stats, ref string[string] namestats, Query query, string user, string host) { | |
string country; | |
if (host.endsWith(".hmsk")) return;; | |
auto parts = host.split("."); | |
while (parts.length > 1) { | |
try { | |
auto addr = new ActuallyWorkingInternetAddress(std.string.join(parts, "."), 0); | |
auto ip = addr.addr(); | |
country = GeoIP.lookup(ip); | |
break; | |
} catch (Exception ex) { /*query.say("error: ", ex);*/ parts = parts[1 .. $]; } | |
} | |
if (!country) country = "unknown"; | |
// if (!country) return; | |
synchronized { | |
if (country /notin/ stats) stats[country] = 1; | |
else stats[country] = stats[country] + 1; | |
auto tmp = user; | |
if (tmp.length > 1) tmp = tmp[0] ~ "" ~ tmp[1 .. $]; | |
// if (tmp.length > 1) tmp = tmp[0] ~ "␣" ~ tmp[1 .. $]; | |
if (country /notin/ namestats) namestats[country] = tmp; | |
else namestats[country] ~= ", " ~ tmp; | |
} | |
}, stuple(stats, namestats, query) /apply/ (ref int[string] stats, ref string[string] namestats, Query query) { | |
if (!stats.length) return query.answer("No country stats available. This is probably due to hostmasking. "); | |
auto rstats = invert(stats), top = rstats.keys.sort; | |
string[][int] res; | |
if (auto nsp = rstats[top[$-1]][0] in namestats) { | |
if ((*nsp).length > 64) | |
(*nsp) = "rest"; | |
} | |
foreach (key, list; rstats) { | |
foreach (country; list) { | |
res[key] ~= Format(country, " (", namestats[country], ")"); | |
} | |
} | |
if (top.length > 10) top.length = 10; | |
query.answer("Country stats: ", top /map/ ex!("r -> b -> stuple(b, r[b])")(res)); | |
})); | |
} | |
void cfind(Query query) { | |
if (!query.param.strip().length) | |
return query.answer("Please supply a filter criterium! "); | |
auto stats = &(new Stuple!(string[][string]))._0; | |
tp.addTask(&query.connection.whoare /fix/ stuple(query.channel, stats /apply/ (ref string[][string] stats, string user, string host) { | |
auto parts = host.split("."); | |
auto domain = std.string.join(parts[1 .. $], "."); | |
if (!domain) return; | |
synchronized { | |
stats[domain] ~= user; | |
} | |
}, stuple(stats, query) /apply/ (ref string[][string] stats, Query query) { | |
bool found; | |
string[][string] mapping; | |
foreach (key, value; stats) { | |
if (key.find(query.param.strip()) != -1) { | |
mapping[key] = value; | |
found = true; | |
} | |
} | |
query.answer("Result: ", mapping); | |
if (found) return; | |
query.answer("No matches. "); | |
})); | |
} | |
import tools.ini; | |
iniFile wbfile, IRCconfig, seenfile, notefile, markovfile, banlist; | |
static this() { | |
New(wbfile, "wb.txt"); | |
New(IRCconfig, "idc.ini"); | |
New(seenfile, "seen.txt"); | |
New(notefile, "notice.txt"); | |
New(markovfile, "markov.dat"); | |
New(banlist, "bans.txt"); | |
} | |
bool isBanned(hostmask host) { | |
auto list = banlist.get!(Stuple!(string, typeof(µsec()))[])("global", "matches", null); | |
auto time = µsec(); | |
int i; | |
scope(exit) banlist.set("global", "matches", list); | |
while (i < list.length) { | |
auto entry = list[i]; | |
if (time > entry._1) { | |
list = list[0..i] ~ list[i+1..$]; | |
continue; | |
} | |
i++; | |
if ((cast(string) host).find(entry._0) != -1) return true; | |
} | |
return false; | |
} | |
void ban(string match, typeof(µsec()) until) { | |
auto list = banlist.get!(Stuple!(string, typeof(µsec()))[])("global", "matches", null); | |
list ~= stuple(match, until); | |
banlist.set("global", "matches", list); | |
} | |
void do_ban(Query query) { | |
if (!auth(query)) return query.answer("You are not authorized! "); | |
if (!query.connection.registered(query.name)) return query.answer("You are not registered! "); | |
string timestr = "1h"; | |
void stripqp() { query.param = query.param.strip(); } | |
stripqp; | |
auto match = query.param.slice(" "); | |
stripqp; | |
if (auto t = query.param.startsWith("for ")) timestr = t; | |
auto until = µsec() + decodeTime(timestr); | |
query.answer(match, " has been banned for ", timestr, ". "); | |
ban(match, until); | |
} | |
import std.stream; | |
File[string] readers, writers; | |
void bridge(Query query) { | |
if (!auth(query)) return query.answer("You are not authorized to set up a bridge!"); | |
auto params = query.param.split(" "); | |
if (!query.param.length || params.length != 4) return query.answer("Usage: bridge <channel> <order [first/second]> <read pipe> <write pipe>"); | |
auto ch = params[0], order = params[1]; | |
query.channel = ch; | |
File bridge_read; | |
void setupRead() { | |
// query.say("Setting up read pipe on |", params[2], "|"); | |
bridge_read = new File(params[2], FileMode.In); | |
readers[ch] = bridge_read; | |
} | |
void setupWrite() { | |
// query.say("Setting up write pipe on |", params[3], "|"); | |
writers[ch] = new File(params[3], FileMode.Out); | |
} | |
if (order == "first") { | |
setupWrite(); | |
setupRead(); | |
} else { | |
setupRead(); | |
setupWrite(); | |
} | |
query.say("Ready. "); | |
while (true) { | |
auto line = bridge_read.readLine(); | |
query.say(line); | |
} | |
} | |
void listen(Query query) { | |
if (!auth(query)) return query.answer("You are not authorized to set up a listen pipe! "); | |
auto file = new File(query.param, FileMode.In); | |
readers["<main>"] = file; | |
while (true) { | |
auto cmd = file.readLine(); | |
query.connection.raw_sendln(IRCFormat(cmd)); | |
} | |
} | |
void raw(Query query) { | |
if (!auth(query)) return query.answer("You are not authorized to use raw! "); | |
query.connection.raw_sendln(query.param.strip()); | |
} | |
void wbOnJoin(Query query) { | |
// sleep(1); | |
// auto q2 = query; | |
// if (q2.channel == "#tropers") q2.name = cast(nickname) q2.channel; | |
// q2.notice("No it hasn't. STFU GreetBot you tard. "); | |
// query.answer(Format("wbOnJoin(", query, ")")); | |
if (query.channel == "#yackfest" /or/ "#sting" /or/ "#pony" /or/ "#trashheap" /or/ "#archlinux" /or/ "#lesswrong") return; | |
auto ch = query.channel.replace("#", ""), name = cast(string) query.name; | |
if (!wbfile.has(ch.clean(), name)) return; | |
auto wbs = wbfile.get!(string[])(ch.clean(), name, null); | |
if (!wbs.length) return; | |
int rsel = rand() % wbs.length; | |
query.act(Format("welcomes ", name, " (", rsel+1, " / ", wbs.length, "): <u>", wbs[rsel], "</u>")); | |
} | |
// note /ignore | |
bool dropnote(nickname nameto, string name, typeof(µsec()) when, string msg) { | |
auto namefrom = msg.between("", " left"); | |
if (namefrom == "Cesar" && nameto == cast(nickname) "zorua") return true; | |
return false; | |
} | |
void onFind(IRCconn ic, string channel, hostmask host, bool wasJoin) { | |
auto q = Query(ic, .nick(host), channel), name = host.nick(); | |
// if (wasJoin) return; | |
// q.say("TEST: name ", name, ", host ", host, ", channel ", channel); | |
synchronized(notefile) { | |
name = cast(nickname) name.tolower(); | |
bool isTime(string name, typeof(µsec()) when, ref string msg) { | |
if (auto rest = msg.startsWith("TIMED ")) { | |
auto num = atol(rest.between("", " ")); | |
if (µsec() > num) { | |
msg = rest.between(" ", "").strip(); | |
return true; | |
} else return false; | |
} else return true; | |
} | |
auto notes = notefile.get!(Stuple!(string, typeof(µsec()), string)[])(channel.clean(), name, null); | |
bool onlyNotify = wasJoin; | |
if (notefile.has(channel.clean(), name)) { | |
typeof(notes) leftnotes, todonotes; | |
foreach (note; notes) | |
if (!isTime(note.tupleof)) leftnotes ~= note; | |
else todonotes ~= note; | |
notes = todonotes; | |
// don't remove notes until we actually deliver them | |
if (!onlyNotify) { | |
if (!leftnotes.length) notefile.del(channel.clean(), name); | |
else notefile.set(channel.clean(), name, leftnotes); | |
} | |
} | |
// else q.say(": not in file"); | |
auto privnotes = notefile.get!(Stuple!(string, typeof(µsec()), string)[])("global", name, null); | |
if (notefile.has("global", name)) { | |
typeof(notes) leftnotes, todonotes; | |
foreach (note; privnotes) | |
if (!isTime(note.tupleof)) leftnotes ~= note; | |
else todonotes ~= note; | |
privnotes = todonotes; | |
if (!onlyNotify) { | |
if (!leftnotes.length) notefile.del("global", name); | |
else notefile.set("global", name, leftnotes); | |
} | |
} | |
string countercheck(string msg) { | |
if (auto rest = msg.startsWith("CHECKED ")) { | |
msg = rest; | |
auto name = msg.between("", " left"); | |
auto notes = notefile.get!(Stuple!(string, typeof(µsec()), string)[])("global", name, null); | |
notes ~= stuple(cast(string) name.tolower(), µsec(), Format("Your note to ", .nick(host), " was delivered $WHEN. ")); | |
notefile.set("global", name, notes); | |
} | |
return msg; | |
} | |
int notecount; | |
foreach (note; notes) if (!dropnote(name, note.tupleof)) { | |
auto fun = (string s) { q.answer(s); }; | |
if (q.channel == "#tgchan") fun = (string s) { q.notice(s); }; | |
if (onlyNotify) fun = (string s) { notecount++; }; | |
fun(countercheck(note._2).replace("$WHEN", timediff(µsec() - note._1))); | |
} | |
foreach (note; privnotes) if (!dropnote(name, note.tupleof)) { | |
if (onlyNotify) notecount++; | |
else q.notice(countercheck(note._2).replace("$WHEN", timediff(µsec() - note._1))); | |
} | |
if (onlyNotify) { | |
if (!notecount) { } | |
else if (notecount == 1) q.notice("You have 1 waiting note ("~channel~")."); | |
else q.notice(Format("You have ", notecount, " waiting notes ("~channel~").")); | |
} | |
} | |
} | |
void onNickChange(IRCconn ic, string channel, hostmask from, hostmask to) { | |
seenfile.set(channel.clean(), (cast(string) .nick(from)).tolower(), stuple("\x00NICK "~cast(string) .nick(to), µsec())); | |
} | |
void dbg(Query q) { | |
q.notice("This does nothing currently. "); | |
return; | |
// q.notice("Users: ", q.connection.users); | |
} | |
long timeparse(ref string code) { | |
long res = µsec(); | |
bool acceptNumber(ref string s, out int i) { | |
string numeral; | |
auto s2 = s.strip(); | |
while (s2.length && s2[0] >= '0' && s2[0] <= '9') { | |
numeral ~= s2[0]; | |
s2 = s2[1..$]; | |
} | |
if (!numeral.length) return false; | |
s = s2; | |
i = atoi(numeral); | |
return true; | |
} | |
bool accept(ref string s, string t) { | |
auto s2 = s.strip(); | |
if (auto rest = s2.startsWith(t)) { | |
s = rest; | |
return true; | |
} | |
return false; | |
} | |
void acceptCode(string id, string id2, long scale) { | |
auto c2 = code.strip(); | |
int num; | |
if (!acceptNumber(c2, num) || ( | |
!accept(c2, id) && !accept(c2, id2) && !accept(c2, id2~"s"))) | |
return; | |
code = c2.strip(); | |
res += cast(long) num * scale; | |
} | |
acceptCode("y", "year", 365L*24L*3600L*1000000L); | |
acceptCode("t", "month", 31L*24L*3600L*1000000L); | |
acceptCode("w", "week", 7L*24L*3600L*1000000L); | |
acceptCode("d", "day", 24L*3600L*1000000L); | |
acceptCode("h", "hour", 3600L*1000000L); | |
acceptCode("m", "minute", 60L*1000000L); | |
acceptCode("s", "second", 1000000L); | |
if (!res) throw new Exception("Unknown time code at "~code); | |
return res; | |
} | |
void addnote(Query q) { | |
// if (q.channel == "#sting") return; | |
bool global, check, checked; string timeinfo; | |
if (auto rest = q.param.startsWith("global ")) { | |
q.param = rest; | |
global = true; | |
} | |
if (auto rest = q.param.startsWith("/checked ")) { | |
q.param = rest; | |
checked = true; | |
} | |
if (auto rest = q.param.startsWith("check")) { | |
q.param = rest; | |
check = true; | |
} | |
if (!q.channel.startsWith("#")) global = true; // privmsg notes are always global | |
if (!q.param.length && !check) return q.answer("Usage: note <target name> <message>"); | |
// if (!q.connection.registered(q.name)) { | |
// return q.answer("You have to be registered to do that! "); | |
// } | |
q.param = q.param.strip(); | |
auto name = q.param.slice(" "); | |
/*if (q.channel in q.connection.users) | |
foreach (other; q.connection.users[q.channel]) { | |
if (name == other) return q.answer("But s/he's right here! "); | |
}*/ | |
if (auto rest = q.param.startsWith("in ")) { | |
timeinfo = Format("TIMED ", timeparse(rest), " "); | |
q.param = rest; | |
} | |
alias Stuple!(string, typeof(µsec()), string) NoteType; | |
synchronized(notefile) { | |
auto a = q.channel.clean(), b = name.tolower(); | |
if (global) a = "global"; | |
if (check) { | |
bool[string] open; | |
auto targets = notefile.section_map(a).keys; | |
foreach (target; targets) { | |
auto theirnotes = notefile.get!(NoteType[])(a, target, null); | |
foreach (note; theirnotes) { | |
if (note._0 == cast(string) q.name.tolower()) | |
open[target] = true; | |
} | |
} | |
return q.notice("You have open notes to: ", open.keys); | |
} | |
if (!q.param.strip().length) return q.answer("No message set! "); | |
start: | |
auto notes = notefile.get!(NoteType[])(a, b, null); | |
int count; | |
foreach (note; notes) | |
if (note._0 == cast(string) q.name.tolower()) count++; | |
if (a == "global") { | |
if (count >= 12) return q.answer("Cannot leave more than twelve global messages for somebody! "); | |
} else { | |
if (count >= 3 && a != "#teentropers" && a != "#127.0.0.1" && a != "#volt" && a != "#navajo" && q.name != root_user) | |
return q.answer("Cannot leave more than three messages for somebody! "); | |
} | |
notes ~= stuple(cast(string) q.name.tolower(), µsec(), Format(timeinfo, checked?"CHECKED ":"", q.name, " left a note $WHEN: ", q.param)); | |
notefile.set(a, b, notes); | |
// intercept | |
if (a == "global" && b == "ghant") { | |
b = "feep"; | |
q.param = "[ghant intercept, "~q.channel.clean()~"] "~q.param; | |
goto start; | |
} | |
return q.notice("Note saved! "); | |
} | |
} | |
string[string] last_added; | |
/* | |
struct Markov { | |
} | |
void name(Query query) { | |
}*/ | |
import tools.downloader; | |
void addquote(Query query) { | |
if (query.channel.tolower() == "#lgbtbbq" /or/ "#esquarium") | |
return query.answer("adding quotes is disabled in this channel"); | |
if (query.channel.tolower() == "#lesswrong") | |
return query.answer("No. Because blame bad people who hate bots."); | |
query.param = query.param.strip(); | |
if (!query.param.length) { | |
return query.answer(CTRL(query.channel), "addquote <nick> <quote> or <quote>"); | |
} | |
auto ch = query.channel.replace("#", ""); | |
auto nick = query.param.slice(" ").strip(); | |
if (nick.startsWith("<")) { | |
query.param = nick ~ " " ~ query.param; // part of message | |
nick = nick[1 .. $]; | |
nick = nick.slice(">").strip(); | |
} | |
if (nick.startsWith("(")) { | |
nick = nick[1 .. $]; | |
nick = nick.slice(")").strip(); | |
query.param = "<" ~ nick ~ "> " ~ query.param; | |
} | |
if (nick.startsWith("*")) { | |
query.param = nick ~ " " ~ query.param; // dito | |
nick = nick[nick.find("*")+1 .. $]; | |
} | |
while (nick.length && "%@+-*".find(nick[0]) != -1) nick = nick[1 .. $]; | |
if (!nick.length) return query.answer("Invalid nickname: "~nick~"!"); | |
if (query.channel.tolower() == "#sondria" && nick.tolower() == "lanos") return query.answer("No."); | |
if (query.channel.tolower() == "#sondria" && nick.tolower() == "feep" && query.name.tolower() == "crontor") return query.answer("Fuck you."); | |
if (cast(nickname) nick == query.name && query.name != root_user) return query.answer("Please don't selfquote! "); | |
last_added[ch.clean() ~ "\x00" ~ cast(string) query.name] = nick; | |
auto quotes = wbfile.get!(string[])(ch.clean(), nick, null); | |
quotes ~= query.param; | |
wbfile.set(ch.clean(), nick, quotes); | |
// return query.answer(Format("Added quote for ", nick, " (", quotes.length, "): ", query.param, ".")); | |
return query.answer("Quote added (", quotes.length, ")"); | |
} | |
int do_remove(string channel, string nick) { | |
auto quotes = wbfile.get!(string[])(channel.clean(), nick, null); | |
if (!quotes.length) return -1; | |
quotes = quotes[0 .. $-1]; | |
wbfile.set(channel.clean(), nick, quotes); | |
return quotes.length; | |
} | |
void unquote(Query query) { | |
query.param = query.param.strip(); | |
if (!query.param.length) { | |
auto key = query.channel.replace("#", "").clean() ~ "\x00" ~ cast(string) query.name; | |
scope(exit) last_added.remove(key); | |
if (auto n = key in last_added) { | |
auto nick = *n; | |
auto u = do_remove(query.channel.replace("#", "").clean(), nick); | |
if (u == -1) throw new Exception("What the hell? "); | |
return query.answer("Last quote removed (", u, ")"); | |
} | |
return query.answer("Usage: unquote <name>"); | |
} | |
bool authorized() { | |
if (.auth(query)) return true; | |
if (query.channel == "#stormbit" && query.name == cast(nickname) "Toast" | |
|| query.channel == "#anime" && query.name == cast(nickname) "Nanobot") | |
return query.connection.registered(query.name); // "Permissions system" | |
if (query.channel == "#ryst" && query.name == cast(nickname) "Rikairchy") return true; // lol | |
return false; | |
} | |
if (!authorized()) return query.answer("Not authorized. "); | |
auto ch = query.channel.replace("#", ""); | |
auto nick = query.param; | |
auto n = do_remove(ch, nick); | |
if (n == -1) return query.answer("No quotes for "~nick~"!"); | |
return query.answer("Last quote removed (", n, ")"); | |
} | |
import tools.downloader, tools.serialize; | |
void quote(Query query) { | |
if (query.param == "s" || query.param.startsWith("s ")) { | |
query.param = query.param[1..$].strip(); | |
return quotes(query); | |
} | |
if (query.param == "help") { | |
return query.answer(CTRL(query.channel), "quote [nick]"); | |
} | |
bool match(string quote, string q) { | |
// if (auto pre = q.endsWith(" ")) if (quote.endsWith(pre)) return true; | |
if (auto qq = q.strip().startsWith("\"").endsWith("\"")) | |
return quote.ifind(qq) != -1; | |
foreach (part; q.split(" ")) if (quote.ifind(part) == -1) return false; | |
return true; | |
} | |
void fn(string q, Query query) { | |
auto ch = query.channel.replace("#", "").strip(); | |
if (!ch.length) return; | |
synchronized (nexts_sync) nexts[query.name] = q /apply/ &fn; | |
string[] quotes; | |
if (q.length) { | |
quotes = wbfile.get!(string[])(ch.clean(), q, null); | |
if (!quotes.length) { | |
auto quotemap = wbfile.section_map(ch.clean()); | |
foreach (key, value; quotemap) | |
quotes ~= deserialize!(string[])(value); | |
string[] matches; | |
foreach (quote; quotes) if (match(quote, q)) matches ~= quote; | |
if (!matches.length) return query.answer("No quotes for "~q~"!"); | |
auto qtext = matches[rand()%$]; | |
if (qtext.strip().startsWith("!" /or/ ".")) qtext = "- " ~ qtext; | |
return query.say(qtext); | |
} | |
} else { | |
auto quotemap = wbfile.section_map(ch.clean()); | |
foreach (key, lines; quotemap) { | |
foreach (entry; wbfile.get!(string[])(ch.clean(), key, null)) | |
quotes ~= Format(key, ": ", entry); | |
} | |
} | |
if (!quotes.length) return query.say(Format("No quotes! ")); | |
else return query.say(Format("Quote for ", quotes[rand%$])); | |
} | |
fn(query.param, query); | |
} | |
alias loli.cmp cmp; | |
import pad.engine: pastepost; | |
void quotes(Query query) { | |
auto ch = query.channel.replace("#", ""); | |
auto quotelist = wbfile.section_map(ch.clean()) | |
/map/ (string key, string value) { return stuple(key, deserialize!(string[])(value)); }; | |
if (query.param.strip() == "count") | |
return query.answer(quotelist.length, " in ", ch); | |
string[] parts; | |
foreach (x; quotelist) { | |
auto key = x._0, value = x._1; | |
string mine; | |
mine ~= Format(key, ": \n"); | |
string[] pieces; | |
foreach (line; value) { | |
pieces ~= " "~line.replace(" <", "\n <"); | |
} | |
mine ~= std.string.join(pieces, "\n --------\n"); | |
parts ~= mine; | |
} | |
auto text = std.string.join(parts, "\n ==========\n"); | |
if (true || text.length > 64000) { | |
query.answer("Hosting locally. "); | |
auto fn = "quotes_"~ch~".txt"; | |
auto localfn = "/home/mathis/public_html/"~fn; | |
localfn.write(text); | |
system("chmod a+r "~localfn); | |
query.answer("http://demented.no-ip.org/~feep/"~fn); | |
return; | |
} | |
query.answer("Uploading ", text.length, "b .. "); | |
return query.answer(pastepost(text, query.connection.nick, "d")); | |
} | |
void wb(Query query) { | |
auto ch = query.channel.replace("#", ""); | |
auto quotes = wbfile.section_map(ch.clean()); | |
query.say("Debug: ch ", ch, " -> ", ch.clean()); | |
string[int] answer; | |
foreach (nick, line; quotes) { | |
int len = wbfile.get!(string[])(ch.clean(), nick, null).length; | |
if (len in answer) answer[len] ~= ", "~nick; | |
else answer[len] = Format(len, " for ", nick); | |
} | |
struct temp { string val; int key; } | |
temp[] flat; | |
foreach (key, value; answer) flat ~= temp(value, key); | |
flat = flat.qsortfn(ex!("a, b -> a.key < b.key")); | |
query.answer("WB stats: ", std.string.join(flat /map/ ex!("a -> a.val"), "; ")); | |
} | |
void prettytex(Query query) { | |
auto input = query.param.strip(); | |
char[512] tmpbuf; | |
auto ptr = std.c.stdio.tmpnam(tmpbuf.ptr); | |
if (!ptr) return; | |
auto tmpfile = toString(ptr).dup; | |
auto texfile = tmpfile~".tex"; | |
auto dvifile = tmpfile~".dvi"; | |
write(texfile, `\documentclass[varwidth=true]{standalone} | |
\usepackage{amsmath} | |
\usepackage{amssymb} | |
\begin{document} | |
`~input~` | |
\end{document} | |
`); | |
if (system("lualatex --output-format=dvi --interaction=nonstopmode --output-directory=$(dirname "~texfile~") "~texfile) != 0) { | |
query.answer("lualatex errored"); | |
return; | |
} | |
auto output = read_cmd("catdvi -e UTF-8 "~dvifile); | |
auto lines = output.split("\n"); | |
while (lines.length && lines[$-1].strip().length == 0) lines = lines[0..$-1]; | |
foreach (line; lines) query.say(": ", line); | |
} | |
import pad.utils: replaceEntities; | |
extern(C) int chmod(char* path, ushort perms); | |
struct PixivSession { | |
string session_id; | |
const User = "feepingcreature", Pass = "lumocol0r"; | |
string relogin() { | |
logln("Acquiring new session id"); | |
string[] ids; | |
string redir; | |
auto res = tools.downloader.download("POST=mode=login&pixiv_id="~User~"&pass="~Pass~" http://www.pixiv.net/login.php", &redir, 16, (string cookie) { | |
logln("COOKIE ", cookie); | |
ids ~= cookie.between("PHPSESSID=", ";"); | |
}); | |
logln("=> ", ids); | |
if (ids.length != 1) throw new Exception("Site behavior has changed. Please reverify. "); | |
session_id = ids[0]; | |
if (res.find("Sign up for pixiv</div") != -1) | |
throw new Exception("Pixiv login data is bad! "); | |
return res; | |
} | |
string download(string url) { | |
auto start_id = session_id; | |
logln("Nurl: COOKIE=PHPSESSID="~session_id~" "~url); | |
auto res = .download("COOKIE=PHPSESSID="~session_id~" "~url); | |
if (res.find("Join pixiv today") != -1 || res.find("enjoy a higher resolution") != -1) { | |
synchronized { | |
if (start_id == session_id) { | |
relogin(); | |
} // else another thread already logged us in | |
res = .download("COOKIE=PHPSESSID="~session_id~" "~url); | |
} | |
} | |
return res; | |
} | |
string savePicture(string url) { | |
auto nurl = url.replace("mode=medium", "mode=big"); | |
auto data = download("REFER="~url~" "~nurl); | |
if (auto rest = data.between("icon_firefox.png", "")) | |
data = rest; | |
auto img = data.between("img src=\"", "\""); | |
if (!img) { | |
logln("Data for ", nurl, ": \n", data); | |
throw new Exception("BAIL"); | |
} | |
auto picurl = "REFER="~nurl~" "~img; | |
if (picurl.getFilename() == "bt_login_base.png") | |
throw new Exception("pixiv extraction failed"); | |
auto target = "/mnt/data/www/srv/pixiv_unsuck".sub(picurl.getFilename()); | |
if (!target.exists()) { | |
target.write(download(picurl)); | |
chmod(toStringz(target), 0744); | |
} | |
return "http://feephome.no-ip.org/~feep/pixiv_unsuck".sub(picurl.getFilename().urlencode()); | |
} | |
} | |
static PixivSession pixivUnsuck; | |
string getTitle(string url, string src = null) { | |
url = url.replace("\x0f", ""); | |
// url = url.replace("https://", "http://"); | |
try return _getTitle(url, src); | |
catch (Exception ex) { | |
auto res = Format("[", ex, "]" /*" - debug ", Format(cast(ubyte[]) url)*/); | |
if (res.startsWith ("[SSL/TLS is not")) return null; | |
if (res.find ("tried to set up a circular") != -1) return null; | |
if (res.find ("4invalid UTF") != -1) return null; | |
return res; | |
} | |
} | |
// remove b from a | |
string remove(string a, string b) { | |
auto as = a.ptr, ae = a.ptr + a.length; | |
auto bs = b.ptr, be = b.ptr + b.length; | |
assert(bs >= as); | |
assert(be <= ae); | |
return a[0 .. bs - as] ~ a[$ - (ae - be) .. $]; | |
} | |
string between_incl(string str, string b, string c) { | |
auto res = str.between(b, c); | |
if (res) { | |
auto sa = str.ptr, sb = str.ptr + str.length; | |
auto ra = res.ptr, rb = res.ptr + res.length; | |
res = str[(ra - sa) - b.length .. $ - (sb - rb) + c.length]; | |
} | |
return res; | |
} | |
string after(string str, string sl) { | |
return str[sl.ptr - str.ptr + sl.length .. $]; | |
} | |
string _getTitle(string url, string src = null) { | |
auto res = __getTitle(url, src); | |
if (res.length && res[0..1] == "!"[] /or/ "."[]) res = "- " ~ res; | |
if (res.length) res = "<b></b>" ~ res; | |
return res; | |
} | |
string __getTitle(string url, string src = null) { | |
// feature is broken, do not use | |
/*if (url.find("pixiv.net/") != -1) { | |
auto nurl = pixivUnsuck.savePicture(url); | |
return "Unsucked to "~nurl~" !"; | |
}*/ | |
auto anchor = url.find("#"); | |
if (anchor != -1) url = url[0 .. anchor]; | |
bool had_partial; | |
if (!src) { src = url.download_first(); had_partial = true; } | |
if (!src) { src = url.download(); had_partial = false; } | |
if (src.startsWith("module")) return null; | |
// return first paragraph | |
if (url.find("wikipedia.org") != -1) { | |
string para; | |
if (auto name = url.between("/wiki/", "")) { | |
if (auto pre = name.between("", "?")) name = pre; | |
para = Format( | |
"http://en.wikipedia.org/w/api.php?action=query" | |
"&format=xml" | |
"&titles="~name~"" | |
"&prop=extracts" | |
"&exintro" | |
// "&explaintext" | |
"&exsentences=2") | |
.download() | |
.between("<extract", "</extract") | |
.between(">", "") | |
.replace("<", "<").replace(">", ">") | |
.replace("\n", "").replace(""", "\""); | |
goto gotPara; | |
} | |
// fallback | |
if (had_partial) { src = url.download(); had_partial = false; } | |
para = src; | |
if (string box = para.between("<table", "infobox", "</table>")) | |
para = para.after(box); | |
// if (string nav = para.between("<table", "navbox", "</table>")) | |
// para = para.after(nav); | |
// cut out side table of contents | |
if (para.find("<table class=\"toccolours") != -1) | |
para = para.between("</table>", ""); | |
para = para.between("<p>", "</p>"); | |
gotPara: | |
para = htmlFormat(para); | |
void rmTag(string tag) { | |
while (true) { | |
auto betw = para.between_incl("<"~tag~" ", ">"); | |
if (!betw) break; | |
para = para.remove(betw); | |
} | |
para = para.replace("<"~tag~">", ""); | |
para = para.replace("</"~tag~">", ""); | |
} | |
rmTag("p"); | |
rmTag("a"); | |
rmTag("abbr"); | |
rmTag("sup"); | |
rmTag("span"); | |
rmTag("small"); | |
rmTag("big"); | |
rmTag("ol"); rmTag("li"); | |
para = para.replace("<br />", "").replace("<br/>", ""); | |
bool rmDot; | |
auto Limit = 384 - url.length - 32; // safety margin, magic number! yay. | |
while (para.length > Limit) { | |
bool inTag; | |
int dp = para.length; | |
void checkInTag(string tag) { | |
if (inTag) return; | |
foreach (area; para.betweens("<"~tag, "</"~tag)) { | |
if (dp >= area.ptr - para.ptr && dp < area.ptr + area.length - para.ptr) { | |
inTag = true; | |
break; | |
} | |
} | |
} | |
do { | |
dp = para[0 .. dp].rfind("."); | |
if (dp == -1) break; | |
inTag = false; | |
checkInTag("i"); | |
checkInTag("b"); | |
} while (inTag); | |
if (dp != -1) { | |
para = para[0 .. dp]; | |
rmDot = true; | |
continue; | |
} | |
break; | |
} | |
if (para.length < Limit) { | |
if (rmDot) para ~= "."; | |
return para; | |
} | |
} | |
auto title = src.between("<title>", "</title>"); | |
if (!title && had_partial) { | |
src = url.download(); | |
had_partial = false; | |
title = src.between("<title>", "</title>"); | |
if (!title) return null; | |
} | |
if (had_partial) { | |
src = url.download(); // we know it's a website; download the full thing | |
had_partial = false; | |
} | |
string extra; | |
string[string] extras; | |
if (pwn_site(url, extras, src)) { | |
foreach (key, value; extras) | |
extra ~= Format("[", key, " ", value, " ]"); | |
} | |
if (url.ifind("escapistmagazine.com/videos/view/") != -1) { | |
src = src.replace(""", "\""); | |
auto js_url = src.between("flashvars=\"config=", "\""); | |
if (!js_url) goto mistake; | |
auto js = js_url.download(); | |
string video; | |
foreach (str; js.betweens("'", "'")) { | |
if (str.endsWith(".flv" /or/ ".mp4")) { video = str; break; } | |
} | |
if (!video) goto mistake; | |
if (video.startsWith("rtmp")) { | |
auto fn = title.between("Video Galleries : ", "").replace(": ", "").replace("'", "").replace(" ", "_")~".mp4"; | |
extra ~= " [ flvstreamer -o "~fn~" -r $(wget "~video.mktiny()~" 2>&1 |grep Location |awk '{print $2}') ]"; | |
} else { | |
extra ~= " ["~video[$-3 .. $]~" "~video.mktiny()~"]"; | |
} | |
} | |
mistake: // Wait, my mistake. Not a ZP video after all. | |
if (title.length > 128) title = title[0..128] ~ "[snip]"; | |
auto res = title.replaceEntities().replace("\n", "").strip() ~ extra; | |
bool booru = | |
url.tolower().find("gelbooru.com") != -1 | |
|| url.tolower().find("booru.donmai.us") != -1; | |
if (booru) { | |
auto tags = res.split(" "); | |
auto shown = tags; if (shown.length > 14) shown.length = 14; | |
string omit; | |
if (tags.length > shown.length) | |
omit = Format("[", tags.length - shown.length, " omitted]"); | |
res = std.string.join(shown, " ") ~ omit; | |
} | |
return res; | |
} | |
string[] safelist; | |
bool isSafe(string str) { | |
foreach (s2; safelist) | |
if (str.startsWith(s2)) return true; | |
return false; | |
} | |
void tell(Query query) { | |
if (query.channel == "#sting") return; | |
auto backup = query; | |
query.name = cast(nickname) query.param.slice(" "); | |
if (auto rest = query.param.strip().startsWith(CTRL(query.channel))) { | |
if (!auth(backup) && !isSafe(rest)) | |
return backup.answer("This used to be a security leak. It's closed now. "); | |
query.param = rest; | |
runQuery(query); | |
} else query.answer(query.param); | |
} | |
void ip(Query query) { | |
query.answer("I'm online from ", "automation.whatismyip.com/n09230945.asp".download()); | |
} | |
void control_char(Query query) { | |
if (!auth(query)) return query.answer("You are not authorized to do that."); | |
setCtrl(query.channel, query.param); | |
query.answer("Control character is now `", CTRL(query.channel), "'."); | |
} | |
string timediff(typeof(µsec()) dist) { | |
dist /= 1_000_000L; | |
string res; | |
void add(string st) { if (res.length) res ~= ", "; res ~= st; } | |
int count; | |
bool addUnit(int size, string name) { | |
if (dist > size) { | |
count ++; | |
auto div = dist / size; | |
add(Format(div, " ", name)); | |
if (div > 100000) | |
add(Format("debug ", dist, " ", cast(long) dist, " ", size)); | |
dist -= div * size; | |
if (size == 1) return true; | |
} | |
if (count == 2) return true; | |
return false; | |
} | |
if (addUnit(60*60*24*7, "weeks") | |
||addUnit(60*60*24, "days") | |
||addUnit(60*60, "hours") | |
||addUnit(60, "minutes") | |
||addUnit(1, "seconds") | |
) return res~" ago"; | |
return "right now"; | |
} | |
typeof(µsec()) decodeTime(string t) { | |
typeof(µsec()) res; | |
if (auto rest = t.endsWith("d")) res = rest.atoi() * 3600L * 24L * 1_000_000L; | |
else if (auto rest = t.endsWith("h")) res = rest.atoi() * 3600L * 1_000_000L; | |
else { | |
throw new Exception(Format("Unknown time format: ", t, ". TODO?")); | |
} | |
return res; | |
} | |
void seen(Query query) { | |
auto fun = delegate void(string s) { query.answer(s); }; | |
if (query.channel == "#tgchan") fun = delegate void(string s) { query.notice(s); }; | |
if (!query.param.length) return fun("Usage: "~CTRL(query.channel)~"seen <nick>"); | |
bool talking; | |
if (auto rest = query.param.strip().startsWith("talking ")) { talking = true; query.param = rest; } | |
if (auto time = query.param.strip().startsWith("within ")) { | |
typeof(µsec()) limit; | |
limit = decodeTime(time); | |
auto cur = µsec(); | |
auto strlist = seenfile.section(query.channel.clean()); | |
Stuple!(string, typeof(µsec()))[string] list; | |
foreach (key, value; seenfile.section_map(query.channel.clean())) { | |
try list[key] = deserialize!(Stuple!(string, typeof(µsec()))) (value); | |
catch (Exception ex) { } | |
} | |
bool[string] names; | |
bool similar(string a, string b) { | |
if (a.length > b.length) swap(a, b); | |
auto rest1 = b.startsWith(a), rest2 = b.endsWith(a); | |
if (rest1 && "_|[".find(rest1[0]) != -1) return true; | |
if (rest2 && "_|]".find(rest2[$-1]) != -1) return true; | |
return false; | |
} | |
string shorter(string a, string b) { | |
if (a.length < b.length) return a; | |
return b; | |
} | |
outer:foreach (key, value; list) { | |
auto dist = cur - value._1; | |
if (dist < limit && (!talking || value._0 != "JOIN")) { | |
auto name = key.dup; | |
auto keys = names.keys.dup; // ouch | |
foreach (key2; keys) { | |
if (similar(key2, name)) { | |
key2 = key2.dup; | |
names.remove(key2); | |
names[shorter(name, key2)] = true; | |
continue outer; | |
} | |
} | |
names[name] = true; | |
} | |
} | |
query.notice("Seen within the last ", time, ": ", std.string.join(names.keys, ", ")); | |
return; | |
} | |
Stuple!(string, typeof(µsec())) entry; | |
string extra = " "; | |
// unsafe | |
if (auto rest = cast(string) null /*query.param.strip().startsWith("global ")*/) { | |
query.param = rest; | |
auto ids = seenfile.sections(); | |
typeof(µsec()) highest_time = 0; | |
bool anyHit; | |
foreach (id; ids) { | |
bool hit = true; | |
auto ent = seenfile.get!(Stuple!(string, typeof(µsec())))(id, query.param.tolower(), { hit = false; return stuple("", µsec()); }()); | |
if (!hit) continue; | |
anyHit = true; | |
if (ent._1 > highest_time) { | |
highest_time = ent._1; | |
entry = ent; | |
extra = format(" in ", id, " "); | |
} | |
} | |
if (!anyHit) return fun("I have not seen anybody named \""~query.param~"\" anywhere at all! "); | |
} else { | |
if (!seenfile.has(query.channel.clean(), query.param.tolower())) { | |
bool success; | |
foreach (key, value; seenfile.section_map(query.channel.clean())) { | |
if (key.find(query.param.tolower()) != -1) { | |
entry = deserialize!(Stuple!(string, typeof(µsec())))(value); | |
query.param = key; | |
success = true; | |
break; | |
} | |
} | |
if (!success) return query.answer("I have not seen anybody named \""~query.param~"\" in this channel!"); | |
} else entry = seenfile.get!(Stuple!(string, typeof(µsec())))(query.channel.clean(), query.param.tolower(), { fail; return stuple("", µsec()); }()); | |
} | |
string mesg; | |
if (auto rest = entry._0.startsWith("\x00NICK ")) { | |
mesg = Format(" changing nick to ", rest); | |
} else if (auto rest = entry._0.startsWith("\x01ACTION ")) { | |
rest = rest[0 .. $-1]; | |
mesg = Format(" doing \" * ", query.param, " ", rest, "\"."); | |
} else mesg = Format(" saying \"", entry._0, "\"."); | |
fun(Format("I have last seen ", query.param, extra, timediff(µsec() - entry._1), mesg)); | |
} | |
void peak(Query query) { | |
if (query.param.length) return query.answer("Usage: "~CTRL(query.channel)~"peak"); | |
if (!IRCconfig.has(query.connection.host, "peak_"~query.channel.clean())) return query.answer("No data."); | |
auto entry = IRCconfig.get!(Stuple!(int, typeof(µsec()), string)) (query.connection.host, "peak_"~query.channel.clean(), { fail; return stuple(0, µsec(), ""); }()); | |
query.answer("User peak was ", timediff(µsec() - entry._1), " when ", entry._2, " brought the total to ", entry._0, "."); | |
// query.notice("debug: ", query.connection.users[query.channel]); | |
} | |
void vstats(Query query) { | |
auto chquery = query; | |
chquery.name = cast(nickname) chquery.channel; | |
int[string] stats; | |
notice_cb = (hostmask h, string s) { | |
auto parts = s.split(" ")[1 .. $]; | |
synchronized { | |
if (parts.length) { | |
parts[0] = parts[0].strip(); | |
if (parts[0] != "(") stats[parts[0]] ++; | |
} | |
} | |
}; | |
chquery.say("\x01VERSION\x01"); | |
auto start = sec(); | |
while (start + 8 > sec()) slowyield(); | |
notice_cb = null; | |
// cribbed from cstats | |
auto rstats = invert(stats), top = rstats.keys.sort; | |
if (top.length > 10) top.length = 10; | |
if (top.length && top[0] == 1) top = top[1 .. $]; | |
query.answer("Version stats: <u>", top /map/ ex!("a -> b -> stuple(b, a[b])")(rstats), "</u>"); | |
} | |
void main(string[] args) { | |
while (true) { | |
try { | |
Main(args); | |
return; | |
} catch (Exception ex) { | |
logln("Closing pipes. "); | |
foreach (reader; readers) reader.close; | |
foreach (writer; writers) writer.close; | |
if (!Format(ex).startsWith("Nick already")) | |
write("error.txt", Format(ex)); | |
logln(ex, "! Restarting in 30s, resetting resume. "); | |
foreach (i, arg; args) if (arg == "--resume") { args = args[0 .. i]; break; } | |
sleep(30); | |
} | |
} | |
} | |
void delegate(hostmask, string) notice_cb; | |
void delegate(Query)[nickname][string] greenmap; | |
void delegate(Query) delegate(nickname)[string] threadgens; | |
void dispatchGreenCmd(string base, Query q) { | |
auto n = q.name; | |
if (base /notin/ threadgens) fail("No such green command: "~base); | |
void delegate(Query) sk; | |
synchronized { | |
if (base /notin/ greenmap) { | |
greenmap[base] = Init!(typeof(greenmap[base])); | |
} | |
if (n /notin/ greenmap[base]) { | |
greenmap[base][n] = threadgens[base](n); // start up thread | |
} | |
sk = greenmap[base][n]; | |
} | |
sk(q); | |
} | |
Threadpool tgtp; | |
static this() { | |
New(tgtp, Threadpool.GROW); | |
threadgens["ps"] = (nickname nick) { | |
auto conduit = new MessageMultiChannel!(Query, false, false); | |
tp.addTask(stuple(conduit, nick) /apply/ (typeof(conduit) conduit, nickname nick) { | |
Query last; | |
string getCmd() { last = conduit.get(); return last.param; } | |
auto max = getCmd().atoi(); | |
if (!max) return last.answer("Please provide a point limit. "); | |
int[6] points; // str dex con int wis cha | |
points[] = 8; | |
int used(int[] field) { | |
int res; | |
foreach (point; field) { | |
point -= 8; res += point; | |
point -= min(point, 6); res += point; | |
point -= min(point, 2); res += point; | |
} | |
return res; | |
} | |
int[string] map = ["str": 0, "dex": 1, "con": 2, "int": 3, "wis": 4, "cha": 5]; | |
while (true) { | |
last.answer("Str ", points[0], " Dex ", points[1], " Con ", points[2], " Int ", points[3], " Wis ", points[4], " Cha ", points[5], | |
". ", max - points.used(), " left. "); | |
dontReprint: | |
auto cmd = getCmd(); | |
auto parts = cmd.split(";"); | |
int[6] dest = points; | |
foreach (part; parts) { | |
part = part.strip(); | |
if (auto rest = part.startsWith("add " /or/ "put ")) { | |
auto name = rest.slice(" "), value = rest; | |
if (!value) value = "1"; | |
if (value.atoi() == 0 && name.atoi() != 0) swap(value, name); | |
auto v = value.atoi(), n = name.strip().tolower(); | |
if (n /notin/ map) { last.answer("No such attribute: ", n, "!"); goto dontReprint; } | |
dest[map[n]] += v; | |
} else if (auto rest = part.startsWith("take " /or/ "sub ")) { | |
auto name = rest.slice(" "), value = rest; | |
if (!value) value = "1"; | |
if (value.atoi() == 0 && name.atoi() != 0) swap(value, name); | |
auto v = value.atoi(), n = name.strip().tolower(); | |
if (n /notin/ map) { last.answer("No such attribute: ", n, "!"); goto dontReprint; } | |
dest[map[n]] -= v; | |
} else if (auto rest = part.startsWith("reset").strip()) { | |
if (rest.atoi()) max = rest.atoi(); | |
dest[] = 8; | |
} else if (auto rest = part.startsWith("set").strip()) { | |
auto sp = rest.split(" "); | |
if (sp.length != 6 /or/ 1) { last.answer("Invalid number of attributes! "); goto dontReprint; } | |
if (sp.length == 1) dest[] = sp[0].atoi(); | |
else dest[] = sp /tools.functional.map/ (string s) { return cast(int) atoi(s); }; | |
} else if (auto rest = part.startsWith("move ")) { | |
auto num = rest.slice(" ").atoi(), from = rest.slice(" ").tolower(), to = rest.slice(" ").tolower(); | |
if (num !> 0 || !from || !to) { last.answer("Invalid parameter! "); goto dontReprint; } | |
if (from /or/ to /notin/ map) { last.answer("No such attribute! "); goto dontReprint; } | |
dest[map[from]] -= num; | |
dest[map[to]] += num; | |
} | |
} | |
if (dest.used() > max) { last.answer("Not enough points: short by ", dest.used() - max, ". "); goto dontReprint; } | |
foreach (val; dest) { | |
if (val < 8) { last.answer("Can't lower attributes below 8! "); goto dontReprint; } | |
} | |
points[] = dest; | |
} | |
}); | |
return &conduit.put; | |
}; | |
} | |
void pointbuy(Query q) { | |
if (!q.param.length) return q.answer("Usage: pointbuy attr1 attr2 .. attr6"); | |
auto attrs = q.param.split(" ") /map/ &atoi; | |
if (attrs.length != 6) return q.answer("Invalid number of attributes! "); | |
int points; | |
attrs /map/ (int i) { | |
i -= 8; points += i; | |
i -= min(i, 6); points += i; | |
i -= min(i, 2); points += i; | |
}; | |
return q.answer(points, " points. "); | |
} | |
void overlap(Query q) { | |
q.param = q.param.strip(); | |
auto ch1 = q.param.slice(" "); | |
if (!q.param.length) return q.answer("Usage: overlap <channel 1> <channel 2>"); | |
auto ch2 = q.param.strip(); | |
auto users1 = new Stuple!(bool[string]); | |
q.connection.whoare(ch1, | |
users1 /apply/ (typeof(users1) users1, string nick, string line) { users1._0[nick] = true; }, | |
stuple(q, ch1, ch2, users1) /apply/ (Query q, string ch1, string ch2, typeof(users1) users1) { | |
auto dups = new Stuple!(string[]); | |
q.connection.whoare(ch2, | |
stuple(users1, dups) /apply/ (typeof(users1) users1, typeof(dups) dups, string nick, string line) { | |
if (nick in users1._0) dups._0 ~= nick; | |
}, stuple(q, ch1, ch2, dups) /apply/ (Query q, string ch1, string ch2, typeof(dups) dups) { | |
string rest; | |
if (dups._0.length > 16) { | |
rest = Format(" and ", dups._0.length - 16, " more"); | |
dups._0 = dups._0[0 .. 16]; | |
} | |
q.answer("Shared users between ", ch1, " and ", ch2, ": ", dups._0, rest, "."); | |
} | |
); | |
} | |
); | |
} | |
void msg(Query query) { | |
if (!auth(query)) { | |
return query.answer("Unauthorized! "); | |
} | |
query.name = cast(nickname) query.param.slice(" "); | |
query.privtell(query.param); | |
} | |
import dice; | |
void roll(Query query) { roll2(query, false); } | |
void roll2(Query query, bool partial) { | |
scope(exit) reset(); | |
string str = query.param.strip(); | |
if (!partial && !str.length) { | |
return query.answer("roll - dice rolls. Supported syntax: 8d4k4 + 3L - H < 15 (<b>k</b>eep, <b>L</b>ow, <b>H</b>igh)."); | |
// query.answer("foo against bar, >= bar, <= bar, >, < can be used to roll against a value."); | |
} | |
Result delegate() dg; | |
auto start_str = str; | |
if (!gotResult(str, dg) && !partial) { | |
return query.answer("I do not understand "~str~"!"); | |
} | |
if (!dg) return; // partial | |
auto munched = start_str[0 .. $-str.length]; | |
bool number = true; | |
foreach (ch; munched) if ((ch < '0' || ch > '9') && ch != '-') number = false; | |
if (number) return; | |
str = str/*.strip()*/; | |
auto res = dg(); | |
if (/*!partial &&*/ str.length && !query.inMiddle) { | |
res.text = res.text ~ " " ~ str; | |
// return query.answer("Left over: "~str~"!"); | |
} | |
if (res.text.length > 256) res.text = res.text[0 .. 256] ~ "[...]"; | |
// return query.answer(res.text, (res.text.length?" :":""), "▕<i>", res.sum, "</i>▏"); | |
if (res.suppressNum) return query.answer(res.text); | |
else return query.answer(res.text, (res.text.length?": ":""), "<b>", res.sum, "</b>"); | |
} | |
void delegate(nickname, string)[][string] channelDgs; | |
float ringblend(float a, float b, float factor) { | |
if (a > b) { | |
swap(a, b); | |
factor = 1 - factor; | |
} | |
while (factor > 1) factor -= 1; | |
while (factor < 0) factor += 1; | |
float res; | |
if (b - a > 0.5) { | |
res = a - (1 - (b - a)) * factor; | |
} else { | |
res = a + (b - a) * factor; | |
} | |
while (res > 1) res -= 1; | |
while (res < 0) res += 1; | |
return res; | |
} | |
float ringdist(float a, float b) { | |
return min(abs(a - b), 1 - abs(a - b)); | |
} | |
// 3d | |
import tools.vector: vec3f; | |
float metric(float a, float b, float c) { | |
// L2 | |
return sqrt(a*a + b*b + c*c); | |
} | |
float ring3dist(vec3f a, vec3f b) { | |
return metric(ringdist(a.x, b.x), ringdist(a.y, b.y), ringdist(a.z, b.z)); | |
} | |
vec3f randvec() { | |
return vec3f(randf(), randf(), randf()); | |
} | |
vec3f ring3blend(vec3f a, vec3f b, float factor) { | |
return vec3f( | |
ringblend(a.x, b.x, factor), | |
ringblend(a.y, b.y, factor), | |
ringblend(a.z, b.z, factor) | |
); | |
} | |
struct Markov { | |
static { | |
struct MarkovEntry { | |
string word; | |
vec3f pos, next; | |
} | |
// TODO: faster lookup via binheap | |
MarkovEntry[] assocs; | |
MarkovEntry* lookupWord(string word) { | |
foreach (ref entry; assocs) { | |
if (entry.word == word) | |
return &entry; | |
} | |
return null; | |
} | |
MarkovEntry* lookupPos(vec3f pos) { | |
float mpos = float.max; | |
int which = -1; | |
foreach (i, ref entry; assocs) { | |
if (ring3dist(entry.pos, pos) < mpos) { | |
mpos = ring3dist(entry.pos, pos); | |
which = i; | |
} | |
} | |
if (which == -1) return null; | |
return &assocs[which]; | |
} | |
void save() { | |
synchronized(markovfile) { | |
markovfile.set("", "assocs", assocs); | |
} | |
} | |
void load() { | |
synchronized(markovfile) { | |
assocs = markovfile.get!(typeof(assocs))("", "assocs", null); | |
} | |
} | |
static this() { | |
load; | |
} | |
string sanify(string s) { | |
string res; | |
foreach (ch; s) | |
// TODO: std.utf | |
if ( | |
ch >= 'A' && ch <= 'Z' || | |
ch >= 'a' && ch <= 'z' || | |
ch >= '0' && ch <= '9' || | |
"'".find(ch) != -1 || | |
ch == ' ' && (!res.length || res[$-1] != ' ') | |
) | |
res ~= ch; | |
return res; | |
} | |
float drift = 0.1; | |
string quote(string nick, string msg) { | |
auto words = msg.sanify().tolower().split(" "); | |
if (words.length > 4) words = words[$-4 .. $]; | |
auto pos = randvec(); | |
int tracklength; | |
foreach (i, word; words) { | |
if (auto entry = lookupWord(word)) { | |
if (!i) { | |
pos = ring3blend(pos, entry.pos, (1-drift/2) + randf() * drift); | |
pos = ring3blend(pos, entry.next, (1-drift/2) + randf() * drift); | |
} else { | |
if (ring3dist(entry.pos, pos) > drift) { | |
// Wait, where the hell are we | |
// reset | |
tracklength = 0; | |
pos = entry.pos; | |
} else { | |
// good, we're on conversational track | |
pos = ring3blend(pos, entry.next, (1-drift/2) + randf() * drift); | |
} | |
} | |
tracklength++; | |
} else if (tracklength > 2) break; | |
} | |
if (tracklength < 2) return null; // not enough to latch on | |
// Now. | |
string[] blargh; | |
float test; vec3f cmp; string near; | |
while (blargh.length < 16) { | |
auto word = lookupPos(pos); | |
if (ring3dist(word.pos, pos) > drift) { test = ring3dist(word.pos, pos); cmp = word.pos; near = word.word; break; } | |
blargh ~= word.word; // word word balls up | |
pos = ring3blend(pos, word.next, 1); // no drift here. | |
} | |
// if (blargh.length < 3) return Format("[no long chain could be constructed: break at ", pos, ", ", test, "; nearest was ", near, " at ", cmp, "]"); | |
if (blargh.length < 3) return null; | |
return tools.compat.join(blargh, " "); | |
} | |
int i; | |
void learn(nickname nick, string msg) { | |
auto words = msg.sanify().tolower().split(" "); | |
auto pos = randvec(); | |
foreach (i, word; words) { | |
if (auto entry = lookupWord(word)) { | |
if (i) entry.pos = ring3blend(entry.pos, pos, drift); | |
pos = ring3blend(pos, entry.pos, (1-drift/2) + randf() * drift); | |
pos = ring3blend(pos, entry.next, (1-drift/2) + randf() * drift); | |
} else { | |
MarkovEntry nentry; | |
nentry.pos = pos; | |
nentry.next = randvec(); | |
nentry.word = word; | |
assocs ~= nentry; | |
} | |
} | |
i++; | |
if (i == 16) { | |
save; | |
i = 0; | |
} | |
} | |
} | |
} | |
void markovTalk(Query q, float chance, nickname nick, string msg) { | |
if (randf() > chance) return; | |
q.name = nick; | |
auto text = Markov.quote(nick, msg); | |
if (!text) return; | |
q.answer(text); | |
} | |
void freebird(Query query) { | |
void day(float f, string s) { | |
query.say(s); sleep(f); | |
} | |
day(3, ":slooow guiiiitar introooo:"); | |
day(4, "then the drums .. "); | |
day(5, "niiyaaa daee woo, dee dee aa dee daa dee; dadaewoo de de deeeaa ooo do da dae deeeeeEEEEE"); | |
day(4, "again"); | |
day(3, ":ahem: if I leave here tomorrooooww .... "); | |
day(3, "would you still remember meee? "); | |
day(3, "for I must be travelling on now .. "); | |
day(3, "'cause there's too many places I've got to see! "); | |
day(3, "and if I stay here with you, girl .. "); | |
day(3, "things just couldn't be the same! "); | |
day(3, "'cause I'm as free as a bird now ... ~ "); | |
day(3, "and this bird you cannot change ~~~~ oooooooh"); | |
day(3, "and the bird you cannot change ~~~ :GUITAR WAIL:"); | |
day(3, "and this bird you cannot change :drums and guitar:"); | |
day(3, "LORD KNOWS I can't change"); | |
day(5, "eeeyaaa daa dee wooo .. dee deaaeeaaeee, daaee, da daewoo woo de de deeeeaaaa woo; da da da DAAA :high guitar wail:"); | |
day(3, "again .. "); | |
day(3, "bye bye, 'ts been sweet love, yea, yeah "); | |
day(3, "though this feeling I can't change .. "); | |
day(3, "but please don't take this so badly! >_<"); | |
day(3, "'cause lord knows I'm to blame! "); | |
day(3, "but if I stay here with you, girl"); | |
day(3, "things just couldn't be the same. "); | |
day(3, "'cause I'm as free as a bird now"); | |
day(3, "and this bird you CANNOT CHANGE! "); | |
day(3, "and the bird you cannot change! "); | |
day(3, "and this bird you <b>cannot change! </b>"); | |
day(3, "LORD KNOWS I can't change!! "); | |
day(3, "Lord help me, I can't CHAIAIAIAIAIAIANGE"); | |
day(3, "WON'T YOU FLYYYY HIIIGH <b><u>FREE BIRD, YEAH!</u></b>"); | |
day(4, "NEYAWOO DE DE DEE DEE, AAWOO DE DE DA DO DEE, DAA WOO DE-DEE IIEE, IEE, IEE"); | |
day(4, "UUAAWOO DE-DE-DEE-DEE, DAA DOO EE EE AA WOO EE, AAWOO DE-EE IIEE, IEE, IEE EE"); | |
day(5, "EE EE EE EE EE EE EE EE EE EE EE EE EE EE EE AOEEAOEEOAAEAOEEAAOEEEAEOAEEOOEAEAOEAOEE"); | |
day(4, "DEAWOO <b>EE EE</b> AOOEAOOEAOOEAOOEAOOEAOOEAOOEAAOE AAOAOO AAOAOO AAOAOOAEEWEEWEWEEE"); | |
day(4, "EAA EAA EAA EAA EAA EAA EAA EAA EAA EAA EAA EAA EAA AA AA AA DAWOO EAAO EAAO EAAO EAAO"); | |
day(4, "EAAO DA DAA DAA DAA WOO DAA WOO A WOOAOO DA DAA! DA WOO DA DA DADAWOO DAA! DA WOO DA DA DADAWOO DAA! "); | |
day(4, "DA WOO DA DA DADAWOO DAA! DAA WOO DA DAA! DAA WOO DA DAA! DAA WOO DA DAA! DAA WOO DA DAA!"); | |
day(4, "DA WOO EEE AADAWOO EE AADAWOO EE AADAWOO EE AADAWOO EE AADAWOO EE AADAWOO EE AADAWOO"); | |
day(4, "EE AADAWOO EE EEE EE EE DA DA WOO DA DA WOO! DA DA DE DE DA DA WOO! UU WE WOO DA DA WOO WOO"); | |
day(4, "UUAAWOO DA DA AWOO; OO OO ADAWAWAWOO, OO OO WA WOO EEWOO EEWOO EWOO"); | |
day(4, "NYAWOO WOWOO DAWOO AWOO NA NE NAOWOO, NAO WA WOO EEWOO EEWOO EE-EE-EE-AA-WOO"); | |
day(4, "BA DA WOO, BA DA WOO, BA DA WOO, BA DA WOO, BA DA WOO, BA DA WOO, BA DA WOO, BA DA EE-EE-EE-AA-WOO"); | |
day(4, "BA DA WOO, BA DA WOO, BA DA WOO, BA DA WOO, BA DA WOO, BA DA WOO, BA DA WOO, BA DA EE-EE-EE-UU-WOO"); | |
day(4, "WOO A WOO DOO, DEAO WOO A WOO DOO, DEAO WOO A WOO DOO, DA WOO WOO WOO NEAO"); | |
day(3, ":unorganized guitar wail:"); | |
day(3, ":HIGHER WAIL:"); | |
day(3, ":<b>LOWER</b> WAIL:"); | |
day(4, "EEE EEE EEE EEE EE AA AA AA WOO WOO WOO OO OO OO OO OO OO OO AA AA AA DOO, NYEAOWEA A DOO DOO, NYEAOWEA A DOO"); | |
day(3, "A DA DOO, A DA DEAWOOEWOOEEWOO, :back to the wails:"); | |
day(3, ":save the wails:"); | |
day(3, ":drums come back in:"); | |
day(3, ":in a stunning majority vote, WAILS has segregated from the COMMONWEALTH today:"); | |
day(3, "EEEWOO EEEE WWOOOO EEEEEEEEEEEEEIIIII WOOO IIIIIIII WHAT ARE YOU DOING TO THAT CAT FEEPBOT"); | |
day(3, "NOO GIVE ME BACK MY GUITAR"); | |
day(3, "Animal cruelty will not be permitted in this bot! "); | |
day(3, ":("); | |
day(3, ":music fades into the distance:"); | |
day(3, ":feepbot bows:"); | |
} | |
void markov(Query query) { | |
if (query.param.startsWith("listen") || query.param.startsWith("learn")) { | |
channelDgs[query.channel] ~= &Markov.learn /todg; | |
query.answer("Now learning on ", query.channel); | |
return; | |
} | |
if (query.param.startsWith("stat")) { | |
query.answer(Markov.assocs.length, " associations saved. "); | |
return; | |
} | |
if (query.param.startsWith("reset") && query.name == root_user) { | |
Markov.assocs = null; | |
Markov.save; | |
query.answer("It's all gone .. "); | |
return; | |
} | |
if (auto rest = query.param.startsWith("drift")) { | |
if (!rest.length) { | |
query.answer("Drift is ", Markov.drift); | |
return; | |
} | |
Markov.drift = rest.atof(); | |
query.answer("Drift set to ", Markov.drift, ". "); | |
return; | |
} | |
if (auto rest = query.param.startsWith("talk ")) { | |
auto chance = rest.atoi() / 100f; | |
channelDgs[query.channel] ~= stuple(query, chance) /apply/ &markovTalk; | |
query.answer("Now active on ", query.channel, ". "); | |
} | |
} | |
void dcode(Query query) { | |
auto code = query.param.strip(); | |
if (!code.length) { query.answer("Usage: <D program>"); return; } | |
auto response = ("POST=lang=D&code="~urlencode(code)~"&run=True&submit=Submit http://codepad.org/").download(); | |
auto output = response.between("<span class=\"heading\">Output:", "</table>").between("</pre>", "</pre>").between("<pre>", "").strip().replace("\n", "\\"); | |
if (output.length > 512) output = output[0 .. 512] ~ Format("[...] ", output.length - 512, " omitted. "); | |
while (true) { | |
auto pos1 = output.find("<a href"); | |
if (pos1 != -1) { | |
output = output[0 .. pos1] ~ output.between("<a href", "").between(">", ""); | |
continue; | |
} | |
auto pos2 = output.find("</a>"); | |
if (pos2 != -1) { | |
output = output[0 .. pos2] ~ output.between("</a>", ""); | |
continue; | |
} | |
break; | |
} | |
query.answer(output.replace(""", "\"")); | |
} | |
void magic(Query q) { | |
if (!q.param.length) { | |
return q.answer("Usage: magic <card name>"); | |
} | |
auto site = googleQuery("site:magiccards.info -inurl:query "~q.param).download(); | |
auto name = site.between("<title>", "</title>").strip(); | |
auto type = site | |
.between("0 0 0.5", "").between("<p>", "</p>").replace("\n", ""); | |
// They broke the shop site by making it needlessly ajaxy | |
// and I cannot really be arsed to figure out their api | |
/*auto pricelink = site.between("href=\"", "magic.tcgplayer.com/db", "\""); | |
string priceinfo; | |
if (!pricelink) priceinfo = "No price info. "; | |
else { | |
auto pricesite = pricelink.download(); | |
auto pricedata = pricesite.between("#D9FCD1", "</div").betweens("<B>", "</b>"); // lol | |
assert(pricedata.length == 3); | |
priceinfo = Format("L ", pricedata[2], ", M ", pricedata[1], ", H ", pricedata[0]); | |
}*/ | |
return q.answer(name, ", ", type/*, ": ", priceinfo, " ", mktiny(pricelink)*/); | |
} | |
bool[string] waitingProcrast; | |
void handleProcrast(Query q) { | |
{ | |
scope(success) waitingProcrast.remove(q.name); | |
if (q.name in waitingProcrast) { | |
int which; | |
if (q.param.ifind("expectancy") != -1 || q.param.ifind("success") != -1) which = 1; | |
else if (q.param.ifind("value") != -1) which = 2; | |
else if (q.param.ifind("impulsive") != -1) which = 3; | |
if (which == 0) { return; } | |
string[] answers; | |
switch (which) { | |
case 1: | |
answers = [ | |
"Try to create a <i>Success Spiral</i>. Build confidence in your success by taking on a series of more immediate goals. " | |
"<br>Remember: rewarding process is more effective than rewarding outcome. ", | |
"Share in success! Find groups of people who regularly succeed at things, and share your accomplishments with them. " | |
"<br>Remember: our brains try to conform to what we perceive as expected. Make use of this.", | |
"Don't just visualize how happy you'll be when you do the task; create a <i>Mental Contrast</i> to how unsatisfied you are now. " | |
].dup; break; | |
case 2: | |
answers = [ | |
"Is your task boring? Try challenging yourself more until the task occupies your full attention." | |
"<br>This will help you enter <i>Flow</i>, a state where your brain is fully concentrated on the problem at hand, with no space for boredom.", | |
"Does your task seem arbitrary? Try to remind yourself of its <i>Meaning</i>; visualize how it will help you achieve the goals you actually care about.", | |
"<i>Energy</i> matters. Consider this checklist: drink water, eat light food, modafinil if available, exercise briefly once a week, shower, cold water, " | |
"music, declutter your life. Good luck! ", | |
"Does your task have a distant payoff? Pair it with a more immediate <i>Reward</i>, like a particular sweet or another guilty pleasure. " | |
"<br> Remember: reward process, not outcome!", | |
"If you aren't <i>Passionate</i> about what you're doing, at least consider the possibility that you might be doing the wrong thing altogether. " | |
].dup; break; | |
case 3: | |
answers = [ | |
"<i>Commit</i>! Deny yourself the possibility of delaying by associating it with costs outside your control, like announcing that you'll have the task completed by dd:mm:hh." | |
"<br>Remember: Schelling points are useful for this! Missing them implies that you'll have to pick another Schelling point, which might be far off. ", | |
"Pick the right <i>Goals</i>! Good goals should be challenging, achievable, practical, specific, and may be either process-based or goal-based. Try both. " | |
"<br>Routines are very powerful tools! Foster them wherever practical. " | |
].dup; break; | |
} | |
q.answer(answers[rand() % $]); | |
return; | |
} | |
} | |
if (q.param.startsWith("I'm procrastinating")) { | |
q.answer("That's good! The first step to improvement is acceptance."); | |
q.answer("What, specifically, are you procrastinating about? And is your " | |
"procrastination about your low expectancy of success, about the value of the task, or are you too impulsive (short-term centered)? "); | |
waitingProcrast[q.name] = true; | |
} | |
} | |
import neatcode; | |
import vars, loli, maid, std.date: getUTCtime, dateToString = toString; | |
// Organically grown. | |
void Main(string[] _args) { | |
exec = _args[0]; args = _args[1 .. $]; | |
uint resume_handle = -1; | |
logln("Arguments: ", args); | |
string hdl_t; | |
if (args.length > 4) | |
if (args[4] == "--resume") { | |
auto parts = args[5 .. $]; | |
args = args[0 .. 4]; | |
auto hdl_id = parts[0]; hdl_t = parts[1]; | |
reload_nick = cast(nickname) parts[2]; | |
count = parts[3].atoi() + 1; | |
resume_handle = hdl_id.atoi(); | |
} | |
if (args.length != 4) { | |
logln("Usage: ", exec, " <server> <nick> <channel> <root user>"); | |
return; | |
} | |
root_user = cast(nickname) args[3]; | |
void help(Query query) { | |
query.notice(Format("Supported commands: ", commands.keys)); | |
} | |
string[string] last_url; | |
auto last_url_sync = new Object; | |
anypos["trope"] = anypos["roll"] = true; | |
bool mask_rp; | |
auto d_imports = "import std.stdio, std.file, std.stream, std.string, std.math; "; | |
foreach (key, value; [ | |
"roll"[]: &roll /todg, "dice": &roll /todg, "statline": &statline /todg, | |
"google": &google /todg, "g ": &google /todg, "gcalc": &gcalc /todg, | |
"anidb": (Query q) { return anidb(q, 0, "", false); }, "decide": &decide /todg, | |
/*"vote": &vote /todg, */"rr": &rr /todg, "rps": &rps /todg, | |
"more": &more /todg, "join": &join /todg, "part": &part /todg, | |
"help": &help, "cstats": &cstats /todg, "cfind": &cfind /todg, "vstats": &vstats /todg, | |
"msg": &msg /todg, | |
"reload": &reload /todg, "ps": &dispatchGreenCmd /fix/ "ps", | |
"wb": &wb /todg, "tell": &tell /todg, /*"ip": &ip /todg,*/ | |
"say": &say /todg, "wp": &wp /todg, "addquote": &addquote /todg, // "loli": &loliHandle /todg, | |
"lw": &lw /todg, | |
"quotes": "es /todg, "quote": "e /todg, | |
"ctrl_char": &control_char /todg, "link": (Query q) { q.answer("I like pie!"); }, | |
"unquote": &unquote /todg, | |
"bridge": &bridge /todg, "seen": &seen /todg, "trope ": &trope /todg, | |
"tropesample": &tropesample /todg, "maid": &mkMaid /todg, "mansion": &mkMansion /todg, | |
"pad": &padfn /todg, "addstart": &addOnStart /todg, "repad": &repad /todg, | |
"pointbuy": &pointbuy /todg, "cah": &cahfn /todg, "df ": &df /todg, | |
"note": &addnote /todg, "dbg": &dbg /todg, "overlap": &overlap /todg, | |
"fwoosh": &gun /todg, "fwoosh_up": &fwoosh_upd /todg, | |
"markov": &markov /todg, "peak": &peak /todg, | |
"dsource": (Query q) { q.answer("http://downforeveryoneorjustme.com/dsource.org".getTitle()); }, | |
"down": (Query q) { q.answer(("http://downforeveryoneorjustme.com/"~q.param).getTitle()); }, | |
"rp": (Query q) { | |
bool mask = true; | |
if (q.channel != "#fetishfuel" /or/ "#ffsa") return q.answer("Wrong channel. "); | |
if (q.param == "done") mask = false; | |
mask_rp = mask; | |
q.channel = "#fetishfuel"; q.answer(mask?"#ffsa masked for RP. ":"#ffsa unmasked. "); | |
q.channel = "#ffsa"; q.answer(mask?"#fetishfuel masked for RP. ":"#fetishfuel unmasked. "); | |
}, "forcequit": (Query q) { if (!auth(q)) return q.answer("Unauthorized access! "); exit(0); }, | |
"woof": &woof /todg, | |
"title": (Query q) { | |
if (q.param.length) q.answer("<b>", q.param.getTitle(), "</b>"); | |
else synchronized(last_url_sync) if (auto p = q.channel in last_url) q.answer("<b>", (*p).getTitle(), "</b>"); | |
}, | |
"dcode": &dcode /todg, | |
"dstmt": (Query q) { q.param = d_imports ~ "void main() { "~q.param~" }"; dcode(q); }, | |
"dexpr": (Query q) { q.param = d_imports ~ "void main() { auto value = "~q.param~"; writefln(\"%s\", value); }"; dcode(q); }, | |
"neat": &neat /todg, "tex": &prettytex /todg, | |
"magic": &magic /todg, "listen": &listen /todg, "ban": &do_ban /todg, "raw": &raw /todg, | |
"imitate": &imitate /todg | |
]) commands[key] = value; | |
safelist ~= ["roll"[], "dice", "wp", "trope", "seen", "df", "woof", "title", "decide", "google", "g", "magic"]; | |
short port = 6667; | |
if (args[0].find(":") != -1) { | |
string portstr = args[0]; | |
args[0] = portstr.slice(":"); | |
port = cast(short) portstr.atoi(); | |
} | |
auto conn = new IRCconn(args[0], cast(nickname) args[1], port, resume_handle); | |
conn.onFind = conn /apply/ &onFind; | |
conn.onNickChange = conn /apply/ &onNickChange; | |
auto aborted = new bool; | |
scope(exit) *aborted = true; | |
tp.addTask(aborted /apply/ (ref bool aborted) { | |
while (!aborted) { | |
conn.raw_sendln("PING "~args[1]); | |
auto start = sec(); | |
sleep(30); | |
if (sec() - start < 20) { | |
logln("Timing is off. "); | |
while (sec() - start < 30) sleep(1); | |
} | |
} | |
}); | |
socket_handle = conn.sock.handle; | |
auto lastpoke = sec(), cooldown = 0.0; | |
bool isPoking() { return sec() < lastpoke + 5; } | |
bool blockPoking() { return sec() < cooldown + 40; } | |
void delegate(int)[string] channel_guards, wb_guards; | |
bool[hostmask] justJoined; | |
// int[hostmask] strikes; | |
int freebird; auto lastbird = sec(); | |
conn.defaultChanHandler = stuple(conn, channel_guards, justJoined) /apply/ (typeof(conn) conn, ref typeof(channel_guards) channel_guards, ref typeof(justJoined) justJoined, string channel, hostmask _host, string msg) { | |
with (conn) { | |
auto host = _host; | |
if (!channel.length && notice_cb) { notice_cb(host, msg); return true; } | |
logln("< ", msg); | |
// lblmcserver compat | |
if (.nick(host) == cast(nickname) "LBLCraftbot") { | |
if (auto rest = msg.startsWith("[")) { // status | |
if (auto name = rest.endsWith(" connected]")) { | |
host = cast(hostmask) (name~"!"); | |
msg = IRCconn.JOIN; | |
} | |
} else if (auto rest = msg.startsWith("(")) { | |
auto nick = rest.slice(") "); | |
host = cast(hostmask) (nick~"!"); | |
msg = rest; | |
} | |
} | |
if (msg == IRCconn.JOIN) { | |
bool netsplit; | |
if (channel /notin/ wb_guards) | |
wb_guards[channel] = floodguard!(false)(3, 1, { netsplit = true; }, { netsplit = false; }); | |
wb_guards[channel](1); | |
auto q = Query(conn, .nick(host), channel); | |
if (!netsplit) wbOnJoin(q); | |
justJoined[host] = true; | |
auto entry = IRCconfig.get!(Stuple!(int, typeof(µsec()), string)) (conn.host, "peak_"~channel.clean(), { return stuple(0, µsec(), ""); }()); | |
if (auto p = channel in conn.users) { | |
auto users = p.length; | |
if (users >= entry._0) | |
IRCconfig.set(conn.host, "peak_"~channel.clean(), stuple(conn.users[channel].length, µsec(), cast(string) .nick(host))); | |
} | |
} else { | |
if (host in justJoined) { | |
if (msg.find("ChimpOut") != -1 || msg.ifind("monkeyshines") != -1) { | |
Query q2 = Query(conn, cast(nickname) "ChanServ", ""); | |
q2.privtell("kickban ", channel, " ", .nick(host)); | |
return true; | |
} | |
if (msg.find("sendspace.com") != -1 || msg.ifind("thescripthere.tk") != -1 || msg.find("proxygod.com") != -1 | |
|| msg.ifind("xroxy.") != -1) { | |
// strikes[host] ++; | |
auto q = Query(conn, cast(nickname) "", channel); | |
// if (strikes[host] >= 3) { | |
q.say("Breakin' out the banhammer! "); | |
ban(channel, host); | |
// } else q.say(Format(3-strikes[host], " strikes left. ")); | |
return true; | |
} | |
justJoined.remove(host); | |
} | |
} | |
// #fetishfuel special handling | |
// ah well. Batabii is gone anyway. | |
// gah. he's back. | |
/*if (channel.tolower() == "#fetishfuel" && msg.characters().tolower() == "lol") { | |
kick(channel, .nick(host), "Ell Oh Ell! "); | |
}*/ | |
if (channel.tolower() == "#lesswrong" && msg.tolower().find(" just precommit") != -1 && msg.tolower().find("not just precommit") == -1 && msg.tolower().find("n't just precommit") == -1) { | |
kick(channel, .nick(host), "Human beings cannot \"just precommit\" to anything. We are not AIs or even TDT reasoners. That's why there's signalling to begin with!"); | |
} | |
// #himitsu special handling | |
if (channel.tolower() == "#himitsu" && msg.startsWith("!list" /or/ "!new")) { | |
tp.addTask(stuple(Query(conn, .nick(host), channel), conn) /apply/ (Query q, IRCconn ic) { | |
if (ic.exists(cast(nickname) "HimitsuBot")) return; | |
q.notice("Our bot isn't here right now. Perhaps try the archive bots in #lurk? "); | |
}); | |
return true; | |
} | |
if (.nick(host) == root_user && msg.find("SHUTDOWN") != -1) throw new Exception("Shut it doooooooown!"); | |
seenfile.set(channel.clean(), (cast(string) .nick(host)).tolower(), stuple(msg, µsec())); | |
bool permit = true; | |
permit &= .nick(host) != cast(nickname) "retardedFaggot"; | |
// if (channel.endsWith("tropers")) permit = false; // fucking bot nazis | |
if (channel.tolower().endsWith("anime")) permit = false; // moar nazis | |
if (channel.tolower().endsWith("lesswrong")) permit = false; // nice people | |
// if (channel.tolower().endsWith("sting")) permit = false; // alt bot | |
if (channel.tolower().endsWith("lgbtbbq")) permit = false; // alt bot I think | |
if (channel.endsWith("archlinux")) permit = false; // whee | |
if (channel.endsWith("flect")) permit = false; | |
if (channel == "#questdis") permit = false; | |
permit &= .nick(host) != cast(nickname) "TropeBot"; | |
permit &= .nick(host) != cast(nickname) "SkepticalBot"; | |
permit &= !(cast(string) .nick(host)).startsWith("GitHub"); | |
permit &= !(cast(string) .nick(host)).startsWith("Volt-Jenkins"); | |
permit &= !(cast(string) .nick(host)).endsWith("-001"); // vcs bot | |
// echo is a silly feature | |
/*if (permit) | |
if (auto action = msg.beginsWith("\x01ACTION ")) { | |
if (blockPoking()) return false; | |
action = action[0 .. $-1]; | |
if (channel /notin/ users) return false; | |
string match; | |
foreach (nick; users[channel]) { | |
if (match.length < nick.length && action.find(nick) != -1) match = nick; | |
} | |
if (!match) return false; | |
if (.nick(host) == cast(nickname) "ImoutoBot") return false; | |
action = action.replace(match, cast(string) .nick(host)); | |
message(channel, "\x01ACTION "~action~"\x01"); | |
lastpoke = sec(); | |
return false; | |
} | |
*/ | |
handleProcrast(Query(conn, .nick(host), channel, msg)); | |
// uncomment for awesome | |
if (false /*msg.ifind("__freebird") != -1*/) { | |
auto q = Query(conn, .nick(host), channel); | |
if (freebird == -1 && sec() - lastbird > 300) { | |
freebird = 0; | |
} | |
if (freebird >= 0) { | |
switch (freebird) { | |
case 0: q.say("Did somebody say freebird? "); break; | |
case 1: q.say("'cause I swear I heard somebody say FREEBIRD"); break; | |
case 2: q.say("You want me to play freebird, is that it? "); break; | |
case 3: q.say("OKAY HERE I GO"); .freebird(q); break; | |
} | |
if (freebird == 3) freebird = -1; | |
else freebird ++; | |
} | |
} | |
if (false && msg.ifind("stop") != -1 && isPoking()) { | |
auto r = rand(); | |
logln(r, " -- ", r % 5); | |
Query(conn, .nick(host), channel).answer( | |
["Aww. Okays."[], "Okay .. :/", "If you say so ._.", "Alright.", "No more pokety :("][r % $] | |
); | |
cooldown = sec(); | |
} | |
if (msg == "\x01VERSION\x01") { | |
Query(conn, .nick(host), channel).notice("\x01VERSION feepbot. made by feep, badger him or leave him a .note global feep message.\x01"); | |
return true; | |
} | |
if (msg.startsWith("\x01PING")) { | |
Query(conn, .nick(host), channel).notice(msg); | |
return true; | |
} | |
/*auto animes = msg.ifind("animes"); | |
auto msg2 = msg.tolower(); | |
if (msg2.between("", "animes").endsWith("the ")) animes = -1; | |
const vocals="aeiouAEIOUqQ"; | |
if (animes != -1 && (animes+6 == msg.length || vocals.find(msg[animes+6]) == -1) && channel.ifind("yackfest") == -1) { | |
auto answer = | |
["Anime."[], "It's anime.", "Anime is its own plural. ", "Anime.", "Anime >_>", "Just \"anime\"."] | |
[rand()%$]; | |
Query(conn, .nick(host), channel).answer(answer); | |
return true; | |
}*/ | |
// already done in irc.d I think | |
if (onFind && msg != IRCconn.JOIN) onFind(channel, host, false); | |
auto name = .nick(host); | |
if (channel == "#writing") xmppSend(conn, name, msg); | |
if (!channel.length) channel = .nick(host); | |
bool wasCommand; | |
auto lpos = msg.find("http://"); | |
if (lpos == -1) lpos = msg.find("https://"); | |
if (auto rest = msg.startsWith(CTRL(channel))) { | |
auto q = Query(conn, name, channel, rest); | |
tp.addTask(q /apply/ (Query q) { | |
try roll2(q, true); | |
catch (Exception ex) { | |
// q.say("Error: ", ex); | |
} | |
}); | |
} | |
if (auto rest = msg.startsWith("> ")) { | |
auto q = Query(conn, name, channel, "> "~rest); | |
try padfn(q); | |
catch (Exception ex) q.answer("Error: ", ex); | |
} | |
// logln("Ctrl code is ", CTRL(channel), " for ", channel); | |
if (auto cmd = msg.startsWith(CTRL(channel))) { | |
if (.nick(host) == cast(nickname) "ljrbot") { | |
Query(conn, .nick(host), channel, "").say("I don't take orders from a <i>bot. </i>"); | |
return true; | |
} | |
bool flooded; | |
synchronized(SyncObj!(channel_guards)) { | |
if (channel /notin/ channel_guards) | |
channel_guards[channel] = floodguard!(false)(6, 3, { flooded = true; }, { flooded = false; }); | |
channel_guards[channel](1); | |
} | |
auto query = Query(conn, name, channel, cmd); | |
if (flooded && (name != root_user)) query.notice("You have triggered my flood guard prevention. Please wait a bit."); | |
else tp.addTask(&runQuery /fix/ query); | |
} else if (channel.endsWith("anime")) { | |
if (msg.ifind("meow") != -1 && msg.ifind("meow_") == -1) | |
Query(conn, name, channel, "meow").act("licks ", name); | |
if (msg.ifind("#techtalk") != -1) | |
Query(conn, name, channel, "meow").act("nods"); | |
} else if (lpos != -1) { | |
auto link = msg[lpos .. $]; | |
if (auto res = link.endsWith("\x01")) link = res; // part of a /me | |
if (link.find(" ") != -1) link = link.slice(" "); | |
synchronized(last_url_sync) last_url[channel] = link; | |
if (channel.endsWith("tropers")) { | |
permit |= link.ifind("youtube.") != -1; | |
permit |= link.ifind("tinyurl.") != -1; | |
permit |= link.ifind("/ptitle") != -1; | |
} | |
permit &= link.ifind("travis-ci.org") == -1; | |
permit &= link.ifind("scp-wiki.wikidot.com") == -1; | |
permit &= channel.tolower() != | |
"#d" /or/ "#d.gdc" /or/ "#yackfest" /or/ "#mcdevs" /or/ "#tgchan" /or/ "#thevillageidiom" | |
/or/ "#c++" /or/ "#amber" /or/ "#trashheap" /or/ "#lw-prog" | |
/or/ "#crosstool-ng"; | |
// git announcers | |
permit &= .nick(host) != cast(nickname) "CIA-110"; | |
permit &= .nick(host) != cast(nickname) "CIA-81"; | |
permit &= .nick(host) != cast(nickname) "gh-clutter"; | |
permit &= .nick(host) != cast(nickname) "travis-ci"; | |
permit &= !(cast(string) .nick(host)).startsWith("Notifico-"); | |
permit &= !(cast(string) .nick(host)).startsWith("Not-"); | |
if (permit) { | |
tp.addTask(stuple(link, channel, Query(conn, name, channel, ""), lpos + link.length + 2) | |
/apply/ (string link, string channel, Query query, int ll) { | |
auto title = link.getTitle(); | |
if (!title) return; | |
if (channel.endsWith("tropers")) { | |
string rss; | |
if (std.file.exists("rss_tropers")) | |
rss = cast(string) "rss_tropers".read(); | |
auto name = title; | |
if (title.find("LostWorlds") != -1) { | |
query.answer("Fuck off, spammer. "); | |
return; | |
} | |
if (!name.length) name = link; | |
string ent_encode(string s) { | |
return s.replace("&", "&"); | |
} | |
auto isImage = name.tolower().endsWith(".jpg" /or/ ".jpeg" /or/ ".gif" /or/ ".png"); | |
if (!rss.length) rss = `<?xml version="1.0"?><rss version="2.0"> | |
<channel><title>#tropers feed</title></channel></rss>`; | |
rss = rss.replace("</channel>", Format(" | |
<item> | |
<title>", name.ent_encode(), "</title> | |
<link>", link.ent_encode(), "</link> | |
<pubDate>", dateToString(getUTCtime()), "</pubDate> | |
<guid>", rss.length, "</guid>", | |
(!isImage)?"":("<description><img src=\""~link~"\" /></description>"), | |
"</item> | |
</channel>")); | |
synchronized { | |
"rss_tropers.tmp".write(cast(void[]) rss); | |
system("rm rss_tropers; mv rss_tropers.tmp rss_tropers; chmod a+r rss_tropers"); | |
} | |
} | |
//string foo; | |
//for (int i = 0; i < ll; ++i) foo ~= " "; | |
string f; | |
if (channel.endsWith("tropers")) f = "b"; | |
else if (channel.endsWith("lets-read"[] /or/ "writing"[]/* /or/ "sting"[]*/)) f = "u"; | |
if (title.length) { | |
if (f) query.say(/*foo, */"<", f, ">", title, "</", f, ">"); | |
else query.say(title); | |
} | |
}); | |
} | |
} else foreach (key, value; commands) { | |
auto match = msg.find(CTRL(channel)~key); | |
if (key.strip() /notin/ anypos) continue; | |
if (match != -1) { | |
if (isBanned(host)) { return true; } | |
else { | |
tp.addTask(&runQuery /fix/ Query(conn, name, channel, msg[match+1 .. $], 0, true)); | |
} | |
wasCommand = true; | |
} | |
} | |
// This is silly. BUT SO WHAT. | |
if (permit && msg.find("♥") != -1) { | |
Query(conn, name, channel, "wub").say("Awwwwwwwwwwww!"); | |
return false; | |
} | |
if (auto p = channel in channelDgs) { | |
foreach (dg; *p) dg(.nick(host), msg); | |
} | |
if (!wasCommand && !(mask_rp && (channel == "#fetishfuel" /or/ "#ffsa"))) { | |
if (auto fdp = channel in writers) { | |
// non-breaking space | |
string mname = name; | |
int last_vocal = -1; | |
foreach_reverse(i, ch; mname) if ("aeiouAEIOU".find(ch) != -1) last_vocal = i; | |
if (last_vocal != -1) mname = mname[0 .. last_vocal] ~ mname[last_vocal] ~ mname[last_vocal .. $]; | |
try { | |
if (auto rest = msg.startsWith("\x01ACTION ")) { | |
fdp.writeLine(Format("* ", mname, " ", rest[0 .. $-1])); | |
} else if (msg == IRCconn.JOIN) { | |
fdp.writeLine(Format("* ", mname, " has joined. ")); | |
} else if (msg == IRCconn.PART) { | |
fdp.writeLine(Format("* ", mname, " has left. ")); | |
} else { | |
fdp.writeLine(Format("<", mname, "> ", msg)); | |
} | |
} catch (Exception ex) { | |
logln(ex, ". Ignoring. "); | |
} | |
} | |
} | |
return false; | |
} | |
}; | |
with (conn) { | |
privmsg = defaultChanHandler /fix/ ""; | |
tp.addTask(aborted /apply/ (ref bool aborted) { | |
auto sem = new Semaphore; | |
bool delegate(string) dg; | |
dg = delegate bool(string s) { | |
if (s.startsWith("MODE")) { | |
sem.release; | |
removeCallback(dg); | |
} | |
return true; | |
}; | |
addCallback(dg); | |
sem.acquire; | |
auto onstart = onStart(args[0]); | |
force_auth = true; | |
scope(exit) { /* work around race condition */ sleep (4); force_auth = false; } | |
if (onstart.length) { | |
auto mesg1 = cast(hostmask) (cast(string) root_user ~ "!@"), mesg2 = onstart.take(); | |
logln("first mesg ", mesg1, ", ", mesg2); | |
privmsg(mesg1, mesg2); | |
} | |
sleep (5); | |
if (aborted) return; | |
join(args[2], defaultChanHandler); | |
foreach (line; onstart) { | |
privmsg(cast(hostmask) (cast(string) root_user ~ "!@"), line); | |
sleep (1); | |
} | |
}); | |
if (hdl_t.length) { | |
if (hdl_t.startsWith("#") && args[2] != hdl_t) join(hdl_t, defaultChanHandler); | |
message(hdl_t, Format("Executable reloaded at request of ", reload_nick, ", ", count, " times on this connection. ")); | |
if (hdl_t.startsWith("#")) | |
whoare(hdl_t, stuple(conn, hdl_t) /apply/ | |
(IRCconn conn, string channel, string nick, string line) { conn.addUser(channel, nick); }, { } | |
); | |
hdl_t = null; | |
} | |
try { | |
loop(); | |
} catch (Exception ex) { | |
/*foreach (channel, bogus; channels) { | |
message(channel, ex.toString~". Time to die. :("); | |
}*/ | |
conn.invalidateHandlers(); | |
logln(ex, " Rethrowing. "); | |
throw ex; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment