fork from production

hash-command-arbitrary
Avril 5 years ago
commit 0e3b24a641

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

@ -0,0 +1,103 @@
<?PHP
#error_reporting(E_ALL);
// Examples:
// $str = base85::encode("Hello world!");
// $str = base85::decode(":e4D*;K$&\Er");
class base85
{
public static function decode($str) {
$str = preg_replace("/ \t\r\n\f/","",$str);
$str = preg_replace("/z/","!!!!!",$str);
$str = preg_replace("/y/","+<VdL/",$str);
// Pad the end of the string so it's a multiple of 5
$padding = 5 - (strlen($str) % 5);
if (strlen($str) % 5 === 0) {
$padding = 0;
}
$str .= str_repeat('u',$padding);
$num = 0;
$ret = '';
// Foreach 5 chars, convert it to an integer
while ($chunk = substr($str, $num * 5, 5)) {
$tmp = 0;
foreach (unpack('C*',$chunk) as $item) {
$tmp *= 85;
$tmp += $item - 33;
}
// Convert the integer in to a string
$ret .= pack('N', $tmp);
$num++;
}
// Remove any padding we had to add
$ret = substr($ret,0,strlen($ret) - $padding);
return $ret;
}
public static function encode($str) {
$ret = '';
$debug = 0;
$padding = 4 - (strlen($str) % 4);
if (strlen($str) % 4 === 0) {
$padding = 0;
}
if ($debug) {
printf("Length: %d = Padding: %s<br /><br />\n",strlen($str),$padding);
}
// If we don't have a four byte chunk, append \0s
$str .= str_repeat("\0", $padding);
foreach (unpack('N*',$str) as $chunk) {
// If there is an all zero chunk, it has a shortcut of 'z'
if ($chunk == "\0") {
$ret .= "z";
continue;
}
// Four spaces has a shortcut of 'y'
if ($chunk == 538976288) {
$ret .= "y";
continue;
}
if ($debug) {
var_dump($chunk); print "<br />\n";
}
// Convert the integer into 5 "quintet" chunks
for ($a = 0; $a < 5; $a++) {
$b = intval($chunk / (pow(85,4 - $a)));
$ret .= chr($b + 33);
if ($debug) {
printf("%03d = %s <br />\n",$b,chr($b+33));
}
$chunk -= $b * pow(85,4 - $a);
}
}
// If we added some null bytes, we remove them from the final string
if ($padding) {
$ret = preg_replace("/z$/",'!!!!!',$ret);
$ret = substr($ret,0,strlen($ret) - $padding);
}
return $ret;
}
} // End of base85 class

@ -0,0 +1,152 @@
<?php
require_once "/main.inc";
class Encryption
{
public $publicKey=null;
public $privateKey=null;
private $res=null;
private $dt=null;
private $psk=null;
private $upsk=false;
private static $sklen = 4096;
private static $config = array(
"digest_alg" => "sha512",
"private_key_bits" => 4096,
"private_key_type"=> OPENSSL_KEYTYPE_RSA,
);
private function generateNewSymmetricKey()
{
return openssl_random_pseudo_bytes(self::$sklen);
}
private function _begin($prec)
{
$this->res = openssl_pkey_new(self::$config);
openssl_pkey_export($this->res, $this->publicKey);
$this->dt=openssl_pkey_get_details($this->res);
$this->publicKey=$this->dt["key"];
if($prec)
{
$this->upsk = true;
$this->psk = generateNewSymmetricKey();
}
}
public function __construct()
{
$this->_begin(false);
}
public function __construct1($p)
{
$this->_begin($p);
}
private function s_decrypt($data, $key)
{
return s_encrypt($data, $key); //same;
}
private function s_encrypt($data, $key)
{
$j=0;
$out = "";
for($i=0;$i<strlen($data);$i++)
{
if($j>=strlen($key)) $j=0;
$out .= ($data[$i] ^ $key[$j]);
$j+=1;
}
return $out;
}
public function encrypt($data)
{
$skey = ($this->upsk?$this->psk:$this->generateNewSymmetricKey());
$e_skey = null;
openssl_public_encrypt($skey, $e_skey, $this->publicKey);
$e_data = $this->s_encrypt($data, $skey);
return CreateFromEncryption(array("ekey"=>$e_skey, "edata"=>$e_data));
}
public function decrypt(EncryptedData $ed)
{
$ekey = $ed->e_symmetricKey();
$edata = $ed->e_data();
$skey = null;
openssl_private_decrypt($ekey, $skey, $this->privateKey);
return $this->s_decrypt($edata, $skey);
}
public function encdata_create($data)
{
return EncryptedData::CreateFromData($data);
}
}
class EncryptedData
{
private $ekey=null;
private $edata=null;
private function __construct(Array $data)
{
$this->ekey = $data["ekey"];
$this->edata = $data["edata"];
}
public function __construct1($ek, $ed)
{
$this->ekey = $ek;
$this->edata = $ed;
}
private function __construct2()
{
}
public function binaryData($set=null)
{
if($set==null) {
return "{".strlen($this->ekey).", ".strlen($this->edata)."}".$this->ekey.$this->edata;
}
else
{
$kvals = explode(", ", substr(substr($set, 0, strpos($set, "}")), 1));
$klen = intval($kvals[0]);
$dlen = intval($kvals[1]);
$this->ekey = substr($set, strpos($set, "}")+1, $klen);
$this->edata = substr($set, strpos($set, "}")+1+$klen, $dlen);
}
}
public function e_symmetricKey($val=null)
{
if($val==null) {
return $this->ekey;
}
else
{
$this->ekey = $val;
}
}
public function e_data($val=null)
{
if($val==null) {
return $this->edata;
}
else
{
$this->edata = $val;
}
}
public static function CreateFromData($data)
{
$val = new EncryptedData();
$val->binaryData($data);
return $val;
}
public static function CreateFromEncryption(Array $data)
{
return new EncryptedData($data);
}
}
?>

@ -0,0 +1,651 @@
<?php
define('TIMEALLOWED', (60*15));
define('TIME_MINUTE', (60));
define('TIME_HOUR', (60*60));
define('TIME_DAY', (60*60*24));
define('TIME_WEEK', (60*60*24*7));
define('TIME_MONTH', (60*60*24*7*5));
define('TIME_YEAR', (60*60*24*365));
define("SQL_DEFAULT_HOST_NAME", "localhost");
define("SQL_DEFAULT_USER", "root");
define("SQL_DEFAULT_PASSWORD", "root");
if(isLogForce()) doLoggerOnce();
if(isForced())
{
if(isBanned(getip())) die();
if(!isActive())
{
if(getAdminLevel(getip())===false) die();
}
}
function setForceLog($fl)
{
file_put_contents("/forcelog", $fl?"true":"false");
}
function isLogForce()
{
$ar = file_get_contents("/forcelog");
return streq($ar,"true");
}
function bcrypt_verify($name, $hash)
{
return bcrypt($name) == $hash;
}
function bcrypt($str)
{
return crypt($str, '_J9..rasm');
}
function pa($str)
{
$out="";
$col="";
$level==0;
$fonttags=0;
$vr=0;
for($i=0;$i<strlen($str);$i++)
{
if($str[$i] == "\n")
{
if($level==1) {
$out .="<font color=#$col>";
$vr+=1;
$col="";
$level-=1;
}
else
$level+=1;
}
elseif($level==1)
{
$col.=$str[$i];
}
else {
$out.=$str[$i];
}
}
return $out.str_repeat("</font>", $vr);
}
function phplib($nm)
{
require_once "/phplib/$nm";
}
function jtostring($val)
{
$out = "{";
$keys = array_keys($val);
for($i=0;$i<count($keys);$i++)
{
$key = is_array($keys[$i])?jtostring($keys[$i]):$keys[$i];
$value = is_array($val[$keys[$i]])?jtostring($val[$keys[$i]]):$val[$keys[$i]];
$out .= "[$key] = [$value]".(($i==(count($keys)-1))?"":", ");
}
return $out . "}";
}
function setupNotifications()
{
jslib("notify.js");
}
function isActive()
{
return streq(file_get_contents("/active"), "true");
}
function getLastMember($array)
{
$el = end($array);
reset($array);
return $el;
}
function arraydup($ar)
{
return unserialize(serialize($ar));
}
function getSessionInfo($ip=null)
{
$sql = beginSQL("server");
$res = mysql_query_return_assoc("SELECT * FROM sessionInfo ".($ip!=null?"WHERE `ip` = '$ip' ":"")."LIMIT 0, 1");
$ret=null;
if($res!==false)
{
$ret = $res;
}
@mysql_free_result_array($res);
endSQL($sql);
return $ret;
}
function defaultNotify()
{
notifyBar("__user_message");
notifyBar("__temp_message");
notifyBar("__get_message");
notifyBar("__server_message");
if(isset($_REQUEST["msg"]))
js("setNotifyBar('#__get_message', '".$_REQUEST["msg"]."', 2000);");
js("defnot_set_load_user_message(\"".getip()."\");
defnot_set_load_server_message();");
}
function startsWith($haystack, $needle) {
// search backwards starting from haystack length characters from the end
return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== FALSE;
}
function endsWith($haystack, $needle) {
// search forward starting from end minus needle length characters
return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0 && strpos($haystack, $needle, $temp) !== FALSE);
}
function checkTempNotify()
{
if(tempNotifyExists())
{
$vl = readTempNotify();
js("setNotifyBar('#__temp_message', '".addslashes($vl)."', 3000);");
}
}
function notifyBar($id)
{
echo("<div class='none' style='font-family: Consolas; font-size: 20px; text-align: center' id='$id'></div>");
}
function getifexists($ar, $vl)
{
if(isset($ar[$vl])) return $ar[$vl];
else return null;
}
function jserialise($ar) {return jsserialise($ar);}
function jsserialise($ar)
{
$keys = array_keys($ar);
$values = array_values($ar);
$out = "";
for($i=0;$i<count($values);$i++)
{
$key = (is_array($keys[$i])?"array_".base64_encode(jsserialise($keys[$i])):base64_encode($keys[$i]));
$value = (is_array($values[$i])?"array_".base64_encode(jsserialise($values[$i])):base64_encode($values[$i]));
$out .= "$key-$value\r\n";
}
return $out;
}
function setForced($vl)
{
file_put_contents("/force", $vl?"true":"false");
}
function isForced()
{
return streq(file_get_contents("/force"), "true");
}
function readUntil($str, $c)
{
$pos = strpos($str, $c);
if($pos!==false&&$pos>0)
{
return substr($str, 0, $pos-1);
}
else return $str;
}
function junserialise($ar) {return jsunserialise($ar);}
function jsunserialise($ar)
{
$ars= array();
foreach(explode("\r\n", $ar) as $line)
{
if($line!="") {
$ur = explode("-", $line);
$key = startsWith($ur[0], "array_")?junserialise(base64_decode(substr($ur[0], 6))):base64_decode($ur[0]);
$value = startsWith($ur[1], "array_")?junserialise(base64_decode(substr($ur[1], 6))):base64_decode($ur[1]);
$ars[$key] = $value;
}
}
return $ars;
}
function formatDateTime($dt)
{
if($dt<TIME_MINUTE)
return $dt." seconds ago";
elseif($dt<TIME_HOUR)
return round($dt/TIME_MINUTE)." minutes ago";
elseif($dt<TIME_DAY)
return round($dt/TIME_HOUR)." hours ago";
elseif($dt<TIME_WEEK)
return round($dt/TIME_DAY)." days ago";
elseif($dt<TIME_MONTH)
return round($dt/TIME_WEEK)." weeks ago";
elseif($dt<TIME_YEAR)
return round($dt/TIME_MONTH)." months ago";
else
return (round(($dt/TIME_YEAR)*100.00)/100.00)." years ago";
}
function jss($script)
{
echo ("<script src='$script'></script>");
}
function jslib($name)
{
jss("/jslib/$name");
}
function isBanned($ip)
{
$sql = beginSQL("server");
$res = mysql_query_return_assoc("SELECT banned FROM sessionInfo WHERE `ip`='$ip'");
$ret = $res[0]["banned"];
@mysql_free_result_array($res);
endSQL($sql);
return $ret;
}
function redirect($page,$wtime=0)
{
echo '<meta http-equiv="REFRESH" content="'.$wtime.';url='.$page.'"></HEAD>';
}
function getAdminLevel($ip)
{
$sql = beginSQL("server");
$res = mysql_query_return_assoc("SELECT `level` FROM admins WHERE `ip`='$ip'");
$ret=false;
if($res != array())
{
$ret = $res[0]["level"];
}
@mysql_free_result_array($res);
endSQL($sql);
return $ret;
}
function banUser($ip)
{
$sql = beginSQL("server");
mysql_query("UPDATE sessionInfo SET
`banned`=1
WHERE `ip`='$ip'");
endSQL($sql);
}
function unbanUser($ip)
{
$sql = beginSQL("server");
mysql_query("UPDATE sessionInfo SET
`banned`=0
WHERE `ip`='$ip'");
endSQL($sql);
}
function jquery()
{
jslib("jquery-1.8.2.js");
jslib("jquery.base64.js");
}
function js($str)
{
echo("<script>$str</script>");
}
function issetmultiple($ar, $vls)
{
$val = true;
foreach($vls as $r) $val = $val && isset($ar[$r]);
return $val;
}
function pageinit_n($sql=null)
{
pageinit($sql);
jquery();
jslib("notify.js");
enableBanCheck();
doActiveCheck();
}
function pathfix($str)
{
return str_replace("\\", "/", $str);
}
function sqldate($stamp=null)
{
return date("y.m.d H.i.s", $stamp==null?time():$stamp);
}
function enableBanCheck()
{
jslib("check.js");
js("banCheck();");
if(isBanned(getip())) doBan();
}
function activeNotifyBar($id, $val)
{
echo("<div class='notify' style='font-family: Consolas; font-size: 20px; text-align: center' id='$id'>$val</div>");
}
function doActiveCheck()
{
if(!isActive()) {
if(getAdminLevel(getip())!==false)
{
echo("<div class='notify' style='font-family: Consolas; font-size: 20px; text-align: center' id='__active_n'>WEBSITE OFFLINE</div>");
}
else
die("website offline");
}
}
function doBan()
{
redirect("/banned.php");
die();
}
function pageinit($vsql=null)
{
$sql = $vsql==null?beginSQL("server"):$vsql;
if($vsql!=null)
{
mysql_select_db("server",$sql);
}
$res = mysql_query_return_assoc("SELECT * FROM `sessionInfo` WHERE `ip`='".getip()."'");
echo(mysql_error());
if($res==array())
{
mysql_query("INSERT INTO sessionInfo (
ip, url, sessionid, last
) VALUES (
'".getip()."',
'".geturl()."',
".(sessionid()==null||streq(sessionid(),"")?"NULL":"'".$_REQUEST["PHPSESSID"]."'").",
'".date("y.m.d H:i:s")."'
)");
echo(mysql_error());
}
else
{
mysql_query("UPDATE sessionInfo SET
url='".geturl()."',
sessionid=".(sessionid()==null||streq(sessionid(),"")?"NULL":"'".$_REQUEST["PHPSESSID"]."'").",
last='".date("y.m.d H:i:s")."',
views=views+1
WHERE
`ip`='".getip()."'
");
echo(mysql_error());
}
@mysql_free_result_array($res);
doLogger();
if($vsql==null) endSQL($sql);
}
function doLoggerOnce()
{
$sql = beginSQL("server");
doLogger();
endSQL($sql);
}
function setLogAll($l)
{
file_put_contents("/logall", $l?"true":"false");
}
function isLogAll()
{
return streq(file_get_contents("/logall"), "true")?true:false;
}
function js_onKeyPress($id, $func, $key)
{
js('$(document).ready(function()
{
$("'.$id.'").keypress(function (e){
if(e.keyCode=='.$key.')
'.$func.';
});
});');
}
function js_onEnterPress($id, $func)
{
js_onKeyPress($id, $func, 13);
}
function doLogger()
{
$r = mysql_query_return_assoc("SELECT * FROM logInfo WHERE `ip`='".getip()."'");
if(($r!=array())||isLogAll())
{
mysql_query("INSERT INTO logs (
ip, url, session, time
) VALUES (
'".getip()."',
'".geturl()."',
".(sessionid()==null||streq(sessionid(),"")?"NULL":"'".$_REQUEST["PHPSESSID"]."'").",
'".sqldate()."'
)");
}
else @mysql_free_result_array($r);
}
function writeAllText($fn, $txt)
{
$p = fopen($fn, "w");
fwrite($p, $txt);
fclose($p);
}
function sessionid()
{
return $_REQUEST["PHPSESSID"];
}
function geturl() {
$actual_link = "http://".$_SERVER["HTTP_HOST"].$_SERVER["REQUEST_URI"];
return $actual_link;
}
function getmsg()
{
$txt = @readalltext("\message");
if(streq($txt,"")) return NULL;
return $txt;
}
function button($value, $onclick=null,$other="")
{
echo("<input type='button' value='$value' ".($onclick==null?"":"onclick='$onclick' ")." $other />");
}
function sessionActive()
{
return sessionid() !== '';
}
function setTempNotify($msg)
{
$_SESSION["msg"]=$msg;
}
function tempNotifyExists()
{
return isset($_SESSION["msg"]);
}
function readTempNotify()
{
$msg=null;
if(isset($_SESSION["msg"])) {
$msg = $_SESSION["msg"];
unset($_SESSION["msg"]);
}
return $msg;
}
function index($ar, $i)
{
return $ar[$i];
}
function mysql_query_return_assoc($q)
{
$res = (mysql_query($q));
$ret=array();
$i=0;
if(!$res) echo(mysql_error());
else
while($cur = mysql_fetch_assoc($res))
{
$ret[$i++] = $cur;
}
//mysql_free_result($q);
return $ret;
}
function readAllText($fn)
{
$fp = fopen($fn,"r");
$data = fread($fp, filesize($fn));
fclose($fp);
return $data;
}
function beginSQL($db)
{
$con = mysql_connect(SQL_DEFAULT_HOST_NAME, SQL_DEFAULT_USER, SQL_DEFAULT_PASSWORD);
mysql_select_db($db, $con);
return $con;
}
function endSQL($con)
{
mysql_close($con);
}
function css($css)
{
echo '<LINK href="'.$css.'" rel="stylesheet" type="text/css">';
}
function css_main($dir="/")
{
echo '<LINK href="'.$dir.'css/main.css" rel="stylesheet" type="text/css">';
}
function title($title, $others="",$id=null)
{
echo "<p id='center' $others class='title' ".($id==null?"":"id='".$id."'").">$title</p>\n";
echo "<title>$title</title>";
}
function mysql_free_result_array($res)
{
foreach($res as $re)
mysql_free_result($re);
}
function lb()
{
echo("<br />");
}
function para()
{
echo("<p />");
}
function validip($ip)
{
return preg_match("~([0-9]{1,3}[.]){3,3}[0-9]{1,3}~",$ip);
}
function getip()
{
if (validip($_SERVER["HTTP_CLIENT_IP"])) {
return $_SERVER["HTTP_CLIENT_IP"];
}
foreach (explode(",",$_SERVER["HTTP_X_FORWARDED_FOR"]) as $ip) {
if (validip(trim($ip))) {
return $ip;
}
}
if (validip($_SERVER["HTTP_X_FORWARDED"])) {
return $_SERVER["HTTP_X_FORWARDED"];
} elseif (validip($_SERVER["HTTP_FORWARDED_FOR"])) {
return $_SERVER["HTTP_FORWARDED_FOR"];
} elseif (validip($_SERVER["HTTP_FORWARDED"])) {
return $_SERVER["HTTP_FORWARDED"];
} elseif (validip($_SERVER["HTTP_X_FORWARDED"])) {
return $_SERVER["HTTP_X_FORWARDED"];
} else {
return $_SERVER["REMOTE_ADDR"];
}
}
function link_tab($res, $txt, $other="")
{
link($res, $txt, "target='_black' ".$other);
}
function link($res, $txt, $other="")
{
echo("<a $other href='$res'>$txt</a>");
}
function streq($str,$str2)
{
return strcmp($str,$str2)==0;
}
function echoline($ln)
{
echo($ln); lb();
}
function echopara($pa)
{
echo($pa); para();
}
class Session {
public static function unserialize($session_data) {
$method = ini_get("session.serialize_handler");
switch ($method) {
case "php":
return self::unserialize_php($session_data);
break;
case "php_binary":
return self::unserialize_phpbinary($session_data);
break;
default:
throw new Exception("Unsupported session.serialize_handler: " . $method . ". Supported: php, php_binary");
}
}
private static function unserialize_php($session_data) {
$return_data = array();
$offset = 0;
while ($offset < strlen($session_data)) {
if (!strstr(substr($session_data, $offset), "|")) {
throw new Exception("invalid data, remaining: " . substr($session_data, $offset));
}
$pos = strpos($session_data, "|", $offset);
$num = $pos - $offset;
$varname = substr($session_data, $offset, $num);
$offset += $num + 1;
$data = unserialize(substr($session_data, $offset));
$return_data[$varname] = $data;
$offset += strlen(serialize($data));
}
return $return_data;
}
private static function unserialize_phpbinary($session_data) {
$return_data = array();
$offset = 0;
while ($offset < strlen($session_data)) {
$num = ord($session_data[$offset]);
$offset += 1;
$varname = substr($session_data, $offset, $num);
$offset += $num;
$data = unserialize(substr($session_data, $offset));
$return_data[$varname] = $data;
$offset += strlen(serialize($data));
}
return $return_data;
}
public static function serialize( $array, $safe = true ) {
// the session is passed as refernece, even if you dont want it to
if( $safe )
$array = unserialize(serialize( $array )) ;
//var_dump($array);
$raw = '' ;
$line = 0 ;
$keys = array_keys( $array ) ;
foreach( $keys as $key ) {
$value = $array[ $key ] ;
$line ++ ;
$raw .= $key .'|' ;
if( is_array( $value ) && isset( $value['huge_recursion_blocker_we_hope'] )) {
$raw .= 'R:'. $value['huge_recursion_blocker_we_hope'] . ';' ;
} else {
$raw .= serialize( $value ) ;
}
$array[$key] = Array( 'huge_recursion_blocker_we_hope' => $line ) ;
}
return $raw ;
}
}
?>

@ -0,0 +1,71 @@
<?php
require_once "/main.inc";
define("CHUNKSIZE", 512);
function imagexy($im)
{
return array(imagesx($im), imagesy($im));
}
class ImageScrambler
{
public $source=null;
public $seed=null;
public function __construct($image, $rndr)
{
$this->source=$image;
$this->seed = $rndr;
srand($rndr);
}
private function getAreas()
{
$v = imagexy($this->source);
$xv = $v[0] % CHUNKSIZE;
$yv = $v[1] % CHUNKSIZE;
$ret = array();
$i=0;
for($y=0;$i<$yv;$y++)
{
for($x=0;$x<$xv;$x++)
{
$ret[$i++] = array($x*CHUNKSIZE, $y*CHUNKSIZE,CHUNKSIZE,CHUNKSIZE);
}
}
return $ret;
}
private function _swap($ar,$i,$j)
{
$cp = $ar[$i];
$ar[$i] = $ar[$j];
$ar[$j] = $cp;
}
private function scrambleArea($ar)
{
for($i=count($ar)-1;$i>=0;$i--)
{
$this->_swap($ar, $i, rand(0, $i));
}
return $ar;
}
public function scramble()
{
$area = $this->getAreas();
$sarea = $this->scrambleArea($area);
$nw = imagecreate(imagesx($this->source), imagesy($this->source));
for($i=0;count($area);$i+=2)
{
imagecopy($nw, $this->source, $sarea[$i+1][0], $sarea[$i+1][1], $sarea[$i][0], $sarea[$i][1],CHUNKSIZE,CHUNKSIZE);
}
imagedestroy($this->source);
$this->source=$nw;
return $sarea;
}
public function destroy()
{
imagedestroy($this->$source);
}
}
?>

@ -0,0 +1,18 @@
<?php
require_once "/main.inc";
function _plist_isName($name, $val)
{
return password_verify($name, $val);
}
function _plist_name($nm) {
return bcrypt($nm);
}
function plist_add($name, $value)
{
$val = _plist_name($name);
}
?>

@ -0,0 +1,119 @@
<?php
require_once "/main.inc";
function mp4video($file) {
$fp = @fopen($file, 'rb');
$size = filesize($file); // File size
$length = $size; // Content length
$start = 0; // Start byte
$end = $size - 1; // End byte
header('Content-type: video/mp4');
//header("Accept-Ranges: 0-$length");
header("Accept-Ranges: bytes");
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $start;
$c_end = $end;
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (strpos($range, ',') !== false) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
exit;
}
if ($range == '-') {
$c_start = $size - substr($range, 1);
}else{
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
}
$c_end = ($c_end > $end) ? $end : $c_end;
if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
exit;
}
$start = $c_start;
$end = $c_end;
$length = $end - $start + 1;
fseek($fp, $start);
header('HTTP/1.1 206 Partial Content');
}
header("Content-Range: bytes $start-$end/$size");
header("Content-Length: ".$length);
$buffer = 1024 * 8;
while(!feof($fp) && ($p = ftell($fp)) <= $end) {
if ($p + $buffer > $end) {
$buffer = $end - $p + 1;
}
set_time_limit(0);
echo fread($fp, $buffer);
flush();
}
fclose($fp);
exit();
}
function webmvideo($file) {
$fp = @fopen($file, 'rb');
$size = filesize($file); // File size
$length = $size; // Content length
$start = 0; // Start byte
$end = $size - 1; // End byte
header('Content-type: video/webm');
//header("Accept-Ranges: 0-$length");
header("Accept-Ranges: bytes");
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $start;
$c_end = $end;
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (strpos($range, ',') !== false) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
exit;
}
if ($range == '-') {
$c_start = $size - substr($range, 1);
}else{
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
}
$c_end = ($c_end > $end) ? $end : $c_end;
if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
exit;
}
$start = $c_start;
$end = $c_end;
$length = $end - $start + 1;
fseek($fp, $start);
header('HTTP/1.1 206 Partial Content');
}
header("Content-Range: bytes $start-$end/$size");
header("Content-Length: ".$length);
$buffer = 1024 * 8;
while(!feof($fp) && ($p = ftell($fp)) <= $end) {
if ($p + $buffer > $end) {
$buffer = $end - $p + 1;
}
set_time_limit(0);
echo fread($fp, $buffer);
flush();
}
fclose($fp);
exit();
}
?>

@ -0,0 +1,3 @@
[Trash Info]
Path=static/www/.file
DeletionDate=2018-07-07T16:06:51

@ -0,0 +1,3 @@
[Trash Info]
Path=rtbw/Untitled%20Folder
DeletionDate=2018-08-29T14:14:52

@ -0,0 +1,3 @@
[Trash Info]
Path=rtbw/Untitled%20Folder
DeletionDate=2018-08-13T18:01:21

@ -0,0 +1,3 @@
[Trash Info]
Path=static/www/i/bg%20%28copy%29.png
DeletionDate=2018-05-11T11:36:18

@ -0,0 +1,3 @@
[Trash Info]
Path=static/www/i/bg.png
DeletionDate=2018-05-11T11:36:13

@ -0,0 +1,3 @@
[Trash Info]
Path=rtbw/maintain/ctl.js
DeletionDate=2018-08-29T14:15:18

@ -0,0 +1,3 @@
[Trash Info]
Path=static/old
DeletionDate=2018-05-11T10:07:58

37
.gitignore vendored

@ -0,0 +1,37 @@
static/
git/
rtbw/
www.old/
www/.*/
api/
www/favicon.ico
*.node
*.pid
*.swp
.build
.lock*
.DS_Store
authdump
config.js
graveyard
error.log
findapng
hot.js
jsmin
node_modules/
package-lock.json
perceptual
imager/tmp
state
tripcode/.build
tripcode/build
upkeep/credentials.json
voice/*.mp3
www/archive
www/js/client*.js
www/js/vendor*.js
www/src
www/thumb
www/mid
www/vint

@ -0,0 +1,19 @@
Copyright (C) 2010-2017 Lal'C Mellk Mal
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,14 @@
all:
$(MAKE) -C imager
$(MAKE) -C tripcode
client:
@echo 'make client' is no longer necessary
@false
.PHONY: all clean client
clean:
rm -rf -- .build state www/js/{client,vendor}{.,-}*.js
$(MAKE) -C imager -w clean
$(MAKE) -C tripcode -w clean

@ -0,0 +1,50 @@
Real-time imageboard.
MIT licensed.
Setup:
* Install dependencies listed below
* Sign up for reCAPTCHA
* Create a GitHub Application (callback URL = site URL + /login)
* Copy config.js.example to config.js and configure
* Copy hot.js.example to hot.js and configure
* Copy imager/config.js.example to imager/config.js and configure
* Copy report/config.js.example to report/config.js and configure
* You might need to run `npm install -g node-gyp`
* Run `npm install` to install npm deps and compile a few helpers
* Run `node builder.js` to run an auto-reloading development server
Production:
* Have your webserver serve www/ (or wherever you've moved src, thumb, etc.)
- Configure `imager.config.MEDIA_URL` appropriately
- Then turn off `SERVE_STATIC_FILES` and `SERVE_IMAGES`
* If you're behind Cloudflare turn on `CLOUDFLARE`
- Or if you're behind any reverse proxy (nginx etc) turn on `TRUST_X_FORWARDED_FOR`
* Run `node server/server.js` for just the server
* You can update client code & hot.js on-the-fly with `node server/kill.js`
* For nginx hosting/reverse proxying, refer to docs/nginx.conf.example
* For a sample init script, refer to docs/doushio.initscript.example
* config.DAEMON support is old and broken, PRs welcome
Dependencies:
* ImageMagick
* libpng
* node.js + npm
* redis
* ffmpeg 2.2+ if supporting WebM
* jhead and jpegtran optionally, for EXIF autorotation
Optional npm deps for various features:
* ~~daemon~~ (broken currently)
* icecast now-playing banners: node-expat
* [send](https://github.com/visionmedia/send) (if you want to serve static files directly from the node.js process; useful in debug mode also)
Standalone upkeep scripts:
* archive/daemon.js - moves old threads to the archive
* upkeep/backup.js - uploads rdb to S3
* upkeep/clean.js - deletes archived images
* upkeep/radio.js - icecast2 server integration

@ -0,0 +1,5 @@
* Update `syncs` in client when on /live
* Actually tell user when a particular thread failed to sync
* Tokenize text instead of multiple levels of regexps
* Should retrieve live body and hctr in one transaction
* Consolidate or disambiguate the (three!) different hook/trigger mechanisms

@ -0,0 +1,473 @@
/* NOTE: This file is processed by server/state.js
and served by server/server.js (to auth'd users only) */
$('<link>', {rel: 'stylesheet', type: 'text/css', href: mediaURL+'css/mod.css'}).appendTo('head');
var $selectButton, $controls;
window.loggedInUser = IDENT.user;
window.x_csrf = IDENT.csrf;
function show_toolbox() {
var specs = [
{name: 'Lewd', kind: 7},
{name: 'Porn', kind: 8},
{name: 'Delete', kind: 9},
{name: 'Lock', kind: 11},
];
if (IDENT.auth == 'Admin')
specs.push({name: 'Panel', kind: 'panel'});
var $toolbox = $('<div/>', {id: 'toolbox', "class": 'mod'});
$selectButton = $('<input />', {
type: 'button', val: 'Select',
click: function (e) { toggle_multi_selecting(); },
});
$toolbox.append($selectButton, ' ');
$controls = $('<span></span>').hide();
_.each(specs, function (spec) {
$controls.append($('<input />', {
type: 'button',
val: spec.name,
data: {kind: spec.kind},
}), ' ');
});
$controls.on('click', 'input[type=button]', tool_action);
_.each(delayNames, function (when, i) {
var id = 'delay-' + when;
var $label = $('<label/>', {text: when, 'for': id});
var $radio = $('<input/>', {
id: id, type: 'radio', val: when, name: 'delay',
});
if (i == 0)
$radio.prop('checked', true);
$controls.append($radio, $label, ' ');
});
$toolbox.append($controls).insertBefore(THREAD ? 'hr:last' : $ceiling);
}
function tool_action(event) {
var ids = [];
var $sel = $('.selected');
$sel.each(function () {
var id = extract_num(parent_post($(this)));
if (id)
ids.push(id);
});
var $button = $(this);
var kind = $button.data('kind');
if (kind == 'panel')
return toggle_panel();
/* On a thread page there's only one thread to lock, so... */
if (kind == 11 && THREAD && !ids.length)
ids = [THREAD];
if (ids.length) {
var when = $('input:radio[name=delay]:checked').val();
ids.unshift(parseInt(kind, 10), {when: when});
send(ids);
$sel.removeClass('selected');
}
else {
var orig = $button.val();
var caption = _.bind($button.val, $button);
caption('Nope.');
if (orig != 'Nope.')
_.delay(caption, 2000, orig);
}
}
readOnly.push('graveyard');
menuOptions.unshift('Select');
menuHandlers.Hide = function () { alert('nope.avi'); };
var multiSelecting = false;
function toggle_multi_selecting(model, $post) {
var oldTarget;
if ($post && model) {
oldTarget = lockTarget;
set_lock_target(model.id);
}
with_dom(function () {
multiSelecting = !multiSelecting;
if (multiSelecting) {
$('body').addClass('multi-select');
make_selection_handle().prependTo('article');
make_selection_handle().prependTo('section > header');
if ($post)
select_post($post);
$controls.show();
$selectButton.val('X');
}
else {
$('body').removeClass('multi-select');
$('.select-handle').remove();
$controls.hide();
$selectButton.val('Select');
}
});
if ($post)
set_lock_target(oldTarget);
}
menuHandlers.Select = toggle_multi_selecting;
function enable_multi_selecting() {
if (!multiSelecting)
toggle_multi_selecting();
}
function select_post($post) {
$post.find('.select-handle:first').addClass('selected');
}
function make_selection_handle() {
return $('<a class="select-handle" href="#"/>');
}
window.fun = function () {
send([33, THREAD]);
};
override(ComposerView.prototype, 'make_alloc_request',
function (orig, text, img) {
var msg = orig.call(this, text, img);
if ($('#authname').is(':checked'))
msg.auth = IDENT.auth;
return msg;
});
$DOC.on('click', '.select-handle', function (event) {
event.preventDefault();
$(event.target).toggleClass('selected');
});
with_dom(function () {
$('h1').text('Moderation - ' + $('h1').text());
var $authname = $('<input>', {type: 'checkbox', id: 'authname'});
var $label = $('<label/>', {text: ' '+IDENT.auth}).prepend($authname);
$name.after(' ', $label);
/* really ought to be done with model observation! */
$authname.change(function () {
if (postForm)
postForm.propagate_ident();
});
oneeSama.hook('fillMyName', function ($el) {
var auth = $('#authname').is(':checked');
$el.toggleClass(IDENT.auth.toLowerCase(), auth);
if (auth)
$el.append(' ## ' + IDENT.auth)
});
Backbone.on('afterInsert', function (model, $el) {
if (multiSelecting)
make_selection_handle().prependTo($el);
});
show_toolbox();
});
var Address = Backbone.Model.extend({
idAttribute: 'key',
defaults: {
count: 0,
},
});
var AddressView = Backbone.View.extend({
className: 'mod address',
events: {
'keydown .name': 'entered_name',
'click .sel-all': 'select_all',
'click .ban': 'ban',
},
initialize: function () {
var $el = this.$el;
$('<span/>', {"class": 'ip'}).appendTo($el);
$el.append(' &nbsp; ', $('<input/>', {
"class": 'sel-all',
type: 'button',
val: 'Sel All'
}));
if (IDENT.auth == 'Admin')
$el.append($('<input/>', {
"class": 'ban',
type: 'button',
val: 'Ban'
}));
$el.append(
'<br>',
$('<input>', {"class": 'name', placeholder: 'Name'})
);
this.listenTo(this.model, 'change', this.render);
},
render: function () {
var attrs = this.model.attributes;
if (attrs.shallow) {
this.$('.ip').text(attrs.ip ? attrs.ip + ' "\u2026"' : 'Loading...');
return this;
}
this.$('.ip').text(attrs.ip);
var $name = this.$('.name');
if (!this.focusedName) {
_.defer(function () {
$name.focus().prop({
selectionStart: 0,
selectionEnd: $name.val().length,
});
});
this.focusedName = true;
}
if (attrs.name != $name.val()) {
$name.val(attrs.name);
}
this.$('.ban')
.val(attrs.ban ? 'Unban' : 'Ban');
return this;
},
entered_name: function (event) {
if (event.which != 13)
return;
event.preventDefault();
var name = this.$('.name').val().trim();
var ip = this.model.get('ip');
send([SET_ADDRESS_NAME, ip, name]);
this.remove();
},
remove: function () {
this.trigger('preremove');
Backbone.View.prototype.remove.call(this);
},
select_all: function () {
if (!THREAD)
return alert('TODO');
// TODO: do where query by ip_key lookup
var models = Threads.get(THREAD).get('replies').where({
ip: this.model.get('ip'),
});
if (!models.length)
return;
enable_multi_selecting();
with_dom(function () {
$.each(models, function () {
select_post($('#' + this.id));
});
});
this.remove();
},
ban: function () {
var ip = this.model.get('ip');
var attrs = this.model.attributes;
var act, type;
if (!attrs.ban) {
act = 'Ban';
type = 'timeout';
} else {
act = 'Unban';
type = 'unban';
}
if (!confirm(act + ' ' + ip + '?'))
return;
send([BAN, ip, type]);
// show ... while processing
this.$('.ban').val('...');
},
});
// basically just a link
var AddrView = Backbone.View.extend({
tagName: 'a',
className: 'mod addr',
events: {
click: 'toggle_expansion',
},
initialize: function () {
this.$el.attr('href', '#');
this.listenTo(this.model, 'change:name', this.render);
},
render: function () {
var attrs = this.model.attributes;
var text = ip_mnemonic(attrs.ip);
if (attrs.name)
text += ' "' + attrs.name + '"';
if (attrs.country)
text += ' ' + attrs.country;
this.$el.attr('title', attrs.ip).text(text);
return this;
},
toggle_expansion: function (event) {
if (event.target !== this.el)
return;
event.preventDefault();
if (this.expansion)
return this.expansion.remove();
var ip = this.model.get('ip');
if (!ip)
return;
this.expansion = new AddressView({model: this.model});
this.$el.after(this.expansion.render().el);
this.listenTo(this.expansion, 'preremove',
this.expansion_removed);
if (this.model.get('shallow'))
send([FETCH_ADDRESS, ip]);
},
remove: function () {
if (this.expansion)
this.expansion.remove();
Backbone.View.prototype.remove.call(this);
},
expansion_removed: function () {
this.expansion = null;
},
});
var Addresses = Backbone.Collection.extend({
model: Address,
comparator: function (a) { return ip_mnemonic(a.ip); },
});
window.addrs = new Addresses;
function hook_up_address(model, $post) {
var $a = $post.find('a.mod.addr:first');
if (!$a.length)
return;
var ip = $a.prop('title') || $a.text();
var givenName;
var m = $a.text().match(/^([\w'.:]+(?: [\w'.:]*)?) "(.+)"$/);
if (m) {
if (is_IPv4_ip(m[1]))
ip = m[1];
givenName = m[2];
}
if (!is_valid_ip(ip))
return;
/* Activate this address link */
var key = ip_key(ip);
var address = window.addrs.get(key);
if (!address) {
address = new Address({ip: ip, key: key});
address.set(givenName ? {name: givenName} : {shallow: true});
window.addrs.add(address);
}
var view = new AddrView({model: address, el: $a[0]});
if (address.get('name'))
view.render();
if (model && model.set && !model.has('ip'))
model.set('ip', ip);
}
Backbone.on('afterInsert', hook_up_address);
with_dom(function () {
$('section').each(function () {
var $section = $(this);
var thread = Threads.get(extract_num($section));
hook_up_address(thread, $section);
var replies = thread && thread.get('replies');
$section.find('article').each(function () {
var $post = $(this);
var model = replies && replies.get(extract_num($post));
hook_up_address(model, $post);
});
});
if (/reported/.test(window.location.search))
enable_multi_selecting();
});
window.adminState = new Backbone.Model({
});
var PanelView = Backbone.View.extend({
id: 'panel',
initialize: function () {
this.listenTo(this.model, 'change:visible', this.renderVis);
this.listenTo(window.addrs, 'add change:count reset',
this.renderIPs);
this.listenTo(this.model, 'change:memoryUsage',
this.renderMemory);
this.listenTo(this.model, 'change:addrs change:bans',
this.renderCounts);
this.listenTo(this.model, 'change:uptime', this.renderUptime);
$('<div/>', {id: 'ips'}).appendTo(this.el);
$('<div/>', {id: 'mem'}).appendTo(this.el);
$('<div/>', {id: 'counts'}).appendTo(this.el);
$('<div/>', {id: 'uptime'}).appendTo(this.el);
},
renderVis: function (model, vis) {
this.$el.toggle(!!vis);
},
renderIPs: function () {
var $ips = this.$('#ips').empty();
window.addrs.forEach(function (addr) {
var n = addr.get('count');
if (!n)
return;
var el = new AddrView({model: addr}).render().el;
$ips.append(el, n>1 ? ' ('+n+')' : '', '<br>');
});
},
renderMemory: function (model, mem) {
function mb(n) {
return Math.round(n/1000000);
}
this.$('#mem').text(
mb(mem.heapUsed) + '/' + mb(mem.heapTotal) +
' MB heap used.'
);
},
renderCounts: function (model) {
var a = model.attributes;
this.$('#counts').text(pluralize(a.addrs, 'addr') + ', ' +
pluralize(a.bans, 'ban') + '.');
},
renderUptime: function (model, s) {
var m = Math.floor(s / 60) % 60;
var h = Math.floor(s / 3600) % 60;
var d = Math.floor(s / (3600*24));
h = h ? h+'h' : '';
d = d ? d+'d' : '';
this.$('#uptime').text('Up '+ d + h + m +'m.');
},
});
function toggle_panel() {
var show = !adminState.get('visible');
send([show ? 60 : 61, 'adminState']);
}
if (IDENT.auth == 'Admin') (function () {
var $panel = $('<div/>', {id: 'panel', "class": 'mod modal'}).hide();
var view = new PanelView({model: adminState, el: $panel[0]});
$panel.appendTo('body');
})();

@ -0,0 +1,194 @@
var _ = require('../lib/underscore');
var config = require('../config');
var common = require('../common');
var DEFINES = exports;
DEFINES.FETCH_ADDRESS = 101;
DEFINES.SET_ADDRESS_NAME = 102;
DEFINES.BAN = 103;
var modCache = {}; // TEMP
exports.modCache = modCache;
var suspensionKeys = ['boxes', 'bans', 'slows', 'suspensions', 'timeouts'];
exports.suspensionKeys = suspensionKeys;
var delayNames = ['now', 'soon', 'later'];
var delayDurations = {now: 0, soon: 60, later: 20*60};
exports.delayDurations = delayDurations;
var mnemonicStarts = ',k,s,t,d,n,h,b,p,m,f,r,g,z,l,ch'.split(',');
var mnemonicEnds = "a,i,u,e,o,a,i,u,e,o,ya,yi,yu,ye,yo,'".split(',');
function ip_mnemonic(ip) {
if (/^[a-fA-F0-9:]{3,45}$/.test(ip))
return ipv6_mnemonic(ip);
if (!is_IPv4_ip(ip))
return null;
var nums = ip.split('.');
var mnemonic = '';
for (var i = 0; i < 4; i++) {
var n = parseInt(nums[i], 10);
var s = mnemonicStarts[Math.floor(n / 16)] +
mnemonicEnds[n % 16];
mnemonic += s;
}
return mnemonic;
}
var ipv6kana = (
',a,i,u,e,o,ka,ki,' +
'ku,ke,ko,sa,shi,su,se,so,' +
'ta,chi,tsu,te,to,na,ni,nu,' +
'ne,no,ha,hi,fu,he,ho,ma,' +
'mi,mu,me,mo,ya,yu,yo,ra,' +
'ri,ru,re,ro,wa,wo,ga,gi,' +
'gu,ge,go,za,ji,zu,ze,zo,' +
'da,de,do,ba,bi,bu,be,bo'
).split(',');
if (ipv6kana.length != 64)
throw new Error('bad ipv6 kana!');
var ipv6alts = {
sa: 'sha', su: 'shu', so: 'sho',
ta: 'cha', tsu: 'chu', to: 'cho',
fu: 'hyu',
za: 'ja', zu: 'ju', zo: 'jo',
};
function ipv6_mnemonic(ip) {
var groups = explode_IPv6_ip(ip);
if (!groups || groups.length != 8)
return null;
// takes 8 bits, returns kana
function p(n) {
// bits 0-5 are lookup; 6-7 are modifier
var kana = ipv6kana[n & 0x1f];
if (!kana)
return kana;
var mod = (n >> 6) & 3;
// posibly modify the kana
if (mod > 1 && kana[0] == 'b')
kana = 'p' + kana[1];
if (mod == 3) {
var alt = ipv6alts[kana];
if (alt)
return alt;
var v = kana[kana.length - 1];
if ('knhmrgbp'.indexOf(kana[0]) >= 0 && 'auo'.indexOf(v) >= 0)
kana = kana[0] + 'y' + kana.slice(1);
}
return kana;
}
var ks = groups.map(function (hex) {
var n = parseInt(hex, 16);
return p(n >> 8) + p(n);
});
function cap(s) {
return s[0].toUpperCase() + s.slice(1);
}
// discard first group (RIR etc)
// also discard some other groups for length/anonymity
var sur = ks.slice(1, 4).join('') || 'z';
var given = ks.slice(6, 8).join('') || 'z';
return cap(sur) + ' ' + cap(given);
}
function append_mnemonic(info) {
var header = info.header, ip = info.data.ip;
if (!ip)
return;
var mnemonic = config.IP_MNEMONIC && ip_mnemonic(ip);
var key = ip_key(ip);
// Terrible hack.
if (mnemonic && modCache.addresses) {
var addr = modCache.addresses[key];
if (addr && addr.name)
mnemonic += ' "' + addr.name + '"';
}
var s = common.safe;
var title = mnemonic ? [s(' title="'), ip, s('"')] : '';
header.push(s(' <a class="mod addr"'), title, s('>'),
mnemonic || ip, s('</a>'));
}
function denote_hidden(info) {
if (info.data.hide)
info.header.push(common.safe(
' <em class="mod hidden">(hidden)</em>'));
}
exports.denote_hidden = denote_hidden;
function is_IPv4_ip(ip) {
if (typeof ip != 'string' || !/^\d+\.\d+\.\d+\.\d+$/.exec(ip))
return false;
var nums = ip.split('.');
for (var i = 0; i < 4; i++) {
var n = parseInt(nums[i], 10);
if (n > 255)
return false;
if (n && nums[i][0] == '0')
return false;
}
return true;
}
exports.is_IPv4_ip = is_IPv4_ip;
exports.is_valid_ip = function (ip) {
return typeof ip == 'string' && /^[\da-fA-F.:]{3,45}$/.test(ip);
}
function explode_IPv6_ip(ip) {
if (typeof ip != 'string')
return null;
var groups = ip.split(':');
var gap = groups.indexOf('');
if (gap >= 0 || groups.length != 8) {
// expand ::
if (gap < 0 || gap != groups.lastIndexOf(''))
return null;
var zeroes = [gap, 1];
for (var i = groups.length; i < 9; i++)
zeroes.push('0');
groups.splice.apply(groups, zeroes);
}
// check hex components
for (var i = 0; i < groups.length; i++) {
var n = parseInt(groups[i], 16);
if (_.isNaN(n) || n > 0xffff)
return null;
groups[i] = n.toString(16);
}
return groups;
}
function ip_key(ip) {
if (!is_IPv4_ip(ip)) {
// chop off the last half of IPv6 ips
var bits = explode_IPv6_ip(ip);
if (bits && bits.length == 8)
return bits.slice(0, 4).join(':');
}
return ip;
}
exports.ip_key = ip_key;
if (typeof IDENT != 'undefined') {
/* client */
window.ip_mnemonic = ip_mnemonic;
oneeSama.hook('headerName', append_mnemonic);
oneeSama.hook('headerName', denote_hidden);
}
else {
exports.ip_mnemonic = ip_mnemonic;
exports.append_mnemonic = append_mnemonic;
}

@ -0,0 +1,71 @@
var authcommon = require('./common'),
caps = require('../server/caps'),
common= require('../common'),
okyaku = require('../server/okyaku'),
STATE = require('../server/state');
require('./panel');
function connect() {
return global.redis;
}
function ban(m, mod, ip, key, type) {
if (type == 'unban') {
// unban from every type of suspension
authcommon.suspensionKeys.forEach(function (suffix) {
m.srem('hot:' + suffix, key);
});
m.hdel('ip:' + key, 'ban');
}
else {
// need to validate that this is a valid ban type
// TODO: elaborate
if (type != 'timeout')
return false;
m.sadd('hot:' + type + 's', key);
m.hset('ip:' + key, 'ban', type);
}
var now = Date.now();
var info = {ip: key, type: type, time: now};
if (key !== ip)
info.realip = ip;
if (mod.ident.email)
info.email = mod.ident.email;
m.rpush('auditLog', JSON.stringify(info));
// trigger reload
m.publish('reloadHot', 'caps');
return true;
}
okyaku.dispatcher[authcommon.BAN] = function (msg, client) {
if (!caps.can_administrate(client.ident))
return false;
var ip = msg[0];
var type = msg[1];
if (!authcommon.is_valid_ip(ip))
return false;
var key = authcommon.ip_key(ip);
var m = connect().multi();
if (!ban(m, client, ip, key, type))
return false;
m.exec(function (err) {
if (err)
return client.kotowaru(err);
var wasBanned = type != 'unban';
/* XXX not DRY */
var ADDRS = authcommon.modCache.addresses;
if (ADDRS[key])
ADDRS[key].ban = wasBanned;
var a = {ban: wasBanned};
client.send([0, common.MODEL_SET, ['addrs', key], a]);
});
return true;
};

@ -0,0 +1,203 @@
var _ = require('../lib/underscore'),
authcommon = require('./common'),
caps = require('../server/caps'),
common = require('../common'),
okyaku = require('../server/okyaku'),
STATE = require('../server/state');
var ADDRS = STATE.dbCache.addresses;
authcommon.modCache.addresses = ADDRS;
var ip_key = authcommon.ip_key;
function on_client_ip(ip, clients) {
var addr = {key: ip_key(ip), ip: ip, count: clients.length};
// This will leak 0-count clients.
// I want them to expire after a delay, really. Should reduce churn.
this.send([0, common.COLLECTION_ADD, 'addrs', addr]);
}
function on_refresh(info) {
this.send([0, common.MODEL_SET, 'adminState', info]);
}
function connect() {
return global.redis;
}
function address_view(addr) {
addr = _.extend({}, addr);
addr.shallow = false;
var clients = STATE.clientsByIP[addr.ip];
if (clients) {
addr.count = clients.length;
addr.country = clients[0] && clients[0].country || '';
}
return addr;
}
okyaku.dispatcher[authcommon.FETCH_ADDRESS] = function (msg, client) {
if (!caps.can_moderate(client.ident))
return false;
var ip = msg[0];
if (!authcommon.is_valid_ip(ip))
return false;
var key = ip_key(ip);
var addr = ADDRS[key];
if (addr) {
client.send([0, common.COLLECTION_ADD, 'addrs', address_view(addr)]);
return true;
}
// Cache miss
ADDRS[key] = addr = {ip, key, shallow: true};
var r = connect();
r.hgetall('ip:'+key, function (err, info) {
if (err) {
if (ADDRS[key] === addr)
delete ADDRS[key];
return client.kotowaru(err);
}
if (ADDRS[key] !== addr)
return;
_.extend(addr, info);
client.send([0, common.COLLECTION_ADD, 'addrs', address_view(addr)]);
});
return true;
}
okyaku.dispatcher[authcommon.SET_ADDRESS_NAME] = function (msg, client) {
if (!caps.can_moderate(client.ident))
return false;
var ip = msg[0], name = msg[1];
if (!authcommon.is_valid_ip(ip) || typeof name != 'string')
return false;
var key = ip_key(ip);
name = name.trim().slice(0, 30);
var m = connect().multi();
if (!name) {
m.hdel('ip:' + key, 'name');
m.srem('namedIPs', key);
}
else {
m.hset('ip:' + key, 'name', name);
m.sadd('namedIPs', key);
}
m.exec(function (err) {
if (err)
return client.kotowaru(err);
// should observe a publication for this cache update
var addr = ADDRS[key];
if (!addr)
addr = ADDRS[key] = {key: key, ip: ip};
addr.name = name;
var amend = {name: name};
client.send([0, common.MODEL_SET, ['addrs', key], amend]);
});
return true;
};
var panelListeners = 0, panelInterval = 0;
function listen_panel(client) {
STATE.emitter.on('change:clientsByIP', client.on_client_ip);
STATE.emitter.on('refresh', client.on_refresh);
panelListeners++;
if (panelListeners == 1) {
panelInterval = setInterval(refresh_panel_state, 10*1000);
}
}
function unlisten_panel(client) {
STATE.emitter.removeListener('change:clientsByIP',client.on_client_ip);
STATE.emitter.removeListener('refresh', client.on_refresh);
panelListeners--;
if (panelListeners == 0) {
clearInterval(panelInterval);
panelInterval = 0;
}
}
function snapshot_panel() {
var addrCount = 0;
for (var key in ADDRS)
addrCount++;
var ranges = STATE.dbCache.ranges;
var banCount = ranges.bans ? ranges.bans.length : 0;
return {
memoryUsage: process.memoryUsage(),
uptime: process.uptime(),
addrs: addrCount,
bans: banCount,
};
}
function refresh_panel_state() {
STATE.emitter.emit('refresh', snapshot_panel());
}
function subscribe() {
if (this.on_client_ip)
return false;
this.on_client_ip = on_client_ip.bind(this);
this.on_refresh = on_refresh.bind(this);
this.unsubscribe_admin_state = unsubscribe.bind(this);
this.once('close', this.unsubscribe_admin_state);
listen_panel(this);
var state = snapshot_panel();
state.visible = true;
this.send([0, common.MODEL_SET, 'adminState', state]);
var ips = [];
for (var ip in STATE.clientsByIP) {
var key = ip_key(ip);
var a = ADDRS[key];
ips.push(a ? address_view(a) : {
key: key, ip: ip, shallow: true,
count: STATE.clientsByIP[ip].length
});
}
this.send([0, common.COLLECTION_RESET, 'addrs', ips]);
return true;
}
function unsubscribe() {
if (!this.on_client_ip)
return false;
unlisten_panel(this);
this.removeListener('close', this.unsubscribe_admin_state);
this.on_client_ip = null;
this.on_refresh = null;
this.unsubscribe_admin_state = null;
this.send([0, common.MODEL_SET, 'adminState', {visible: false}]);
return true;
}
okyaku.dispatcher[common.SUBSCRIBE] = function (msg, client) {
if (!caps.can_administrate(client.ident))
return false;
if (msg[0] != 'adminState')
return false;
return subscribe.call(client);
};
okyaku.dispatcher[common.UNSUBSCRIBE] = function (msg, client) {
if (!caps.can_administrate(client.ident))
return false;
if (msg[0] != 'adminState')
return false;
return unsubscribe.call(client);
};

@ -0,0 +1,92 @@
/* Copies old threads to the archive board.
* Run this in parallel with the main server.
*/
var config = require('../config'),
db = require('../db'),
winston = require('winston');
// Load hooks
require('../imager');
require('../server/amusement');
var yaku;
function connect() {
var r;
if (!yaku) {
yaku = new db.Yakusoku('archive', db.UPKEEP_IDENT);
r = yaku.connect();
r.on('error', function (err) {
winston.error(err);
process.exit(1);
});
}
else
r = yaku.connect();
return r;
}
function at_next_minute(func) {
var now = Date.now();
var inFive = new Date(now + 5000);
var nextMinute = inFive.getTime();
var ms = inFive.getMilliseconds(), s = inFive.getSeconds();
if (ms > 0) {
nextMinute += 1000 - ms;
s++;
}
if (s > 0 && s < 60)
nextMinute += (60 - s) * 1000;
var delay = nextMinute - now;
return setTimeout(func, delay);
}
var CLEANING_LIMIT = 10; // per minute
function clean_up() {
var r = connect();
var expiryKey = db.expiry_queue_key();
var now = Math.floor(Date.now() / 1000);
r.zrangebyscore(expiryKey, 1, now, 'limit', 0, CLEANING_LIMIT,
function (err, expired) {
if (err) {
winston.error(err);
return;
}
expired.forEach(function (entry) {
var m = entry.match(/^(\d+):/);
if (!m)
return;
var op = parseInt(m[1], 10);
if (!op)
return;
yaku.archive_thread(op, function (err) {
if (err)
return winston.error(err);
r.zrem(expiryKey, entry, function (err, n) {
if (err)
return winston.error(err)
winston.info("Archived thread #" + op);
if (n != 1)
winston.warn("Not archived?");
});
});
});
});
at_next_minute(clean_up);
}
if (require.main === module) process.nextTick(function () {
connect();
var args = process.argv;
if (args.length == 3) {
yaku.archive_thread(parseInt(args[2], 10), function (err) {
if (err)
throw err;
process.exit(0);
});
}
at_next_minute(clean_up);
});

@ -0,0 +1,250 @@
/* Dumps the given thread to JSON.
* Not production-ready.
*/
var _ = require('../lib/underscore'),
async = require('async'),
caps = require('../server/caps'),
db = require('../db'),
etc = require('../etc'),
fs = require('fs'),
joinPath = require('path').join,
render = require('../server/render'),
util = require('util');
var DUMP_DIR = 'www/archive';
var AUTH_DUMP_DIR = 'authdump';
var DUMP_IDENT = {ip: '127.0.0.1', auth: 'dump'};
require('../server/amusement'); // load dice hook
function Dumper(reader, out) {
this.out = out;
this.reader = reader;
_.bindAll(this);
reader.on('thread', this.on_thread);
reader.on('post', this.on_post);
reader.on('endthread', this.on_endthread);
}
var D = Dumper.prototype;
D.on_thread = function (op_post) {
if (this.needComma) {
this.out.write(',\n');
this.needComma = false;
}
this.op = op_post.num;
this.out.write('[\n' + JSON.stringify(tweak_post(op_post)));
};
D.on_post = function (post) {
this.out.write(',\n' + JSON.stringify(tweak_post(post, this.op)));
};
D.on_endthread = function () {
this.out.write('\n]');
this.needComma = true;
this.op = null;
};
D.destroy = function () {
this.reader.removeListener('thread', this.on_thread);
this.reader.removeListener('post', this.on_post);
this.reader.removeListener('endthread', this.on_endthread);
this.reader = null;
this.out = null;
};
function AuthDumper(reader, out) {
Dumper.call(this, reader, out);
}
util.inherits(AuthDumper, Dumper);
var AD = AuthDumper.prototype;
AD.on_thread = function (post) {
this.out.write('{"ips":{');
if (post.num && post.ip) {
this.out.write('"'+post.num+'":'+JSON.stringify(post.ip));
this.needComma = true;
}
};
AD.on_post = function (post) {
if (post.num && post.ip) {
if (this.needComma)
this.out.write(',');
else
this.needComma = true;
this.out.write('"'+post.num+'":'+JSON.stringify(post.ip));
}
};
AD.on_endthread = function () {
this.out.write('}}');
this.needComma = false;
};
function tweak_post(post, known_op) {
post = _.clone(post);
/* thread-only */
if (typeof post.tags == 'string')
post.tags = db.parse_tags(post.tags);
if (typeof post.origTags == 'string')
post.origTags = db.parse_tags(post.origTags);
if (typeof post.hctr == 'string')
post.hctr = parseInt(post.hctr, 10);
if (typeof post.imgctr == 'string')
post.imgctr = parseInt(post.imgctr, 10);
/* post-only */
if (known_op == post.op)
delete post.op;
if (post.hideimg) {
delete post.image;
delete post.hideimg;
}
if (post.body == '')
delete post.body;
/* blacklisting is bad... */
delete post.ip;
return post;
}
function dump_thread(op, board, ident, outputs, cb) {
if (!caps.can_access_board(ident, board))
return cb(404);
if (!caps.can_access_thread(ident, op))
return cb(404);
var yaku = new db.Yakusoku(board, ident);
var reader = new db.Reader(yaku);
reader.get_thread(board, op, {});
reader.once('nomatch', function () {
cb(404);
yaku.disconnect();
});
reader.once('redirect', function (op) {
cb('redirect', op);
yaku.disconnect();
});
reader.once('begin', function (preThread) {
var dumper = new Dumper(reader, outputs.json);
var authDumper = new AuthDumper(reader, outputs.auth);
var out = outputs.html;
render.write_thread_head(out, '', board, op, {
subject: preThread.subject,
});
var fakeReq = {ident: ident, headers: {}};
var opts = {fullPosts: true, board: board};
render.write_thread_html(reader, fakeReq, out, opts);
reader.once('end', function () {
outputs.json.write('\n');
outputs.auth.write('\n');
render.write_page_end(out, ident, true);
yaku.disconnect();
cb(null);
});
});
function on_err(err) {
yaku.disconnect();
cb(err);
}
reader.once('error', on_err);
yaku.once('error', on_err);
}
function close_stream(stream, cb) {
if (!stream.writable)
return cb(null);
if (stream.write(''))
close();
else
stream.once('drain', close);
function close() {
// deal with process.stdout not being closable
try {
stream.destroySoon(function (err) {
if (cb)
cb(err);
cb = null;
});
}
catch (e) {
if (cb)
cb(null);
cb = null;
}
}
}
function load_state(cb) {
async.series([
etc.checked_mkdir.bind(null, DUMP_DIR),
etc.checked_mkdir.bind(null, AUTH_DUMP_DIR),
require('../server/state').reload_hot_resources,
db.track_OPs,
], cb);
}
if (require.main === module) (function () {
var op = parseInt(process.argv[2], 10), board = process.argv[3];
if (!op) {
console.error('Usage: node upkeep/dump.js <thread>');
process.exit(-1);
}
console.log('Loading state...');
load_state(function (err) {
if (err)
throw err;
if (!board)
board = db.first_tag_of(op);
if (!board) {
console.error(op + ' has no tags.');
process.exit(-1);
}
console.log('Dumping thread...');
var base = joinPath(DUMP_DIR, op.toString());
var authBase = joinPath(AUTH_DUMP_DIR, op.toString());
var outputs = {
auth: fs.createWriteStream(authBase + '.json'),
json: fs.createWriteStream(base + '.json'),
html: fs.createWriteStream(base + '.html'),
};
dump_thread(op, board, DUMP_IDENT, outputs, function (err) {
if (err)
throw err;
var streams = [];
for (var k in outputs)
streams.push(outputs[k]);
async.each(streams, close_stream, quit);
});
});
function quit() {
// crappy flush for stdout (can't close it)
if (process.stdout.write(''))
process.exit(0);
else
process.stdout.on('drain', function () {
process.exit(0);
});
}
})();

@ -0,0 +1,38 @@
var _ = require('./lib/underscore'),
config = require('./config'),
deps = require('./deps'),
fs = require('fs'),
child_process = require('child_process');
if (config.DAEMON)
throw "Can't run dev server in daemon mode.";
var server;
var start_server = _.debounce(function () {
if (server)
server.kill('SIGTERM');
server = child_process.spawn('node', ['server/server.js']);
server.stdout.pipe(process.stdout);
server.stderr.pipe(process.stderr);
}, 500);
var reload_state = _.debounce(function () {
if (server)
server.kill('SIGHUP');
}, 500);
deps.SERVER_DEPS.forEach(monitor.bind(null, start_server));
deps.SERVER_STATE.forEach(monitor.bind(null, reload_state));
deps.CLIENT_DEPS.forEach(monitor.bind(null, reload_state));
function monitor(func, dep) {
var mtime = new Date;
fs.watchFile(dep, {interval: 500, persistent: true}, function (event) {
if (event.mtime > mtime) {
func();
mtime = event.mtime;
}
});
}
start_server();

@ -0,0 +1,111 @@
(function () {
var $banner;
function queue_roll(bit) {
var n = this.allRolls.sent++;
var info = this.allRolls[n];
if (!info)
info = this.allRolls[n] = {};
info.bit = bit;
info.$tag = $(this.callback(safe('<strong>')));
this.strong = true;
if(bit =='#loli' && info.dice) {
this.callback(safe("<a href='"+info.dice[0]+"' target='_blank' rel='noopener noreferrer'><img src='"+info.dice[1]+"'></img></a>"));
}
else this.callback(info.dice ? readable_dice(bit, info.dice) : bit);
this.strong = false;
this.callback(safe('</strong>'));
}
oneeSama.hook('imouto', function (imouto) {
imouto.dice = GAME_BOARDS.indexOf(BOARD) >= 0;
imouto.queueRoll = queue_roll;
imouto.allRolls = {sent: 0, seen: 0};
});
oneeSama.hook('insertOwnPost', function (extra) {
if (!postForm || !postForm.imouto || !extra || !extra.dice)
return;
var rolls = postForm.imouto.allRolls;
for (var i = 0; i < extra.dice.length; i++) {
var n = rolls.seen++;
var info = rolls[n];
if (!info)
info = rolls[n] = {};
info.dice = extra.dice[i];
if (info.$tag) {
if(info.bit == "#loli")
info.$tag.html("<a href='"+info.dice[0]+"' target='_blank' ref='noopener noreferrer'><img src='"+info.dice[1]+"'></img></a>");
else info.$tag.text(readable_dice(info.bit, info.dice));
}
}
});
var bannerExtra = null; //$.parseHTML('<b>Other stream info</b>');
dispatcher[UPDATE_BANNER] = function (msg, op) {
msg = msg[0];
if (!$banner) {
var dest;
if (THREAD == op)
dest = '#lock';
else {
var $s = $('#' + op);
if ($s.is('section'))
dest = $s.children('header');
}
if (dest)
$banner = $('<span id="banner"/>').insertAfter(dest);
}
if ($banner) {
if (Array.isArray(msg)) {
construct_banner(msg);
if (bannerExtra)
$banner.append(' / ', bannerExtra);
}
else if (msg) {
$banner.text(msg);
if (bannerExtra)
$banner.append(' / ', bannerExtra);
}
else if (bannerExtra) {
$banner.empty().append(bannerExtra);
}
else {
$banner.remove();
$banner = null;
}
}
};
function construct_banner(parts) {
$banner.empty();
parts.forEach(function (part) {
if (part.href)
$('<a></a>', _.extend({target: '_blank', rel: 'noopener noreferrer'}, part)
).appendTo($banner);
else
$banner.append(document.createTextNode(part));
});
}
dispatcher[EXECUTE_JS] = function (msg, op) {
if (THREAD != op)
return;
try {
eval(msg[0]);
}
catch (e) {
/* fgsfds */
}
};
window.flash_bg = function (color) {
var $b = $('body');
var back = $b.css('background');
$b.css('background', color);
setTimeout(function () { $b.css('background', back); }, 200);
};
})();

@ -0,0 +1,553 @@
function inject(frag) {
var dest = this.buffer;
for (var i = 0; i < this.state[1]; i++)
dest = dest.children('del:last');
if (this.state[0] & S_BIG)
dest = dest.children('h4:last');
if (this.state[0] & S_QUOTE)
dest = dest.children('em:last');
if (this.strong)
dest = dest.children('strong:last');
if (this.sup_level) {
for (var i = 0; i < this.sup_level; i++)
dest = dest.children('sup:last');
}
var out = null;
if (frag.safe) {
var m = frag.safe.match(/^<(\w+)>$/);
if (m)
out = document.createElement(m[1]);
else if (/^<\/\w+>$/.test(frag.safe))
out = '';
}
if (out === null) {
if (Array.isArray(frag))
out = $(flatten(frag).join(''));
else
out = escape_fragment(frag);
}
if (out)
dest.append(out);
return out;
}
// TODO: Unify self-updates with OneeSama; this is redundant
oneeSama.hook('insertOwnPost', function (info) {
if (!postForm || !info.links)
return;
postForm.buffer.find('.nope').each(function () {
var $a = $(this);
var text = $a.text();
var m = text.match(/^>>(\d+)/);
if (!m)
return;
var num = m[1], op = info.links[num];
if (!op)
return;
var realRef = postForm.imouto.post_ref(num, op, false);
var $ref = $(flatten([realRef]).join(''));
$a.attr('href', $ref.attr('href')).removeAttr('class');
var refText = $ref.text();
if (refText != text)
$a.text(refText);
});
});
/* Mobile */
function touchable_spoiler_tag(del) {
del.html = '<del onclick="void(0)">';
}
oneeSama.hook('spoilerTag', touchable_spoiler_tag);
function get_focus() {
var $focus = $(window.getSelection().focusNode);
if ($focus.is('blockquote'))
return $focus.find('textarea');
}
function section_abbrev(section) {
var stat = section.find('.omit');
var m = stat.text().match(/(\d+)\D+(\d+)?/);
if (!m)
return false;
return {stat: stat, omit: parseInt(m[1], 10),
img: parseInt(m[2] || 0, 10)};
}
function shift_replies(section) {
if (THREAD)
return;
var shown = section.children('article[id]:not(:has(form))');
var rem = shown.length;
if (rem < ABBREVIATED_REPLIES)
return;
var $stat, omit = 0, img = 0;
var info = section_abbrev(section);
if (info) {
$stat = info.stat;
omit = info.omit;
img = info.img;
}
else {
$stat = $('<span class="omit"></span>');
section.children('blockquote,form').last().after($stat);
}
var omitsBefore = omit;
for (var i = 0; i < shown.length; i++) {
var cull = $(shown[i]);
if (rem-- < ABBREVIATED_REPLIES)
break;
if (cull.has('figure').length)
img++;
omit++;
cull.remove();
}
$stat.text(abbrev_msg(omit, img));
if (omitsBefore <= THREAD_LAST_N && omit > THREAD_LAST_N) {
var $expand = section.find('header .act');
if ($expand.length == 1) {
var num = extract_num(section);
var $lastN = $(oneeSama.last_n_html(num));
$expand.after(' ', $lastN);
}
}
}
function spill_page() {
if (THREAD)
return;
/* Ugh, this could be smarter. */
var ss = $('body > section[id]:visible');
for (var i = THREADS_PER_PAGE; i < ss.length; i++)
$(ss[i]).prev('hr').andSelf().hide();
}
var dispatcher = {};
dispatcher[PING] = function (msg) {};
dispatcher[INSERT_POST] = function (msg) {
var orig_focus = get_focus();
var num = msg[0];
msg = msg[1];
var isThread = !msg.op;
if (isThread)
syncs[num] = 1;
msg.editing = true;
msg.num = num;
// did I create this post?
var el;
var nonce = msg.nonce;
delete msg.nonce;
var myNonce = get_nonces()[nonce];
var bump = BUMP;
var myTab = myNonce && myNonce.tab == TAB_ID;
if (myTab) {
// posted in this tab; transform placeholder
ownPosts[num] = true;
oneeSama.trigger('insertOwnPost', msg);
postSM.feed('alloc', msg);
bump = false;
// delete only after a delay so all tabs notice that it's ours
setTimeout(destroy_nonce.bind(null, nonce), 10*1000);
// if we've already made a placeholder for this post, use it
if (postForm && postForm.el)
el = postForm.el;
}
/* This conflict is really dumb. */
var links = oneeSama.links = msg.links;
delete msg.links;
// create model or fill existing shallow model
var model;
if (!isThread) {
model = UnknownThread.get('replies').get(num);
if (model) {
UnknownThread.get('replies').remove(num);
model.unset('shallow');
model.set(msg);
}
else
model = new Post(msg);
}
else {
model = new Thread(msg);
}
if (myNonce) {
model.set('mine', true);
Mine.write(num, Mine.now());
}
// insert it into the DOM
var $section, $hr;
if (!isThread) {
var article = new Article({model: model, id: num, el: el});
if (!el)
el = article.render().el;
var thread = Threads.lookup(msg.op, msg.op);
thread.get('replies').add(model);
add_post_links(model, links, msg.op);
$section = $('#' + msg.op);
shift_replies($section);
$section.children('blockquote,.omit,form,article[id]:last'
).last().after(el);
if (is_sage(msg.email)) {
bump = false;
}
if (postForm) {
// don't bump due to replies while posting (!)
bump = false;
}
if (bump) {
$hr = $section.next();
$section.detach();
$hr.detach();
}
}
else {
Threads.add(model);
}
// only add new threads on /live
if (isThread && BUMP) {
if (!el) {
$section = $($.parseHTML(oneeSama.monomono(msg
).join('')));
$section = $section.filter('section');
el = $section[0];
}
else {
$section = $(el);
}
var section = new Section({model: model, id: num, el: el});
$hr = $('<hr/>');
if (!postForm)
$section.append(make_reply_box());
}
Backbone.trigger('afterInsert', model, $(el));
if (bump) {
var fencepost = $('body > aside');
$section.insertAfter(fencepost.length ? fencepost : $ceiling);
if ($hr)
$section.after($hr);
spill_page();
}
if (orig_focus)
orig_focus.focus();
};
dispatcher[MOVE_THREAD] = function (msg, op) {
msg = msg[0];
msg.num = op;
var orig_focus = get_focus();
var model = new Thread(msg);
Threads.add(model);
oneeSama.links = msg.links;
var $el = $($.parseHTML(oneeSama.monomono(msg).join('')));
var el = $el.filter('section')[0];
var section = new Section({model: model, id: op, el: el});
var $hr = $('<hr/>');
// No make_reply_box since this is archive-only for now
if (!BUMP) {
$el.hide();
$hr.hide();
}
if (msg.replyctr > 0) {
var omitMsg = abbrev_msg(msg.replyctr, msg.imgctr - 1);
$('<span class="omit"/>').text(omitMsg).appendTo($el);
}
Backbone.trigger('afterInsert', model, $el);
if (BUMP) {
var fencepost = $('body > aside');
$el.insertAfter(fencepost.length ? fencepost : $ceiling
).after($hr);
spill_page();
}
if (orig_focus)
orig_focus.focus();
};
dispatcher[INSERT_IMAGE] = function (msg, op) {
var focus = get_focus();
var num = msg[0];
var post = Threads.lookup(num, op);
if (saku && saku.get('num') == num) {
if (post)
post.set('image', msg[1], {silent: true}); // TEMP
postForm.insert_uploaded(msg[1]);
}
else if (post)
post.set('image', msg[1]);
if (num == MILLION) {
var $el = $('#' + num);
$el.css('background-image', oneeSama.gravitas_style(msg[1]));
var bg = $el.css('background-color');
$el.css('background-color', 'black');
setTimeout(function () { $el.css('background-color', bg); }, 500);
}
if (focus)
focus.focus();
};
dispatcher[UPDATE_POST] = function (msg, op) {
var num = msg[0], links = msg[4], extra = msg[5];
var state = [msg[2] || 0, msg[3] || 0];
var post = Threads.lookup(num, op);
if (post) {
add_post_links(post, links, op);
var body = post.get('body') || '';
post.set({body: body + msg[1], state: state});
}
if (num in ownPosts) {
if (extra)
extra.links = links;
else
extra = {links: links};
oneeSama.trigger('insertOwnPost', extra);
return;
}
var bq = $('#' + num + ' > blockquote');
if (bq.length) {
oneeSama.dice = extra && extra.dice;
oneeSama.links = links || {};
oneeSama.callback = inject;
oneeSama.buffer = bq;
oneeSama.state = state;
oneeSama.fragment(msg[1]);
}
};
dispatcher[FINISH_POST] = function (msg, op) {
var num = msg[0];
delete ownPosts[num];
var thread = Threads.get(op);
var post;
if (op == num) {
if (!thread)
return;
post = thread;
}
else {
if (!thread)
thread = UnknownThread;
post = thread.get('replies').get(num);
}
if (post)
post.set('editing', false);
};
dispatcher[DELETE_POSTS] = function (msg, op) {
var replies = Threads.lookup(op, op).get('replies');
var $section = $('#' + op);
var ownNum = saku && saku.get('num');
msg.forEach(function (num) {
var postVisible = $('#' + num).is('article');
delete ownPosts[num];
var post = replies.get(num);
clear_post_links(post, replies);
if (num === ownNum)
return postSM.feed('done');
if (num == lockTarget)
set_lock_target(null);
if (post)
replies.remove(post);
if (!THREAD && !postVisible) {
/* post not visible; decrease omit count */
var info = section_abbrev($section);
if (info && info.omit > 0) {
/* No way to know if there was an image. Doh */
var omit = info.omit - 1;
if (omit > 0)
info.stat.text(abbrev_msg(omit,
info.img));
else
info.stat.remove();
}
}
});
};
dispatcher[DELETE_THREAD] = function (msg, op) {
delete syncs[op];
delete ownPosts[op];
if (saku) {
var num = saku.get('num');
if ((saku.get('op') || num) == op)
postSM.feed('done');
if (num == op)
return;
}
var thread = Threads.get(op);
if (thread)
thread.trigger('destroy', thread, thread.collection);
};
dispatcher[LOCK_THREAD] = function (msg, op) {
var thread = Threads.get(op);
if (thread)
thread.set('locked', true);
};
dispatcher[UNLOCK_THREAD] = function (msg, op) {
var thread = Threads.get(op);
if (thread)
thread.set('locked', false);
};
dispatcher[DELETE_IMAGES] = function (msg, op) {
var replies = Threads.lookup(op, op).get('replies');
msg.forEach(function (num) {
var post = replies.get(num);
if (post)
post.unset('image');
});
};
dispatcher[SPOILER_IMAGES] = function (msg, op) {
var thread = Threads.get(op);
var replies = thread.get('replies');
msg.forEach(function (info) {
var num = info[0];
var post = (num == op) ? thread : replies.get(num);
if (post)
post.set('spoiler', info[1]);
});
};
function insert_image(info, header, toppu) {
var html = flatten(oneeSama.gazou(info, toppu)).join('');
// HACK ought to add `class="new"` in common?
html = html.replace(/<img src/, '<img class="new" src');
var fig = $(html);
if (toppu)
header.before(fig);
else
header.after(fig);
// fade in
setTimeout(function () { fig.find('img').removeClass('new'); }, 10);
}
function set_highlighted_post(num) {
$('.highlight').removeClass('highlight');
$('article#' + num).addClass('highlight');
}
var samePage = new RegExp('^(?:' + THREAD + ')?#(\\d+)$');
$DOC.on('click', 'a', function (event) {
var target = $(this);
var href = target.attr('href');
if (href && (THREAD || postForm)) {
var q = href.match(/#q(\d+)/);
if (q) {
event.preventDefault();
var id = parseInt(q[1], 10);
set_highlighted_post(id);
with_dom(function () {
open_post_box(id);
postForm.add_ref(id);
});
}
else if (THREAD) {
q = href.match(samePage);
if (q)
set_highlighted_post(q[1]);
}
}
});
$DOC.on('click', 'del', function (event) {
if (!event.spoilt) {
event.spoilt = true;
$(event.target).toggleClass('reveal');
}
});
$DOC.on('click', '.pagination input', function (event) {
location.href = $('link[rel=next]').prop('href');
});
dispatcher[SYNCHRONIZE] = connSM.feeder('sync');
dispatcher[INVALID] = connSM.feeder('invalid');
function lookup_model_path(path) {
var o = window;
if (!Array.isArray(path))
return o[path];
o = o[path[0]];
if (o) {
for (var i = 1; i < path.length; i++) {
o = o.get(path[i]);
if (!o)
break;
}
}
return o;
}
dispatcher[MODEL_SET] = function (msg, op) {
var target = lookup_model_path(msg[0]);
if (target && target.set)
target.set(msg[1]);
};
dispatcher[COLLECTION_RESET] = function (msg, op) {
var target = lookup_model_path(msg[0]);
if (target && target.reset)
target.reset(msg[1]);
};
dispatcher[COLLECTION_ADD] = function (msg, op) {
var target = lookup_model_path(msg[0]);
if (target && target.add)
target.add(msg[1], {merge: true});
};
(function () {
var m = window.location.hash.match(/^#q?(\d+)$/);
if (m)
set_highlighted_post(m[1]);
$('section').each(function () {
var s = $(this);
syncs[s.attr('id')] = parseInt(s.attr('data-sync'));
/* Insert image omission count (kinda dumb) */
if (!THREAD) {
var img = parseInt(s.attr('data-imgs')) -
s.find('img').length;
if (img > 0) {
var stat = s.find('.omit');
var o = stat.text().match(/(\d*)/)[0];
stat.text(abbrev_msg(parseInt(o), img));
}
}
});
$('del').attr('onclick', 'void(0)');
// Android browsers have no easy way to return to the top, so link it
var android = /Android/.test(navigator.userAgent);
if (android) {
var t = $.parseHTML(action_link_html('#', 'Top'))[0];
$('#bottom').css('min-width', 'inherit').after('&nbsp;', t);
}
})();

@ -0,0 +1,199 @@
(function () {
var socket, attempts, attemptTimer, pingTimer;
window.send = function (msg) {
// need deferral or reporting on these lost messages...
if (connSM.state != 'synced' && connSM.state != 'syncing')
return;
if (socket.readyState != SockJS.OPEN) {
if (console)
console.warn("Attempting to send while socket closed");
return;
}
msg = JSON.stringify(msg);
if (DEBUG)
console.log('<', msg);
socket.send(msg);
};
var pong = '[[0,' + PING + ']]';
function on_message(e) {
if (e == pong)
return;
if (DEBUG)
console.log('>', e.data);
var msgs = JSON.parse(e.data);
with_dom(function () {
for (var i = 0; i < msgs.length; i++) {
var msg = msgs[i];
var op = msg.shift();
var type = msg.shift();
if (is_pubsub(type) && op in syncs)
syncs[op]++;
dispatcher[type](msg, op);
}
});
}
function sync_status(msg, hover) {
$('#sync').text(msg).attr('class', hover ? 'error' : '');
}
connSM.act('load + start -> conn', function () {
sync_status('Connecting...', false);
attempts = 0;
connect();
});
function connect() {
if (socket) {
socket.onclose = null;
socket.onmessage = null;
}
if (window.location.protocol == 'file:') {
console.log("Page downloaded locally; refusing to sync.");
return;
}
socket = window.new_socket(attempts);
socket.onopen = connSM.feeder('open');
socket.onclose = connSM.feeder('close');
socket.onmessage = on_message;
if (DEBUG)
window.socket = socket;
}
window.new_socket = function (attempt) {
var protocols = ['xdr-streaming', 'xhr-streaming', 'iframe-eventsource', 'iframe-htmlfile', 'xdr-polling', 'xhr-polling', 'iframe-xhr-polling', 'jsonp-polling'];
if (config.USE_WEBSOCKETS)
protocols.unshift('websocket');
var url = SOCKET_PATH;
if (typeof ctoken != 'undefined') {
url += '?' + $.param({ctoken: ctoken});
}
return new SockJS(url, null, {
transports: protocols,
});
};
connSM.act('conn, reconn + open -> syncing', function () {
sync_status('Syncing...', false);
CONN_ID = random_id();
send([SYNCHRONIZE, CONN_ID, BOARD, syncs, BUMP, document.cookie]);
if (pingTimer)
clearInterval(pingTimer);
pingTimer = setInterval(ping, 25000);
});
connSM.act('syncing + sync -> synced', function () {
sync_status('Synced.', false);
attemptTimer = setTimeout(function () {
attemptTimer = 0;
reset_attempts();
}, 10000);
});
function reset_attempts() {
if (attemptTimer) {
clearTimeout(attemptTimer);
attemptTimer = 0;
}
attempts = 0;
}
connSM.act('* + close -> dropped', function (e) {
if (socket) {
socket.onclose = null;
socket.onmessage = null;
}
if (DEBUG)
console.error('E:', e);
if (attemptTimer) {
clearTimeout(attemptTimer);
attemptTimer = 0;
}
if (pingTimer) {
clearInterval(pingTimer);
pingTimer = 0;
}
sync_status('Dropped.', true);
attempts++;
var n = Math.min(Math.floor(attempts/2), 12);
var wait = 500 * Math.pow(1.5, n);
// wait maxes out at ~1min
setTimeout(connSM.feeder('retry'), wait);
});
connSM.act('dropped + retry -> reconn', function () {
connect();
/* Don't show this immediately so we don't thrash on network loss */
setTimeout(function () {
if (connSM.state == 'reconn')
sync_status('Reconnecting...', true);
}, 100);
});
connSM.act('* + invalid, desynced + close -> desynced', function (msg) {
msg = (msg && msg[0]) ? 'Out of sync: ' + msg[0] : 'Out of sync.';
sync_status(msg, true);
if (attemptTimer) {
clearTimeout(attemptTimer);
attemptTimer = 0;
}
socket.onclose = null;
socket.onmessage = null;
socket.close();
socket = null;
if (DEBUG)
window.socket = null;
});
function window_focused() {
var s = connSM.state;
if (s == 'desynced')
return;
// might have just been suspended;
// try to get our SM up to date if possible
if (s == 'synced' || s == 'syncing' || s == 'conn') {
var rs = socket.readyState;
if (rs != SockJS.OPEN && rs != SockJS.CONNECTING) {
connSM.feed('close');
return;
}
else if (navigator.onLine === false) {
connSM.feed('close');
return;
}
ping();
}
connSM.feed('retry');
}
function ping() {
if (socket.readyState == SockJS.OPEN)
socket.send('['+PING+']');
else if (pingTimer) {
clearInterval(pingTimer);
pingTimer = 0;
}
}
(function () {
_.defer(connSM.feeder('start'));
$(window).focus(function () {
setTimeout(window_focused, 20);
});
window.addEventListener('online', function () {
reset_attempts();
connSM.feed('retry');
});
window.addEventListener('offline', connSM.feeder('close'));
})();
})();

@ -0,0 +1,71 @@
(function () {
function drop_shita(e) {
e.stopPropagation();
e.preventDefault();
var files = e.dataTransfer.files;
if (!files.length)
return;
if (!postForm) {
with_dom(function () {
if (THREAD)
open_post_box(THREAD);
else {
var $s = $(e.target).closest('section');
if (!$s.length)
return;
open_post_box($s.attr('id'));
}
});
}
else {
var attrs = postForm.model.attributes;
if (attrs.uploading || attrs.uploaded)
return;
}
if (files.length > 1) {
postForm.upload_error('Too many files.');
return;
}
var extra = postForm.prep_upload();
var fd = new FormData();
fd.append('image', files[0]);
for (var k in extra)
fd.append(k, extra[k]);
/* Can't seem to jQuery this shit */
var xhr = new XMLHttpRequest();
xhr.open('POST', image_upload_url());
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = upload_shita;
xhr.send(fd);
postForm.notify_uploading();
}
function upload_shita() {
if (this.readyState != 4 || this.status == 202)
return;
var err = this.responseText;
if (this.status != 500 || !err || err.length > 100)
err = "Couldn't get response.";
postForm.upload_error(err)
}
function stop_drag(e) {
e.stopPropagation();
e.preventDefault();
}
function setup_upload_drop(e) {
function go(nm, f) { e.addEventListener(nm, f, false); }
go('dragenter', stop_drag);
go('dragexit', stop_drag);
go('dragover', stop_drag);
go('drop', drop_shita);
}
setup_upload_drop(document.body);
})();

@ -0,0 +1,494 @@
/* YOUTUBE */
// fairly liberal regexp that will accept things like
// https://m.youtube.com/watch/?v=abcdefghijk&t=2m
// youtube.com/watch/?foo=bar&v=abcdefghijk#t=5h2s
// >>>youtu.be/abcdefghijk
var youtube_url_re = /(?:>>>*?)?(?:https?:\/\/)?(?:www\.|m.)?(?:youtu\.be\/|youtube\.com\/watch\/?\?((?:[^\s#&=]+=[^\s#&]*&)*)?v=)([\w-]{11})((?:&[^\s#&=]+=[^\s#&]*)*)&?([#\?]t=[\dhms]{1,9})?/;
var youtube_time_re = /^[#\?]t=(?:(\d\d?)h)?(?:(\d{1,3})m)?(?:(\d{1,3})s)?$/;
(function () {
function make_video(id, params, start) {
if (!params)
params = {allowFullScreen: 'true'};
params.allowScriptAccess = 'always';
var query = {
autohide: 1,
fs: 1,
modestbranding: 1,
origin: document.location.origin,
rel: 0,
showinfo: 0,
};
if (start)
query.start = start;
if (params.autoplay)
query.autoplay = params.autoplay;
if (params.loop) {
query.loop = '1';
query.playlist = id;
}
var uri = encodeURI('https://www.youtube.com/embed/' + id) + '?' +
$.param(query);
return $('<iframe></iframe>', {
type: 'text/html', src: uri,
frameborder: '0',
attr: video_dims(),
"class": 'youtube-player',
});
}
window.make_video = make_video;
function video_dims() {
if (window.screen && screen.width <= 320)
return {width: 250, height: 150};
else
return {width: 560, height: 340};
}
$DOC.on('click', '.watch', function (e) {
if (e.which > 1 || e.metaKey || e.ctrlKey || e.altKey || e.shiftKey)
return;
var $target = $(e.target);
/* maybe squash that double-play bug? ugh, really */
if (!$target.is('a'))
return;
var $video = $target.find('iframe');
if ($video.length) {
$video.siblings('br').andSelf().remove();
$target.css('width', 'auto');
return false;
}
if ($target.data('noembed'))
return;
var m = $target.attr('href').match(youtube_url_re);
if (!m) {
/* Shouldn't happen, but degrade to normal click action */
return;
}
var start = 0;
if (m[4]) {
var t = m[4].match(youtube_time_re);
if (t) {
if (t[1])
start += parseInt(t[1], 10) * 3600;
if (t[2])
start += parseInt(t[2], 10) * 60;
if (t[3])
start += parseInt(t[3], 10);
}
}
var $obj = make_video(m[2], null, start);
with_dom(function () {
$target.css('width', video_dims().width).append('<br>', $obj);
});
return false;
});
$DOC.on('mouseenter', '.watch', function (event) {
var $target = $(event.target);
if ($target.data('requestedTitle'))
return;
$target.data('requestedTitle', true);
/* Edit textNode in place so that we don't mess with the embed */
var node = text_child($target);
if (!node)
return;
var orig = node.textContent;
with_dom(function () {
node.textContent = orig + '...';
});
var m = $target.attr('href').match(youtube_url_re);
if (!m)
return;
$.ajax({
url: 'https://www.googleapis.com/youtube/v3/videos',
data: {id: m[2],
key: config.GOOGLE_API_KEY,
part: 'snippet,status',
fields: 'items(snippet(title),status(embeddable))'},
dataType: 'json',
success: function (data) {
with_dom(gotInfo.bind(null, data));
},
error: function () {
with_dom(function () {
node.textContent = orig + '???';
});
},
});
function gotInfo(data) {
var title = data && data.items && data.items[0].snippet &&
data.items[0].snippet.title;
if (title) {
node.textContent = orig + ': ' + title;
$target.css({color: 'black'});
}
else
node.textContent = orig + ' (gone?)';
if (data && data.items && data.items[0].status &&
data.items[0].status.embeddable == false) {
node.textContent += ' (EMBEDDING DISABLED)';
$target.data('noembed', true);
}
}
});
function text_child($target) {
return $target.contents().filter(function () {
return this.nodeType === 3;
})[0];
}
/* SOUNDCLOUD */
window.soundcloud_url_re = /(?:>>>*?)?(?:https?:\/\/)?(?:www\.)?soundcloud\.com\/([\w-]{1,40}\/[\w-]{1,80})\/?/;
function make_soundcloud(path, dims) {
var query = {
url: 'http://soundcloud.com/' + path,
color: 'ffaa66',
auto_play: false,
show_user: false,
show_comments: false,
};
var uri = 'https://w.soundcloud.com/player/?' + $.param(query);
return $('<iframe></iframe>', {
src: uri, width: dims.width, height: dims.height,
scrolling: 'no', frameborder: 'no',
});
}
$DOC.on('click', '.soundcloud', function (e) {
if (e.which > 1 || e.ctrlKey || e.altKey || e.shiftKey || e.metaKey)
return;
var $target = $(e.target);
var $obj = $target.find('iframe');
if ($obj.length) {
$obj.siblings('br').andSelf().remove();
$target.css('width', 'auto');
return false;
}
var m = $target.attr('href').match(soundcloud_url_re);
if (!m) {
/* Shouldn't happen, but degrade to normal click action */
return;
}
var width = Math.round($(window).innerWidth() * 0.75);
var $obj = make_soundcloud(m[1], {width: width, height: 166});
with_dom(function () {
$target.css('width', width).append('<br>', $obj);
});
return false;
});
/* lol copy pasta */
$DOC.on('mouseenter', '.soundcloud', function (event) {
var $target = $(event.target);
if ($target.data('requestedTitle'))
return;
$target.data('requestedTitle', true);
/* Edit textNode in place so that we don't mess with the embed */
var node = text_child($target);
if (!node)
return;
var orig = node.textContent;
with_dom(function () {
node.textContent = orig + '...';
});
var m = $target.attr('href').match(soundcloud_url_re);
if (!m)
return;
$.ajax({
url: '//soundcloud.com/oembed',
data: {format: 'json', url: 'http://soundcloud.com/' + m[1]},
dataType: 'json',
success: function (data) {
with_dom(gotInfo.bind(null, data));
},
error: function () {
with_dom(function () {
node.textContent = orig + '???';
});
},
});
function gotInfo(data) {
var title = data && data.title;
if (title) {
node.textContent = orig + ': ' + title;
$target.css({color: 'black'});
}
else
node.textContent = orig + ' (gone?)';
}
});
/* TWITTER */
window.twitter_url_re = /(?:>>>*?)?(?:https?:\/\/)?(?:www\.|mobile\.|m\.)?twitter\.com\/(\w{1,15})\/status\/(\d{4,20})\/?/;
$DOC.on('click', '.tweet', function (e) {
if (e.which > 1 || e.metaKey || e.ctrlKey || e.altKey || e.shiftKey)
return;
var $target = $(e.target);
if (!$target.is('a.tweet') || $target.data('tweet') == 'error')
return;
setup_tweet($target);
var $tweet = $target.find('.twitter-tweet');
if ($tweet.length) {
$tweet.siblings('br').andSelf().remove();
$target.css('width', 'auto');
text_child($target).textContent = $target.data('tweet-expanded');
return false;
}
fetch_tweet($target, function (err, info) {
var orig = $target.data('tweet-ref');
if (err) {
$target.data('tweet', 'error');
if (info && info.node) {
with_dom(function () {
info.node.textContent = orig + ' (error: ' + err + ')';
});
}
return;
}
$target.data('tweet', info.tweet);
var w = 500;
if (window.screen && screen.width && screen.width < w)
w = screen.width - 20;
var $tweet = $($.parseHTML(info.tweet.html)[0]);
with_tweet_widget(function () {
$target.append('<br>', $tweet);
$target.css('width', w);
info.node.textContent = orig;
});
});
return false;
});
$DOC.on('mouseenter', '.tweet', function (event) {
var $target = $(event.target);
if (!$target.is('a.tweet') || $target.data('tweet'))
return;
setup_tweet($target);
fetch_tweet($target, function (err, info) {
if (err) {
if (info && info.node) {
$target.data('tweet', 'error');
with_dom(function () {
info.node.textContent += ' (error: ' + err + ')';
});
}
else
console.warn(err);
return;
}
var node = info.node;
var orig = $target.data('tweet-ref') || node.textContent;
var html = info.tweet && info.tweet.html;
if (!html) {
$target.data('tweet', 'error');
node.textContent = orig + ' (broken?)';
return;
}
$target.data('tweet', info.tweet);
// twitter sends us HTML of the tweet; scrape it a little
var $tweet = $($.parseHTML(html)[0]);
var $p = $tweet.find('p');
if ($p.length) {
// chop the long ID number off our ref
var prefix = orig;
var m = /^(.+)\/\d{4,20}(?:s=\d+)?$/.exec(prefix);
if (m)
prefix = m[1];
var text = scrape_tweet_p($p);
with_dom(function () {
var expanded = prefix + ' \u00b7 ' + text;
$target.data('tweet-expanded', expanded);
node.textContent = expanded;
$target.css({color: 'black'});
});
}
else {
with_dom(function () {
node.textContent = orig + ' (could not scrape)';
});
}
});
});
/// call this before fetch_tweet or any DOM modification of the ref
function setup_tweet($target) {
setup_tweet_widgets_script();
if ($target.data('tweet-ref'))
return;
var node = text_child($target);
if (!node)
return;
$target.data('tweet-ref', node.textContent);
}
/// fetch & cache the json about the tweet referred to by >>>/@ref $target
function fetch_tweet($target, cb) {
var node = text_child($target);
if (!node)
return cb("ref's text node not found");
var cached = $target.data('tweet');
if (cached == 'error')
return cb('could not contact twitter', {node: node});
if (cached && cached.inflight) {
var queue = TW_CB[cached.inflight];
if (queue)
queue.callbacks.push(cb);
else
cb('gone', {node: node});
return;
}
if (cached)
return cb(null, {tweet: cached, node: node});
var tweet_url = $target.attr('href');
var m = tweet_url.match(twitter_url_re);
if (!m)
return cb('invalid tweet ref', {node: node});
var handle = m[1];
var id = m[2];
// if this request is already in-flight, just wait on the result
var flight = TW_CB[id];
if (flight) {
flight.node = node;
flight.callbacks.push(cb);
return;
}
// chop the prefix off the url and add our own
var chop = tweet_url.indexOf(handle);
if (chop < 0)
return;
var our_url = '../outbound/tweet/' + tweet_url.substr(chop);
// we're ready, make the call
TW_CB[id] = {node: node, callbacks: [cb]};
$target.data('tweet', {inflight: id});
var theme = 'light'; // TODO tie into current theme
$.ajax({
url: our_url,
data: {theme: theme},
dataType: 'json',
success: function (json) {
got_tweet(json, id);
},
error: function (xhr, stat, error) {
failed_tweet(error, id);
},
});
var orig = $target.data('tweet-ref') || node.textContent;
with_dom(function () {
node.textContent = orig + '...';
});
}
function scrape_tweet_p($p) {
var bits = $p.contents();
var text = "";
var i;
for (i = 0; i < bits.length; i++) {
var node = bits[i];
if (node.nodeType == 3)
text += node.textContent;
else if (node.nodeType == 1) {
if (node.tagName == 'A')
text += node.textContent;
else
break;
}
else
break;
}
if (i < bits.length)
text += ' \u2026';
if (!text)
text = $p.text();
return text;
}
var TW_CB = {};
function failed_tweet(err, id) {
var req = TW_CB[id];
if (!req)
return;
delete TW_CB[id];
var node = req.node;
if (node) {
req.node = null;
var payload = {node: node};
while (req.callbacks.length)
req.callbacks.shift()(err || 'offline?', payload);
}
req.callbacks = [];
}
function got_tweet(tweet, id) {
var saved = TW_CB[id];
if (!saved) {
console.warn('tweet callback for non-pending tweet', tweet);
return;
}
delete TW_CB[id];
var payload = {node: saved.node, tweet: tweet};
saved.node = null;
while (saved.callbacks.length)
saved.callbacks.shift()(null, payload);
}
var TW_WG_SCRIPT;
function setup_tweet_widgets_script() {
TW_WG_SCRIPT = $.getScript('https://platform.twitter.com/widgets.js').done(function () {
TW_WG_SCRIPT = {done: twttr.ready};
twttr.ready(function () {
TW_WG_SCRIPT = true;
});
});
setup_tweet_widgets_script = function () {};
}
/// when creating a tweet widget, wrap the DOM insertion with this
function with_tweet_widget(func) {
function go() {
func();
if (window.twttr)
twttr.widgets.load();
}
if (TW_WG_SCRIPT && TW_WG_SCRIPT.done) {
TW_WG_SCRIPT.done(function () {
with_dom(go);
});
}
else
with_dom(go);
}
})();

@ -0,0 +1,100 @@
// remember which posts are mine for two days
var Mine = new Kioku('mine', 2);
// no cookie though
Mine.bake_cookie = function () { return false; };
$.cookie('mine', null); // TEMP
(function () {
var mine = Mine.read_all();
function extract_post_model(el) {
/* incomplete */
var info = {num: parseInt(el.id, 10)};
var $article = $(el);
/* TODO: do these all in one pass */
var $header = $article.children('header');
var $b = $header.find('b');
if ($b.length)
info.name = $b.text();
var $code = $header.find('code');
if ($code.length)
info.trip = $code.text();
var $time = $header.find('time');
if ($time.length)
info.time = new Date($time.attr('datetime')).getTime();
var $fig = $article.children('figure');
if ($fig.length) {
var $cap = $fig.children('figcaption');
var image = {
MD5: $fig.data('md5'),
size: $fig.data('size'),
src: $cap.children('a').text(),
};
var $i = $cap.children('i');
var t = $i.length && $i[0].childNodes[0];
t = t && t.data;
var m = /(\d+)x(\d+)/.exec(t);
if (m)
image.dims = [parseInt(m[1], 10), parseInt(m[2], 10)];
if (t && t.indexOf(audioIndicator) == 1)
image.audio = true;
var $nm = $i.find('a');
image.imgnm = $nm.attr('title') || $nm.text() || '';
var $img = $fig.find('img');
image.thumb = $img.attr('src');
if (image.dims && $img.length) {
image.dims.push($img.width(), $img.height());
}
info.image = image;
}
info.body = ''; // TODO
if (mine[info.num])
info.mine = true;
return new Post(info);
}
function extract_thread_model(section) {
var replies = [];
for (var i = 0; i < section.childElementCount; i++) {
var el = section.children[i];
if (el.tagName != 'ARTICLE')
continue;
var post = extract_post_model(el);
new Article({model: post, el: el});
replies.push(post);
}
var thread = new Thread({
num: parseInt(section.id, 10),
replies: new Replies(replies),
});
if (mine[thread.num])
thread.set('mine', true);
return thread;
}
function scan_threads_for_extraction() {
var bod = document.body;
var threads = [];
for (var i = 0; i < bod.childElementCount; i++) {
var el = bod.children[i];
if (el.tagName != 'SECTION')
continue;
var thread = extract_thread_model(el);
new Section({model: thread, el: el});
threads.push(thread);
}
Threads.add(threads);
if (THREAD)
CurThread = Threads.get(THREAD);
}
scan_threads_for_extraction();
Mine.purge_expired_soon();
})();

@ -0,0 +1,12 @@
(function () {
function audio(vid, options) {
var $body = $('body');
if ($body.data('vid') == vid)
return;
$body.data({vid: vid}).find('.audio').remove();
if (!options)
options = {autoplay: '1', loop: '1'};
make_video(vid, options).css({'margin-left': '-9001px', 'position': 'absolute'}).addClass('audio').prependTo($body);
}
audio('BnC-cpUCdns');
})();

@ -0,0 +1,27 @@
(function () {
Backbone.on('afterInsert', function (model) {
if (model.id != MILLION)
return;
if (!model.get('op'))
gravitas_body();
});
ComposerView.prototype.add_own_gravitas = function (msg) {
var $el = this.$el.addClass('gravitas');
if (msg.image) {
$el.css('background-image', oneeSama.gravitas_style(msg.image));
var bg = $el.css('background-color');
$el.css('background-color', 'black');
setTimeout(function () { $el.css('background-color', bg); }, 500);
}
if (!this.model.get('op'))
gravitas_body();
this.blockquote.css({'margin-left': '', 'padding-left': ''});
};
if (window.gravitas)
$(gravitas_body);
})();

@ -0,0 +1,45 @@
(function () {
// How many days before forgetting that we hid a given post
// (Otherwise the cookie will balloon in size)
var EXPIRY = 7;
var Hidden = new Kioku('hide', EXPIRY);
oneeSama.hook('menuOptions', function (info) {
if (!info.model)
return; // can't hide drafts
if (postForm && postForm.model.id == info.model.id)
return; // can't hide own post
info.options.push('Hide');
});
menuHandlers.Hide = function (model, $post) {
Hidden.write(model.id, Hidden.now());
model.set('hide', true);
Backbone.trigger('hide', model); // bit of a hack...
};
/* Options menu clear control */
var $clear = $('<input>', {
type: 'button',
val: 'Clear hidden',
css: {display: 'block', 'margin-top': '1em'},
click: function () {
Hidden.purge_all();
$clear.hide();
},
});
oneeSama.hook('initOptions', function ($opts) {
$opts.append($clear);
});
oneeSama.hook('renderOptions', function ($opts) {
$clear.toggle(!_.isEmpty(Hidden.read_all()));
});
Hidden.purge_expired_soon();
})();

@ -0,0 +1,126 @@
(function () {
var preview, previewNum;
$DOC.mousemove(mouse_ugoku);
function mouse_ugoku(event) {
if (!nashi.hover && /^A$/i.test(event.target.tagName)) {
var m = $(event.target).text().match(/^>>(\d+)/);
if (m && preview_miru(event, parseInt(m[1], 10)))
return;
}
if (preview) {
preview.remove();
preview = previewNum = null;
}
}
function preview_miru(event, num) {
/* If there was an old preview of a different thread, remove it */
if (num != previewNum) {
var post = $('#' + num);
if (!post.length)
return false;
if (preview)
preview.remove();
var bits = post.children();
if (bits.find('video').length) {
preview = proper_preview(event, num);
}
else {
/* stupid hack, should be using views */
if (bits[0] && $(bits[0]).is('.select-handle'))
bits = bits.slice(1);
if (post.is('section'))
bits = bits.slice(0, 3);
preview = $('<div class="preview"/>').append(
bits.clone());
}
}
if (!preview)
return false;
var overflow = position_preview(event, preview);
/* Add it to the page if it's new */
if (num != previewNum) {
if (overflow > 0) {
scale_down_to_fit(preview.find('img'), overflow);
position_preview(event, preview);
}
$(document.body).append(preview);
previewNum = num;
}
return true;
}
function proper_preview(event, num) {
var model = Threads.get(num);
if (!model) {
var $s = $(event.target).closest('section');
if ($s.length)
model = Threads.lookup(num, extract_num($s));
}
if (!model)
return false;
// TEMP: OPs not extracted
if (!model.get('image'))
return false;
var article = new Article({model: model});
return $(article.render().el).addClass('preview');
}
function position_preview(event, $el) {
var width = $el.width();
var height = $el.height();
if (height < 5) {
$el.hide();
$(document.body).append($el);
width = $el.width();
height = $el.height();
$el.detach().show();
}
var x = event.pageX + 20;
var y = event.pageY - height - 20;
var $w = $(window);
var overflow = x + width - $w.innerWidth();
if (overflow > 0) {
x = Math.max(0, event.pageX - width - 20);
overflow = x + width - $w.innerWidth();
}
var scrollTop = $w.scrollTop();
if (y < scrollTop) {
var newY = event.pageY + 20;
if (newY + height <= scrollTop + $w.height())
y = newY;
}
$el.css({left: x, top: y});
return overflow;
}
function scale_down_to_fit($img, amount) {
var w = $img.width(), h = $img.height();
if (w - amount > 50) {
var aspect = h / w;
w -= amount;
h = aspect * w;
$img.width(w).height(h);
}
}
/* We'll get annoying preview pop-ups on touch screens, so disable it.
Touch detection is unreliable, so wait for an actual touch event */
document.addEventListener('touchstart', touch_screen_event, false);
function touch_screen_event() {
nashi.hover = true;
nashi.shortcuts = true;
if (preview)
preview.remove();
$DOC.unbind('mousemove', mouse_ugoku);
document.removeEventListener('touchstart', touch_screen_event, false);
}
})();

@ -0,0 +1,31 @@
var CurThread;
var $DOC = $(document);
var $name = $('#xD'), $email = $('#pogchamp');
var $ceiling = $('hr:first');
DEFINES.PAGE_BOTTOM = -1;
var menuOptions = ['Focus'];
var menuHandlers = {};
var syncs = {}, ownPosts = {};
var readOnly = ['archive'];
var connSM = new FSM('load');
var postSM = new FSM('none');
var TAB_ID = random_id();
var CONN_ID;
var oneeSama = new OneeSama(function (num) {
var frag;
if (this.links && num in this.links) {
var op = this.links[num];
var post = Threads.lookup(num, op);
var desc = post && post.get('mine') && '(You)';
frag = this.post_ref(num, op, desc);
}
else
frag = '>>' + num;
this.callback(frag);
});
oneeSama.full = oneeSama.op = THREAD;

@ -0,0 +1,76 @@
function Kioku(key, expiry) {
this.key = key;
this.expiry = expiry;
}
Kioku.prototype.bake_cookie = function (o) {
var nums = Object.keys(o);
nums.sort(function (a, b) {
return parseInt(a, 10) - parseInt(b, 10);
});
return nums.join(',');
};
Kioku.prototype.now = function () {
return Math.floor(new Date().getTime() / 1000);
};
Kioku.prototype.purge_all = function () {
localStorage.removeItem(this.key);
$.cookie(this.key, null);
};
Kioku.prototype.purge_expired = function () {
if (!this.expiry)
return;
var o = this.read_all();
var now = this.now(), expired = [];
for (var k in o) {
var time = o[k];
// TEMP cleanup
if (time === true) {
expired.push(k);
continue;
}
if (time && now > time + 60*60*24*this.expiry)
expired.push(k);
}
if (expired.length) {
expired.forEach(function (k) {
delete o[k];
});
this.write_all(o);
}
};
Kioku.prototype.purge_expired_soon = function () {
var delay = 5000 + Math.floor(Math.random() * 5000);
setTimeout(this.purge_expired.bind(this), delay);
};
Kioku.prototype.read_all = function () {
var o;
try {
o = JSON.parse(localStorage.getItem(this.key));
}
catch (e) {}
return _.isObject(o) ? o : {};
};
Kioku.prototype.write = function (k, v) {
// XXX race, would need lock if highly contended
var o = this.read_all();
o[k] = v;
this.write_all(o);
};
Kioku.prototype.write_all = function (o) {
if (_.isEmpty(o)) {
this.purge_all();
return;
}
localStorage.setItem(this.key, JSON.stringify(o));
var baked = this.bake_cookie(o);
if (baked)
$.cookie(this.key, baked, {expires: this.expiry});
};

@ -0,0 +1,82 @@
(function () {
$DOC.on('click', '.control', function (event) {
var $target = $(event.target);
if ($target.is('li')) {
var handler = menuHandlers[$target.text()];
if (handler) {
var $post = parent_post($target);
var model = parent_model($target);
handler(model, $post);
}
}
var $menu = $(this).find('ul');
if ($menu.length)
$menu.remove();
else {
$menu = $('<ul/>', {"class": 'popup-menu'});
var model = parent_model($target);
var mine, opts;
if (!model) {
mine = !!postForm;
opts = ['Focus'];
}
else {
mine = postForm && postForm.model.id == model.id;
opts = menuOptions.slice();
}
oneeSama.trigger('menuOptions', {
options: opts, // alter this in-place
model: model,
mine: mine,
$button: $target,
});
opts.forEach(function (opt) {
$('<li/>').text(opt).appendTo($menu);
});
$menu.appendTo(this);
}
});
$DOC.on('mouseleave', '.popup-menu', function (event) {
var $ul = $(this);
if (!$ul.is('ul'))
return;
event.stopPropagation();
var timer = setTimeout(function () {
/* Using $.proxy() here breaks FF? */
$ul.remove();
}, 300);
/* TODO: Store in view instead */
$ul.data('closetimer', timer);
});
$DOC.on('mouseenter', '.popup-menu', function (event) {
var $ul = $(this);
var timer = $ul.data('closetimer');
if (timer) {
clearTimeout(timer);
$ul.removeData('closetimer');
}
});
oneeSama.hook('headerFinish', function (info) {
info.header.unshift(safe('<span class="control"/>'));
});
oneeSama.hook('draft', function ($post) {
$post.find('header').prepend('<span class=control/>');
});
$('<span class=control/>').prependTo('header');
$('#persona').click(function (e) {
e.preventDefault();
var $fs = $('fieldset');
$fs.css('visibility', $fs.css('visibility') == 'hidden' ? 'visible' : 'hidden');
});
})();

@ -0,0 +1,265 @@
window.hot = new Backbone.Model({
readOnly: false,
});
var Post = Backbone.Model.extend({
idAttribute: 'num',
});
var Replies = Backbone.Collection.extend({model: Post});
var Thread = Backbone.Model.extend({
idAttribute: 'num',
initialize: function () {
if (!this.get('replies'))
this.set('replies', new Replies([]));
},
});
var ThreadCollection = Backbone.Collection.extend({
model: Thread,
lookup: function (num, op) {
var thread = this.get(op) || UnknownThread;
return (num == op) ? thread : thread.get('replies').get(num);
},
});
var Threads = new ThreadCollection();
var UnknownThread = new Thread();
function model_link(key) {
return function (event) {
this.model.set(key, $(event.target).val());
};
}
var Section = Backbone.View.extend({
tagName: 'section',
initialize: function () {
this.listenTo(this.model, {
'change:hide': this.renderHide,
'change:locked': this.renderLocked,
'change:spoiler': this.renderSpoiler,
destroy: this.remove,
});
this.listenTo(this.model.get('replies'), {
remove: this.removePost,
});
},
renderHide: function (model, hide) {
this.$el.next('hr').andSelf().toggle(!hide);
},
renderLocked: function (model, locked) {
this.$el.toggleClass('locked', !!locked);
},
renderSpoiler: function (model, spoiler) {
var $img = this.$el.children('figure').find('img');
var sp = oneeSama.spoiler_info(spoiler, true);
$img.replaceWith($('<img>', {
src: sp.thumb, width: sp.dims[0], height: sp.dims[1],
}));
},
remove: function () {
var replies = this.model.get('replies');
replies.each(function (post) {
clear_post_links(post, replies);
});
replies.reset();
this.$el.next('hr').andSelf().remove();
this.stopListening();
},
removePost: function (model) {
model.trigger('removeSelf');
},
});
/* XXX: Move into own views module once more substantial */
var Article = Backbone.View.extend({
tagName: 'article',
initialize: function () {
this.listenTo(this.model, {
'change:backlinks': this.renderBacklinks,
'change:editing': this.renderEditing,
'change:hide': this.renderHide,
'change:image': this.renderImage,
'change:spoiler': this.renderSpoiler,
'removeSelf': this.remove,
});
},
render: function () {
var html = oneeSama.mono(this.model.attributes);
this.setElement($($.parseHTML(html)).filter('article')[0]);
return this;
},
renderBacklinks: function () {
if (options.get('nobacklinks'))
return this; /* ought to disconnect handler? */
var backlinks = this.model.get('backlinks');
var $list = this.$el.find('small');
if (!backlinks || !backlinks.length) {
$list.remove();
return this;
}
if (!$list.length)
$list = $('<small/>', {text: 'Replies:'}).appendTo(
this.$el);
// TODO: Sync up DOM gracefully instead of clobbering
$list.find('a').remove();
backlinks.forEach(function (num) {
var $a = $('<a/>', {href: '#'+num, text: '>>'+num});
$list.append(' ', $a);
});
return this;
},
renderEditing: function (model, editing) {
this.$el.toggleClass('editing', !!editing);
if (!editing)
this.$('blockquote')[0].normalize();
},
renderHide: function (model, hide) {
this.$el.toggle(!hide);
},
renderImage: function (model, image) {
var hd = this.$('header'), fig = this.$('figure');
if (!image)
fig.remove();
else if (hd.length && !fig.length) {
/* Is this focus business necessary here? */
var focus = get_focus();
insert_image(image, hd, false);
if (focus)
focus.focus();
}
},
renderSpoiler: function (model, spoiler) {
var $img = this.$('figure').find('img');
var sp = oneeSama.spoiler_info(spoiler, false);
$img.replaceWith($('<img>', {
src: sp.thumb,
width: sp.dims[0], height: sp.dims[1],
}));
},
});
/* BATCH DOM UPDATE DEFER */
var deferredChanges = {links: {}, backlinks: {}};
var haveDeferredChanges = false;
/* this runs just before every _outermost_ wrap_dom completion */
Backbone.on('flushDomUpdates', function () {
if (!haveDeferredChanges)
return;
haveDeferredChanges = false;
for (var attr in deferredChanges) {
var deferred = deferredChanges[attr];
var empty = true;
for (var id in deferred) {
deferred[id].trigger('change:'+attr);
empty = false;
}
if (!empty)
deferredChanges[attr] = {};
}
});
/* LINKS */
function add_post_links(src, links, op) {
if (!src || !links)
return;
var thread = Threads.get(op);
var srcLinks = src.get('links') || [];
var repliedToMe = false;
for (var destId in links) {
var dest = thread && thread.get('replies').get(destId);
if (!dest) {
/* Dest doesn't exist yet; track it anyway */
dest = new Post({id: destId, shallow: true});
UnknownThread.get('replies').add(dest);
}
if (dest.get('mine'))
repliedToMe = true;
var destLinks = dest.get('backlinks') || [];
/* Update links and backlinks arrays in-order */
var i = _.sortedIndex(srcLinks, dest.id);
if (srcLinks[i] == dest.id)
continue;
srcLinks.splice(i, 0, dest.id);
destLinks.splice(_.sortedIndex(destLinks, src.id), 0, src.id);
force_post_change(src, 'links', srcLinks);
force_post_change(dest, 'backlinks', destLinks);
}
if (repliedToMe && !src.get('mine')) {
/* Should really be triggered only on the thread */
Backbone.trigger('repliedToMe');
}
}
function force_post_change(post, attr, val) {
if (val === undefined && post.has(attr))
post.unset(attr);
else if (post.get(attr) !== val)
post.set(attr, val);
else if (!(post.id in deferredChanges[attr])) {
/* We mutated the existing array, so `change` won't fire.
Dumb hack ensues. Should extend Backbone or something. */
/* Also, here we coalesce multiple changes just in case. */
/* XXX: holding a direct reference to post is gross */
deferredChanges[attr][post.id] = post;
haveDeferredChanges = true;
}
}
function clear_post_links(post, replies) {
if (!post)
return;
(post.get('links') || []).forEach(function (destId) {
var dest = replies.get(destId);
if (!dest)
return;
var backlinks = dest.get('backlinks') || [];
var i = backlinks.indexOf(post.id);
if (i < 0)
return;
backlinks.splice(i, 1);
if (!backlinks.length)
backlinks = undefined;
force_post_change(dest, 'backlinks', backlinks);
});
(post.get('backlinks') || []).forEach(function (srcId) {
var src = replies.get(srcId);
if (!src)
return;
var links = src.get('links') || [];
var i = links.indexOf(post.id);
if (i < 0)
return;
links.splice(i, 1);
if (!links.length)
links = undefined;
force_post_change(src, 'links', links);
});
post.unset('links', {silent: true});
post.unset('backlinks');
}

@ -0,0 +1,57 @@
(function () {
// Should be part of a greater thread model
var Unread = new Backbone.Model({unreadCount: 0});
var normalTitle = document.title;
window.addEventListener('focus', function () {
Unread.set({blurred: false, unreadCount: 0, reply: false});
}, false);
window.addEventListener('blur', function () {
Unread.set({blurred: true, unreadCount: 0, reply: false});
}, false);
connSM.on('synced', function () {
Unread.set('alert', false);
});
function dropped() {
Unread.set('alert', true);
}
connSM.on('dropped', dropped);
connSM.on('desynced', dropped);
Backbone.on('repliedToMe', function () {
Unread.set({reply: true});
});
Backbone.on('afterInsert', function (model) {
if (model && model.get('mine'))
return; // It's ours, don't notify unread
if (Unread.get('blurred'))
Unread.set('unreadCount', Unread.get('unreadCount') + 1);
});
Unread.on('change', function (model) {
var attrs = model.attributes;
if (!attrs.blurred) {
document.title = normalTitle;
return;
}
if (attrs.alert) {
document.title = '--- ' + normalTitle;
return;
}
var prefix = '';
if (attrs.reply)
prefix += '>> ';
if (attrs.unreadCount)
prefix += '(' + attrs.unreadCount + ') ';
document.title = prefix + normalTitle;
});
})();

@ -0,0 +1,653 @@
var optSpecs = [];
var nashi = {opts: []}, inputMinSize = 300, fullWidthExpansion = false;
var shortcutKeys = {};
function extract_num(q) {
return parseInt(q.attr('id'), 10);
}
function parent_post($el) {
return $el.closest('article, section');
}
function parent_model($el) {
var $a = parent_post($el);
var op = extract_num($a);
if (!op)
return null;
if ($a.is('section'))
return Threads.get(op);
var $s = $a.parent('section');
if (!$s.length) {
// when we have better hover/inline expansion we will have to
// deal with this, probably by setting data-op on the post
console.warn($a, "'s parent is not thread?!");
return null;
}
var num = op;
op = extract_num($s);
return Threads.lookup(num, op);
}
(function () {
/* OPTIONS LIST */
optSpecs.push(option_inline_expansion);
if (window.devicePixelRatio > 1)
optSpecs.push(option_high_res);
optSpecs.push(option_thumbs);
optSpecs.push(option_autocomplete);
optSpecs.push(option_backlinks);
optSpecs.push(option_reply_at_right);
optSpecs.push(option_theme);
optSpecs.push(option_last_n);
_.defaults(options, {
lastn: config.THREAD_LAST_N,
inlinefit: 'width',
});
options = new Backbone.Model(options);
nashi.upload = !!$('<input type="file"/>').prop('disabled');
if (window.screen && screen.width <= 480) {
inputMinSize = 50;
fullWidthExpansion = true;
}
function load_ident() {
try {
var id = JSON.parse(localStorage.ident);
if (id.name)
$name.val(id.name);
if (id.email)
$email.val(id.email);
}
catch (e) {}
}
function save_ident() {
try {
var name = $name.val(), email = $email.val();
if (email == 'misaki') {
$email.val('');
$.getScript(mediaURL + 'js/login.js');
email = false;
}
else if (is_sage(email) && !is_noko(email))
email = false;
var id = {};
if (name || email) {
if (name)
id.name = name;
if (email)
id.email = email;
localStorage.setItem('ident', JSON.stringify(id));
}
else
localStorage.removeItem('ident');
}
catch (e) {}
}
options.on('change', function () {
try {
localStorage.options = JSON.stringify(options);
}
catch (e) {}
});
/* LAST N CONFIG */
function option_last_n(n) {
if (!reasonable_last_n(n))
return;
$.cookie('lastn', n);
// should really load/hide posts as appropriate
}
option_last_n.id = 'lastn';
option_last_n.label = '[Last #]';
option_last_n.type = 'positive';
oneeSama.lastN = options.get('lastn');
options.on('change:lastn', function (model, lastN) {
oneeSama.lastN = lastN;
});
/* THEMES */
var themes = [
'moe',
'gar',
'mawaru',
'moon',
'ashita',
'console',
'tea',
'higan',
];
function option_theme(theme) {
if (theme) {
var css = theme + '.css?v=' + themeVersion;
$('#theme').attr('href', mediaURL + 'css/' + css);
}
}
option_theme.id = 'board.$BOARD.theme';
option_theme.label = 'Theme';
option_theme.type = themes;
/* THUMBNAIL OPTIONS */
var revealSetup = false;
function option_thumbs(type) {
$.cookie('thumb', type);
// really ought to apply the style immediately
// need pinky/mid distinction in the model to do properly
oneeSama.thumbStyle = type;
var hide = type == 'hide';
if (hide)
$('img').hide();
else
$('img').show();
if (hide && !revealSetup)
$DOC.on('click', 'article', reveal_thumbnail);
else if (!hide && revealSetup)
$DOC.off('click', 'article', reveal_thumbnail);
revealSetup = hide;
}
option_thumbs.id = 'board.$BOARD.thumbs';
option_thumbs.label = 'Thumbnails';
option_thumbs.type = thumbStyles;
/* Alt-click a post to reveal its thumbnail if hidden */
function reveal_thumbnail(event) {
if (!event.altKey)
return;
var $article = $(event.target);
var $img = $article.find('img');
if ($img.length) {
with_dom(function () { $img.show(); });
return false;
}
/* look up the image info and make the thumbnail */
var thread = Threads.get(extract_num($article.closest('section')));
if (!thread)
return;
var post = thread.get('replies').get(extract_num($article));
if (!post)
return;
var info = post.get('image');
if (!info)
return;
with_dom(function () {
var img = oneeSama.gazou_img(info, false);
var $img = $.parseHTML(flatten(img.html).join(''));
$article.find('figcaption').after($img);
});
return false;
}
/* REPLY AT RIGHT */
function option_reply_at_right(r) {
if (r)
$('<style/>', {
id: 'reply-at-right',
text: 'aside { margin: -26px 0 2px auto; }',
}).appendTo('head');
else
$('#reply-at-right').remove();
}
option_reply_at_right.id = 'replyright';
option_reply_at_right.label = '[Reply] at right';
option_reply_at_right.type = 'checkbox';
/* AUTOCOMPLETE */
function option_autocomplete(b) {
if (postForm)
postForm.model.set('autocomplete', b);
}
option_autocomplete.id = 'autocomplete';
option_autocomplete.label = 'Auto-complete';
option_autocomplete.type = 'checkbox';
/* BACKLINKS */
function option_backlinks(b) {
if (b)
$('small').remove();
else
show_backlinks();
}
option_backlinks.id = 'nobacklinks';
option_backlinks.label = 'Backlinks';
option_backlinks.type = 'revcheckbox';
function show_backlinks() {
if (load_thread_backlinks) {
with_dom(function () {
$('section').each(function () {
load_thread_backlinks($(this));
});
});
load_thread_backlinks = null;
return;
}
Threads.each(function (thread) {
thread.get('replies').each(function (reply) {
if (reply.has('backlinks'))
reply.trigger('change:backlinks');
});
});
}
var load_thread_backlinks = function ($section) {
var op = extract_num($section);
var replies = Threads.get(op).get('replies');
$section.find('blockquote a').each(function () {
var $a = $(this);
var m = $a.attr('href').match(/^\d*#(\d+)$/);
if (!m)
return;
var destId = parseInt(m[1], 10);
if (!replies.get(destId)) // local backlinks only for now
return;
var src = replies.get(extract_num(parent_post($a)));
if (!src)
return;
var update = {};
update[destId] = op;
add_post_links(src, update, op);
});
};
/* INLINE EXPANSION */
function option_inline_expansion() {
/* TODO: do it live */
}
option_inline_expansion.id = 'inlinefit';
option_inline_expansion.label = 'Expansion';
option_inline_expansion.type = ['none', 'full', 'width', 'height', 'both'];
option_inline_expansion.labels = ['no', 'full-size', 'fit to width',
'fit to height', 'fit to both'];
function option_high_res() {
}
option_high_res.id = 'nohighres';
option_high_res.label = 'High-res expansions';
option_high_res.type = 'revcheckbox';
$DOC.on('mouseup', 'img, video', function (event) {
/* Bypass expansion for non-left mouse clicks */
if (options.get('inlinefit') != 'none' && event.which > 1) {
var img = $(this);
img.data('skipExpand', true);
setTimeout(function () {
img.removeData('skipExpand');
}, 100);
}
});
$DOC.on('click', 'img, video', function (event) {
if (options.get('inlinefit') != 'none') {
var $target = $(this);
if (!$target.data('skipExpand'))
toggle_expansion($target, event);
}
});
function toggle_expansion(img, event) {
var href = img.parent().attr('href');
if (/^\.\.\/outbound\//.test(href))
return;
if (event.metaKey)
return;
event.preventDefault();
var expand = !img.data('thumbSrc');
img.closest('article').toggleClass('expanded', expand);
var $imgs = img;
if (THREAD && (event.altKey || event.shiftKey)) {
var post = img.closest('article');
if (post.length)
$imgs = post.nextAll(':has(img):lt(4)').andSelf();
else
$imgs = img.closest('section').children(
':has(img):lt(5)');
$imgs = $imgs.find('img');
}
with_dom(function () {
$imgs.each(function () {
var $img = $(this);
if (expand)
expand_image($img);
else {
contract_image($img, event);
event = null; // de-zoom to first image only
}
});
});
}
function contract_image($img, event) {
var thumb = $img.data('thumbSrc');
if (!thumb)
return;
// try to keep the thumbnail in-window for large images
var h = $img.height();
var th = parseInt($img.data('thumbHeight'), 10);
if (event) {
var y = $img.offset().top, t = $(window).scrollTop();
if (y < t && th < h)
window.scrollBy(0, Math.max(th - h,
y - t - event.clientY + th/2));
}
if (fullWidthExpansion)
contract_full_width(parent_post($img));
$img.replaceWith($('<img>')
.width($img.data('thumbWidth')).height(th)
.attr('src', thumb));
}
function expand_image($img) {
if ($img.data('thumbSrc'))
return;
var a = $img.parent();
var href = a.attr('href');
if (!href)
return;
var cap = a.siblings('figcaption').text();
var dims = cap.match(/(\d+)x(\d+)/);
var video = /^Video/.test(cap);
if (!dims)
return;
var tw = $img.width(), th = $img.height();
var w = parseInt(dims[1], 10), h = parseInt(dims[2], 10);
// if this is a high-density screen, reduce image size appropriately
var r = window.devicePixelRatio;
if (!options.get('nohighres') && !video && r && r > 1) {
var min = 1000;
if ((w > min || h > min) && w/r > tw && h/r > th) {
w /= r;
h /= r;
}
}
$img.remove();
$img = $(video ? '<video>' : '<img>', {
src: href,
width: w, height: h,
data: {
thumbWidth: tw, thumbHeight: th,
thumbSrc: $img.attr('src'),
},
prop: video ? {autoplay: true, loop: true} : {},
}).appendTo(a);
var fit = options.get('inlinefit');
if (fit != 'none') {
var both = fit == 'both';
fit_to_window($img, w, h, both || fit == 'width',
both || fit == 'height');
}
}
function fit_to_window($img, w, h, widthFlag, heightFlag) {
var $post = parent_post($img);
var overX = 0, overY = 0;
if (widthFlag) {
var innerWidth = $(window).innerWidth();
var rect = $post.length && $post[0].getBoundingClientRect();
if ($post.is('article')) {
if (fullWidthExpansion && w > innerWidth) {
overX = w - innerWidth;
expand_full_width($img, $post, rect);
heightFlag = false;
}
else {
overX = rect.right - innerWidth;
// right-side posts might fall off the left side of the page
// account for them + left margin
if (rect.left < 0) {
// there has to be a better way...
function m($el) {
var m = parseInt($el.css('marginLeft'), 10);
var p = parseInt($el.css('paddingLeft'), 10);
return (m || 0) + (p || 0);
}
var $sec = $post.closest('section');
var margin = m($('body')) + m($post) + m($sec) + 13;
overX -= rect.left - margin;
}
}
}
else if ($post.is('section'))
overX = w - (innerWidth - rect.left*2);
}
if (heightFlag) {
overY = h - ($(window).innerHeight() - 20);
}
var aspect = h / w;
var newW, newH;
if (overX > 0) {
newW = w - overX;
newH = aspect * newW;
}
if (overY > 0) {
// might have to fit to both width and height
var maybeH = h - overY;
if (!newH || maybeH < newH) {
newH = maybeH;
newW = newH / aspect;
}
}
if (newW > 50 && newH > 50)
$img.width(newW).height(newH);
}
function expand_full_width($img, $post, rect) {
if ($post.hasClass('floop')) {
$post.removeClass('floop');
$post.data('crouching-floop', true);
}
var img = $img[0].getBoundingClientRect();
$img.css('margin-left', -img.left + 'px');
var over = rect.right - img.right;
if (over > 0) {
$post.css({
'margin-right': -over+'px',
'padding-right': 0,
'border-right': 'none',
});
}
}
function contract_full_width($post) {
if ($post.css('margin-right')[0] == '-') {
$post.css({
'margin-right': '',
'padding-right': '',
'border-right': '',
});
}
if ($post.data('crouching-floop')) {
$post.addClass('floop');
$post.removeData('crouching-floop');
}
}
/* SHORTCUT KEYS */
var shortcuts = [
{label: 'New post', name: 'new', which: 78},
{label: 'Image spoiler', name: 'togglespoiler', which: 73},
{label: 'Finish post', name: 'done', which: 83},
{label: 'Flip side', name: 'flip', which: 70},
];
function toggle_shortcuts(event) {
event.preventDefault();
var $shortcuts = $('#shortcuts');
if ($shortcuts.length)
return $shortcuts.remove();
$shortcuts = $('<div/>', {
id: 'shortcuts',
click: select_shortcut,
keyup: change_shortcut,
});
shortcuts.forEach(function (s) {
var value = String.fromCharCode(shortcutKeys[s.name]);
var $label = $('<label>', {text: s.label});
$('<input>', {
id: s.name, maxlength: 1, val: value,
}).prependTo($label);
$label.prepend(document.createTextNode('Alt+'));
$shortcuts.append($label, '<br>');
});
$shortcuts.appendTo('#options-panel');
}
function select_shortcut(event) {
if ($(event.target).is('input'))
$(event.target).val('');
}
function change_shortcut(event) {
if (event.which == 13)
return false;
var $input = $(event.target);
var letter = $input.val();
if (!(/^[a-z]$/i.exec(letter)))
return;
var which = letter.toUpperCase().charCodeAt(0);
var name = $input.attr('id');
if (!(name in shortcutKeys))
return;
shortcutKeys[name] = which;
var shorts = options.get('shortcuts')
if (!_.isObject(shorts)) {
shorts = {};
shorts[name] = which;
options.set('shortcuts', shorts);
}
else {
shorts[name] = which;
options.trigger('change'); // force save
}
$input.blur();
}
_.defer(function () {
load_ident();
var save = _.debounce(save_ident, 1000);
function prop() {
if (postForm)
postForm.propagate_ident();
save();
}
$name.input(prop);
$email.input(prop);
optSpecs.forEach(function (spec) {
spec.id = spec.id.replace(/\$BOARD/g, BOARD);
});
$('<a id="options">Options</a>').click(function () {
var $opts = $('#options-panel');
if (!$opts.length)
$opts = make_options_panel().appendTo('body');
if ($opts.is(':hidden'))
oneeSama.trigger('renderOptions', $opts);
$opts.toggle('fast');
}).insertAfter('#sync');
optSpecs.forEach(function (spec) {
spec(options.get(spec.id));
});
var prefs = options.get('shortcuts') || {};
shortcuts.forEach(function (s) {
shortcutKeys[s.name] = prefs[s.name] || s.which;
});
});
function make_options_panel() {
var $opts = $('<div/>', {"class": 'modal', id: 'options-panel'});
$opts.change(function (event) {
var $o = $(event.target), id = $o.attr('id'), val;
var spec = _.find(optSpecs, function (s) {
return s.id == id;
});
if (!spec)
return;
if (spec.type == 'checkbox')
val = !!$o.prop('checked');
else if (spec.type == 'revcheckbox')
val = !$o.prop('checked');
else if (spec.type == 'positive')
val = Math.max(parseInt($o.val(), 10), 1);
else
val = $o.val();
options.set(id, val);
with_dom(function () {
spec(val);
});
});
optSpecs.forEach(function (spec) {
var id = spec.id;
if (nashi.opts.indexOf(id) >= 0)
return;
var val = options.get(id), $input, type = spec.type;
if (type == 'checkbox' || type == 'revcheckbox') {
var b = (type == 'revcheckbox') ? !val : val;
$input = $('<input type="checkbox" />')
.prop('checked', b ? 'checked' : null);
}
else if (type == 'positive') {
$input = $('<input />', {
width: '4em',
maxlength: 4,
val: val,
});
}
else if (type instanceof Array) {
$input = $('<select/>');
var labels = spec.labels || {};
type.forEach(function (item, i) {
var label = labels[i] || item;
$('<option/>')
.text(label).val(item)
.appendTo($input);
});
if (type.indexOf(val) >= 0)
$input.val(val);
}
var $label = $('<label/>').attr('for', id).text(spec.label);
$opts.append($input.attr('id', id), ' ', $label, '<br>');
});
if (!nashi.shortcuts) {
$opts.append($('<a/>', {
href: '#', text: 'Shortcuts',
click: toggle_shortcuts,
}));
}
oneeSama.trigger('initOptions', $opts);
return $opts.hide();
}
})();

File diff suppressed because it is too large Load Diff

@ -0,0 +1,142 @@
var lockTarget, lockKeyHeight;
var $lockTarget, $lockIndicator;
var lockedManually;
var dropAndLockTimer;
var nestLevel = 0;
function with_dom(func) {
var lockHeight, locked = lockTarget, $post;
if (locked == PAGE_BOTTOM)
lockHeight = $DOC.height();
else if (locked) {
$post = $('#' + locked);
var r = $post.length && $post[0].getBoundingClientRect();
if (r && r.bottom > 0 && r.top < window.innerHeight)
lockHeight = r.top;
else
locked = false;
}
var ret;
try {
nestLevel++;
ret = func.call(this);
}
finally {
if (!--nestLevel)
Backbone.trigger('flushDomUpdates');
}
if (locked == PAGE_BOTTOM) {
var height = $DOC.height();
if (height > lockHeight - 10)
window.scrollBy(0, height - lockHeight + 10);
}
else if (locked && lockTarget == locked) {
var newY = $post[0].getBoundingClientRect().top;
window.scrollBy(0, newY - lockHeight);
}
return ret;
}
function set_lock_target(num, manually) {
lockedManually = manually;
if (!num && at_bottom())
num = PAGE_BOTTOM;
if (num == lockTarget)
return;
lockTarget = num;
var bottom = lockTarget == PAGE_BOTTOM;
if ($lockTarget)
$lockTarget.removeClass('scroll-lock');
if (num && !bottom && manually)
$lockTarget = $('#' + num).addClass('scroll-lock');
else
$lockTarget = null;
var $ind = $lockIndicator;
if ($ind) {
var visible = bottom || manually;
$ind.css({visibility: visible ? 'visible' : 'hidden'});
if (bottom)
$ind.text('Locked to bottom');
else if (num) {
$ind.empty().append($('<a/>', {
text: '>>' + num,
href: '#' + num,
}));
}
}
}
oneeSama.hook('menuOptions', function (info) {
var opts = info.options;
if (lockTarget && info.model && lockTarget == info.model.id)
opts.splice(opts.indexOf('Focus'), 1, 'Unfocus');
});
Backbone.on('hide', function (model) {
if (model && model.id == lockTarget)
set_lock_target(null);
});
connSM.on('dropped', function () {
if (!dropAndLockTimer)
dropAndLockTimer = setTimeout(drop_and_lock, 10 * 1000);
});
function drop_and_lock() {
if (connSM.state == 'synced')
return;
// On connection drop, focus the last post.
// This to prevent jumping to thread bottom on reconnect.
if (CurThread && !lockedManually) {
var last = CurThread.get('replies').last();
if (last)
set_lock_target(last.id, false);
}
}
connSM.on('synced', function () {
// If we dropped earlier, stop focusing now.
if (!lockedManually)
set_lock_target(null);
if (dropAndLockTimer) {
clearTimeout(dropAndLockTimer);
dropAndLockTimer = null;
}
});
var at_bottom = function() {
return window.scrollY + window.innerHeight >= $DOC.height() - 5;
}
if (window.scrollMaxY !== undefined)
at_bottom = function () {
return window.scrollMaxY <= window.scrollY;
};
(function () {
menuHandlers.Focus = function (model) {
var num = model && model.id;
set_lock_target(num, true);
};
menuHandlers.Unfocus = function () {
set_lock_target(null);
};
function scroll_shita() {
if (!lockTarget || (lockTarget == PAGE_BOTTOM))
set_lock_target(null);
}
if (THREAD) {
$lockIndicator = $('<span id=lock>Locked to bottom</span>', {
css: {visibility: 'hidden'},
}).appendTo('body');
$DOC.scroll(scroll_shita);
scroll_shita();
}
})();

@ -0,0 +1,36 @@
(function () {
var readable_time = oneeSama.readable_time;
function adjust_all_times() {
$('time').each(function () {
var date = date_from_time_el(this);
this.innerHTML = readable_time(date.getTime());
});
}
function date_from_time_el(el) {
var d = el.getAttribute('datetime').replace(/-/g, '/'
).replace('T', ' ').replace('Z', ' GMT');
return new Date(d);
}
function is_skewed() {
var el = document.querySelector('time');
if (!el)
return false;
var d = date_from_time_el(el);
return readable_time(d.getTime()) != el.innerHTML;
}
if (is_skewed()) {
adjust_all_times();
setTimeout(function () {
// next request, have the server render the right times
var tz = -new Date().getTimezoneOffset() / 60;
$.cookie('timezone', tz, { expires: 90 });
}, 3000);
}
})();

File diff suppressed because it is too large Load Diff

@ -0,0 +1,53 @@
var config = {
LISTEN_PORT: 8000,
LISTEN_HOST: null,
DEBUG: true,
SECURE_SALT: "LALALALALALALALA", /* [A-Za-z0-9./]{16} */
SOCKET_PATH: '/hana',
SERVE_STATIC_FILES: true,
SERVE_IMAGES: true,
GZIP: false, /* not preferred; use nginx (or other)'s gzipping */
USE_WEBSOCKETS: true,
REDIS_PORT: 6379,
READ_ONLY: false,
TRUST_X_FORWARDED_FOR: false,
CLOUDFLARE: false,
RESTRICTED_COUNTRIES: ['T1'], /* cloudflare only; T1 = Tor */
BOARDS: ['moe', 'gar', 'tea', 'meta', 'archive', 'staff'],
DEFAULT_BOARD: 'moe',
GAME_BOARDS: ['moe', 'archive'],
STAFF_BOARD: 'staff',
THREADS_PER_PAGE: 10,
ABBREVIATED_REPLIES: 5,
THREAD_LAST_N: 100,
CURFEW_BOARDS: ['tea'],
CURFEW_HOURS: [22, 23],
THREAD_THROTTLE: 60,
THREAD_EXPIRY: 3600 * 24 * 7,
SHORT_TERM_LIMIT: 2000,
LONG_TERM_LIMIT: 2000*20*12,
NEW_POST_WORTH: 50,
IMAGE_WORTH: 50,
SUBJECT_MAX_LENGTH: 50,
EXCLUDE_REGEXP: /[\u2000-\u200f\u202a-\u202f\u205f-\u206f]+/g,
SAGE_ENABLED: true,
ADMIN_GITHUBS: ['lalcmellkmal'],
MODERATOR_GITHUBS: [],
GITHUB_CLIENT_ID: '',
GITHUB_CLIENT_SECRET: '',
LOGIN_COOKIE_DOMAIN: 'example.com',
LOGIN_SESSION_TIME: 60*60*24*14,
IP_MNEMONIC: true,
// API key with Youtube Data API v3 (browser) access
// obtain from https://console.developers.google.com
GOOGLE_API_KEY: '',
};
module.exports = config;

@ -0,0 +1,64 @@
(function () {
function game_over() {
setTimeout(function () {
location.reload(true);
}, 2000);
$DOC.children().remove();
}
function shut_down_everything() {
var $threads = $('section');
if (!$threads.length)
return setTimeout(game_over, 1000);
pick_random($threads, 0.2).remove();
pick_random($('hr, aside, h1, fieldset'), 0.2).remove();
setTimeout(shut_down_everything, 500);
}
function shut_down_something() {
var $posts = $('article');
if (!$posts.length)
return setTimeout(shut_down_everything, 500);
var $posts = pick_random($posts, 0.1);
$posts.each(function () {
var $post = $(this);
var $section = $post.closest('section');
try {
var thread = Threads.get(extract_num($section));
var replies = thread.get('replies');
var num = extract_num($post);
clear_post_links(replies.get(num), replies);
}
catch (e) {}
});
$posts.remove();
if (Math.random() < 0.2)
pick_random($('figure, blockquote, b'), 0.002).remove();
setTimeout(shut_down_something, 500);
}
var tearingDown = false;
dispatcher[TEARDOWN] = function () {
if (tearingDown)
return;
tearingDown = true;
window.onbeforeunload = null;
shut_down_something();
};
function pick_random($items, proportion) {
var len = $items.length;
var origLen = len;
var toDelete = Math.max(1, Math.min(len, Math.ceil(len * proportion)));
var $picked = $();
for (; len > 0 && toDelete > 0; toDelete--) {
var i = Math.floor(Math.random() * len);
$picked = $picked.add($items[i]);
$items.splice(i, 1);
len = $items.length;
}
return $picked;
}
})();

@ -0,0 +1,133 @@
var _ = require('../lib/underscore'),
caps = require('../server/caps'),
config = require('../config'),
db = require('../db'),
hooks = require('../hooks'),
web = require('../server/web'),
winston = require('winston');
var RES = require('../server/state').resources;
hooks.hook_sync('temporalAccessCheck', function (info) {
if (under_curfew(info.ident, info.board))
info.access = false;
});
hooks.hook_sync('boardDiversion', function (info) {
if (info.diverted)
return;
if (under_curfew(info.ident, info.board)) {
info.diverted = true;
var resp = info.resp;
resp.writeHead(200, web.noCacheHeaders);
resp.write(RES.curfewTmpl[0]);
resp.write('/' + info.board + '/');
resp.write(RES.curfewTmpl[1]);
var ending = curfew_ending_time(info.board);
resp.write(ending ? ''+ending.getTime() : 'null');
resp.end(RES.curfewTmpl[2]);
}
});
function under_curfew(ident, board) {
if (caps.can_administrate(ident))
return false;
var curfew = config.CURFEW_HOURS;
if (!curfew || (config.CURFEW_BOARDS || []).indexOf(board) < 0)
return false;
var hour = new Date().getUTCHours();
return curfew.indexOf(hour) < 0;
}
function curfew_ending_time(board) {
var curfew = config.CURFEW_HOURS;
if (!curfew || (config.CURFEW_BOARDS || []).indexOf(board) < 0)
return null;
var now = new Date();
var tomorrow = day_after(now);
var makeToday = hour_date_maker(now);
var makeTomorrow = hour_date_maker(tomorrow);
/* Dumb brute-force algorithm */
var candidates = [];
config.CURFEW_HOURS.forEach(function (hour) {
candidates.push(makeToday(hour), makeTomorrow(hour));
});
candidates.sort(compare_dates);
for (var i = 0; i < candidates.length; i++)
if (candidates[i] > now)
return candidates[i];
return null;
}
function curfew_starting_time(board) {
var curfew = config.CURFEW_HOURS;
if (!curfew || (config.CURFEW_BOARDS || []).indexOf(board) < 0)
return null;
var now = new Date();
var tomorrow = day_after(now);
var makeToday = hour_date_maker(now);
var makeTomorrow = hour_date_maker(tomorrow);
/* Even dumber brute-force algorithm */
var candidates = [];
config.CURFEW_HOURS.forEach(function (hour) {
hour = (hour + 1) % 24;
if (config.CURFEW_HOURS.indexOf(hour) < 0)
candidates.push(makeToday(hour), makeTomorrow(hour));
});
candidates.sort(compare_dates);
for (var i = 0; i < candidates.length; i++)
if (candidates[i] > now)
return candidates[i];
return null;
};
function compare_dates(a, b) {
return a.getTime() - b.getTime();
}
function day_after(today) {
/* Leap shenanigans? This is probably broken somehow. Yay dates. */
var tomorrow = new Date(today.getTime() + 24*3600*1000);
if (tomorrow.getUTCDate() == today.getUTCDate())
tomorrow = new Date(tomorrow.getTime() + 12*3600*1000);
return tomorrow;
}
function hour_date_maker(date) {
var prefix = date.getUTCFullYear() + '/' + (date.getUTCMonth()+1)
+ '/' + date.getUTCDate() + ' ';
return (function (hour) {
return new Date(prefix + hour + ':00:00 GMT');
});
}
/* DAEMON */
function shutdown(board, cb) {
var yaku = new db.Yakusoku(board, db.UPKEEP_IDENT);
yaku.teardown(board, function (err) {
yaku.disconnect();
cb(err);
});
}
function at_next_curfew_start(board, func) {
var when = curfew_starting_time(board);
winston.info('Next curfew for ' + board + ' at ' + when.toUTCString());
setTimeout(func, when.getTime() - Date.now());
}
function enforce(board) {
at_next_curfew_start(board, function () {
winston.info('Curfew ' + board + ' at ' +
new Date().toUTCString());
shutdown(board, function (err) {
if (err)
winston.error(err);
});
setTimeout(enforce.bind(null, board), 30 * 1000);
});
}
if (config.CURFEW_BOARDS && config.CURFEW_HOURS)
config.CURFEW_BOARDS.forEach(enforce);

1947
db.js

File diff suppressed because it is too large Load Diff

@ -0,0 +1,89 @@
var config = require('./config');
var minJs = config.DEBUG ? '.js' : '.min.js';
exports.VENDOR_DEPS = [
'lib/es5-shim' + minJs,
'lib/underscore' + minJs,
'lib/backbone' + minJs,
'lib/oninput' + minJs,
'lib/jquery.cookie' + minJs,
];
exports.CLIENT_DEPS = [
'common.js',
'client/init.js',
'client/memory.js',
'client/models.js',
'client/extract.js',
'client/options.js',
'client/scroll.js',
'client/client.js',
'client/posting.js',
'client/menu.js',
'client/conn.js',
'client/time.js',
'client/notify.js',
'client/drop.js',
'client/hide.js',
'client/hover.js',
'client/amusement.js',
'client/embed.js',
'client/gravitas.js',
'curfew/client.js',
'report/client.js',
];
exports.SERVER_DEPS = [
'admin/common.js',
'admin/index.js',
'admin/panel.js',
'common.js',
'config.js',
'db.js',
'deps.js',
'etc.js',
'hooks.js',
'make_client.js',
'pipeline.js',
'tail.js',
'curfew/server.js',
'lib/underscore.js',
'lua/finish.lua',
'lua/get_thread.lua',
'imager/config.js',
'imager/daemon.js',
'imager/db.js',
'imager/index.js',
'imager/jobs.js',
'report/config.js',
'report/server.js',
'server/amusement.js',
'server/caps.js',
'server/msgcheck.js',
'server/okyaku.js',
'server/opts.js',
'server/perceptual.c',
'server/auth.js',
'server/render.js',
'server/server.js',
'server/state.js',
'server/web.js',
'tripcode/tripcode.cc',
];
// Changes to these only require a state.js reload
exports.SERVER_STATE = [
'admin/client.js',
'hot.js',
'tmpl/curfew.html',
'tmpl/filter.html',
'tmpl/index.html',
'tmpl/login.html',
'tmpl/redirect.html',
];
exports.MOD_CLIENT_DEPS = [
'admin/common.js',
'admin/client.js',
];

@ -0,0 +1,74 @@
#! /bin/sh
### BEGIN INIT INFO
# Provides: doushio
# Required-Start: redis-server
# Required-Stop:
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Custom init wrapper script for the doushio image board node.js server
# Description: Custom init wrapper script for the doushio image board node.js server
### END INIT INFO
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="doushio image board node.js server"
NAME=doushio
USER=doushio
DOUSHIO_DIR=/home/${USER}/server
NODE=/usr/bin/node
DAEMON=${DOUSHIO_DIR}/server/server.js
SCRIPTNAME=/etc/init.d/$NAME
LOG=/var/log/doushio.error.log
do_start()
{
if ps -p $(cat ${DOUSHIO_DIR}/doushio.pid) >/dev/null; then
do_stop
fi
if [ ! -f $LOG ]; then
touch $LOG
chown $USER $LOG
fi
su -l $USER -c "echo [`date -u +%Y-%m-%dT%T.%3NZ`] Starting >> $LOG
cd $DOUSHIO_DIR
$NODE server/server.js 1>/dev/null 2>>$LOG &
echo \$! > doushio.pid
$NODE archive/daemon.js 1>/dev/null 2>>$LOG &
echo \$! > doushio.archive.pid" $USER
}
do_stop()
{
kill $(cat ${DOUSHIO_DIR}/doushio.archive.pid)
kill $(cat ${DOUSHIO_DIR}/doushio.pid)
su -l $USER -c "echo [`date -u +%Y-%m-%dT%T.%3NZ`] Stopping >> $LOG"
}
do_reload()
{
su -l $USER -c "cd $DOUSHIO_DIR; $NODE server/kill.js --pid doushio.pid"
}
case "$1" in
start)
echo "Starting $DESC"
do_start
;;
stop)
echo "Stopping $DESC"
do_stop
;;
restart)
echo "Restarting $DESC"
do_start
;;
reload)
echo "Reloading $DESC"
do_reload
;;
*)
echo "Usage: $SCRIPTNAME {start|stop|reload|restart}" >&2
exit 3
;;
esac
:

@ -0,0 +1,72 @@
# Requires nginx >=1.4.
# Based on bakape and bakaonto's example configurations.
upstream node {
# Endpoint of the doushio node.js server
server 127.0.0.1:8000;
# Or if using a unix domain socket:
#server unix:/path/to/unix/domain/socket;
}
access_log /var/log/nginx/doushio.log;
# Additional WebSocket proxying support.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
# Domain the website will be hosted on.
server_name mydomain.com;
# You can forward various root-directory static files here.
root /path/to/doushio/www/;
location = /favicon.ico {}
location = /robots.txt {}
# Handles static assets (images, JS, CSS, etc.)
# Requires "SERVE_STATIC_FILES: false" in ./config.js
# Set imager/config MEDIA_URL to '/ass/'.
# The trailing "/" is important.
location /ass/ {
alias /path/to/doushio/www/;
expires 2d;
add_header X-Content-Type-Options nosniff;
}
# Handles image uploads.
location /upload/ {
# If you use imager/config DAEMON, add an upstream for the
# imager daemon, and point this at it.
proxy_pass http://node/upload/;
# For forwarding IPs:
# Set "TRUST_X_FORWARDED_FOR: true" in ./config.js
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip; # CloudFlare
# Adjust this to your imager/config IMAGE_FILESIZE_MAX.
client_max_body_size 5m;
# Allow for prolonged uploads.
client_body_timeout 300s;
# This may give you more accurate upload progress.
#proxy_buffering off;
}
# Handles the imageboard.
location / {
proxy_pass http://node;
proxy_buffering off;
# WebSockets support.
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip; # CloudFlare
}
}

@ -0,0 +1,94 @@
var child_process = require('child_process'),
fs = require('fs'),
util = require('util'),
winston = require('winston');
/* Non-wizard-friendly error message */
function Muggle(message, reason) {
if (!(this instanceof Muggle))
return new Muggle(message, reason);
Error.call(this, message);
Error.captureStackTrace(this, this.constructor);
this.message = message;
this.reason = reason;
}
util.inherits(Muggle, Error);
exports.Muggle = Muggle;
Muggle.prototype.most_precise_error_message = function () {
var deepest = this.message;
var muggle = this;
var sanity = 10;
while (muggle.reason && muggle.reason instanceof Muggle) {
muggle = muggle.reason;
if (muggle.message && typeof muggle.message == 'string')
deepest = muggle.message;
if (--sanity <= 0)
break;
}
return deepest;
};
Muggle.prototype.deepest_reason = function () {
if (this.reason && this.reason instanceof Muggle)
return this.reason.deepest_reason();
return this;
};
exports.move = function (src, dest, callback) {
child_process.execFile('/bin/mv', ['--', src, dest],
function (err, stdout, stderr) {
if (err)
callback(Muggle("Couldn't move file into place.",
stderr || err));
else
callback(null);
});
};
exports.movex = function (src, dest, callback) {
child_process.execFile('/bin/mv', ['-n', '--', src, dest],
function (err, stdout, stderr) {
if (err)
callback(Muggle("Couldn't move file into place.",
stderr || err));
else
callback(null);
});
};
exports.cpx = function (src, dest, callback) {
// try to do a graceful (non-overwriting) copy
child_process.execFile('/bin/cp', ['-n', '--', src, dest],
function (err, stdout, stderr) {
if (err) {
winston.warn('overwriting (' + src + ') to (' + dest + ').');
// just overwrite
child_process.execFile('/bin/cp', ['--', src, dest], function (err, o, e) {
if (err)
callback(Muggle("Couldn't copy file into place.",
e || err));
else
callback(null);
});
}
else
callback(null);
});
};
exports.checked_mkdir = function (dir, cb) {
fs.mkdir(dir, function (err) {
cb(err && err.code == 'EEXIST' ? null : err);
});
};
// TEMP duplicated from common.js for imager daemon sanity
exports.random_id = function () {
return Math.floor(Math.random() * 1e16) + 1;
};
exports.json_paranoid = function (obj) {
return JSON.stringify(obj).replace(/\//g, '\\x2f');
};

@ -0,0 +1,38 @@
var async = require('async');
var HOOKS = {}, SYNC_HOOKS = {};
exports.hook = function (key, func) {
var hs = HOOKS[key];
if (hs)
hs.push(func);
else
HOOKS[key] = [func];
};
exports.trigger = function (key, arg, cb) {
var hs = HOOKS[key] || [];
async.forEachSeries(hs, function (hook, next) {
hook(arg, next);
}, function (err) {
if (err)
cb(err);
else
cb(null, arg);
});
};
exports.hook_sync = function (key, func) {
var hs = SYNC_HOOKS[key];
if (hs)
hs.push(func);
else
SYNC_HOOKS[key] = [func];
};
exports.trigger_sync = function (key, arg) {
var hs = SYNC_HOOKS[key] || [];
hs.forEach(function (func) {
func(arg);
});
};

@ -0,0 +1,31 @@
this.hot = {
EMAIL: "lalc@doushio.com",
TITLES: {
moe: "/moe/ - Sweets",
gar: "/gar/ - Hard Work &amp; Guts",
meta: "/meta/ - The Abyss",
archive: "/archive/",
graveyard: "/graveyard/",
staff: "/staff/",
},
/* Default theme to use on each board */
BOARD_CSS: {
moe: 'moe',
gar: 'gar',
meta: 'mawaru',
archive: 'archive',
graveyard: 'mawaru',
staff: 'moe',
},
/* Bump this version whenever you change base.css */
BASE_CSS_VERSION: 49,
/* Bump this version whenever you modify any existing theme CSS */
THEME_CSS_VERSION: 11,
INTER_BOARD_NAVIGATION: true,
SPECIAL_TRIPCODES: {
kyubey: "/人◕ ‿‿ ◕人\",
},
};

@ -0,0 +1,15 @@
all: findapng perceptual
PNGFLAGS := $(shell pkg-config libpng --cflags)
PNGLIBS := $(shell pkg-config libpng --libs)
findapng: findapng.c
gcc -O2 $(PNGFLAGS) -o $@ $^ $(PNGLIBS)
perceptual: perceptual.c
gcc -O2 -o $@ $^
.PHONY: all clean
clean:
rm -rf -- findapng perceptual

@ -0,0 +1,79 @@
module.exports = {
IMAGE_FILESIZE_MAX: 1024 * 1024 * 3,
IMAGE_WIDTH_MAX: 6000,
IMAGE_HEIGHT_MAX: 6000,
IMAGE_PIXELS_MAX: 4500*4500,
MEDIA_DIRS: {
src: 'www/src',
thumb: 'www/thumb',
mid: 'www/mid',
vint: 'www/vint',
dead: 'graveyard',
tmp: 'imager/tmp',
},
MEDIA_URL: '../',
UPLOAD_URL: '../upload/',
// this should be the same as location.origin
// in your browser's javascript console
MAIN_SERVER_ORIGIN: 'http://localhost:8000',
// this is the origin of upload_url
// (usually this will be the same as the main server origin,
// or maybe a different subdomain)
UPLOAD_ORIGIN: 'http://localhost:8000',
PINKY_QUALITY: 50,
PINKY_DIMENSIONS: [125, 125],
THUMB_QUALITY: 50,
THUMB_DIMENSIONS: [250, 250],
EXTRA_MID_THUMBNAILS: true,
// PNG thumbnails for PNG images. This enables thumbnail transparency.
// Warning: significantly increases page size for large threads.
PNG_THUMBS: false,
PNG_THUMB_QUALITY: 105,
// allow video files (requires ffmpeg)
VIDEO: false,
VIDEO_EXTS: ['.webm', '.mp4'],
// allow audio streams
AUDIO: false,
// uncomment this to have all audio uploads overlaid with
// the corresponding spoiler image
/*
AUDIO_SPOILER: 20,
*/
// set to 0 to disable duplicate detector
DUPLICATE_COOLDOWN: 3600,
SPOILER_DIR: '../assets/kana',
// this indicates which spoiler images may be selected by posters.
// each number or ID corresponds to a set of images in SPOILER_DIR
// (named spoilX.png, spoilerX.png and spoilersX.png)
// (e.g. https://github.com/lalcmellkmal/assets/tree/master/kana)
//
// the spoilers in the `normal` list are simple images,
// while the spoilers in the `trans` list are composited on top
// of the original image semi-opaquely (and must be transparent PNGs)
SPOILER_IMAGES: {
normal: [],
trans: [4, 5, 6, 7, 8, 9, 10, 11]
},
IMAGE_HATS: false,
// uncomment DAEMON if you will run `node imager/daemon.js` separately.
// if so, either
// 1) customize UPLOAD_URL above appropriately, or
// 2) configure your reverse proxy so that requests for /upload/
// are forwarded to LISTEN_PORT.
/*
DAEMON: {
LISTEN_PORT: 9000,
// this doesn't have to (and shouldn't) be the same redis db
// as is used by the main doushio server.
REDIS_PORT: 6379,
},
*/
};

@ -0,0 +1,831 @@
var async = require('async'),
config = require('./config'),
child_process = require('child_process'),
etc = require('../etc'),
Muggle = etc.Muggle,
imagerDb = require('./db'),
index = require('./'),
formidable = require('formidable'),
fs = require('fs'),
jobs = require('./jobs'),
path = require('path'),
urlParse = require('url').parse,
util = require('util'),
winston = require('winston');
var IMAGE_EXTS = ['.png', '.jpg', '.gif'];
if (config.VIDEO && !config.DAEMON) {
console.warn("Please enable imager.config.DAEMON security.");
}
function new_upload(req, resp) {
var upload = new ImageUpload;
upload.handle_request(req, resp);
}
exports.new_upload = new_upload;
function get_thumb_specs(image, pinky, scale) {
var w = image.dims[0], h = image.dims[1];
var bound = config[pinky ? 'PINKY_DIMENSIONS' : 'THUMB_DIMENSIONS'];
var r = Math.max(w / bound[0], h / bound[1], 1);
var dims = [Math.round(w/r) * scale, Math.round(h/r) * scale];
var specs = {bound: bound, dims: dims, format: 'jpg'};
// Note: WebMs pretend to be PNGs at this step,
// but those don't need transparent backgrounds.
// (well... WebMs *can* have alpha channels...)
if (config.PNG_THUMBS && image.ext == '.png' && !image.video) {
specs.format = 'png';
specs.quality = config.PNG_THUMB_QUALITY;
}
else if (pinky) {
specs.bg = '#ffffff';
specs.quality = config.PINKY_QUALITY;
}
else {
specs.bg = '#ffffff';
specs.quality = config.THUMB_QUALITY;
}
return specs;
}
var ImageUpload = function (client_id) {
this.db = new imagerDb.Onegai;
this.client_id = client_id;
};
var IU = ImageUpload.prototype;
var validFields = ['spoiler', 'op'];
IU.status = function (msg) {
this.client_call('status', msg);
};
IU.client_call = function (t, msg) {
this.db.client_message(this.client_id, {t: t, arg: msg});
};
IU.respond = function (code, msg) {
if (!this.resp)
return;
const origin = config.MAIN_SERVER_ORIGIN;
this.resp.writeHead(code, {
'Content-Type': 'text/html; charset=UTF-8',
'Access-Control-Allow-Origin': origin,
});
this.resp.end('<!doctype html><title>Upload result</title>\n'
+ 'This is a legitimate imager response.\n'
+ '<script>\nparent.postMessage(' + etc.json_paranoid(msg)
+ ', ' + etc.json_paranoid(origin) + ');\n'
+ '</script>\n');
this.resp = null;
};
IU.handle_request = function (req, resp) {
if (req.method.toLowerCase() != 'post') {
resp.writeHead(405, {Allow: 'POST'});
resp.end();
return;
}
this.resp = resp;
var query = req.query || urlParse(req.url, true).query;
this.client_id = parseInt(query.id, 10);
if (!this.client_id || this.client_id < 1) {
this.respond(400, "Bad client ID.");
return;
}
var len = parseInt(req.headers['content-length'], 10);
if (len > 0 && len > config.IMAGE_FILESIZE_MAX + (20*1024))
return this.failure(Muggle('File is too large.'));
var form = new formidable.IncomingForm({
uploadDir: config.MEDIA_DIRS.tmp,
maxFieldsSize: 50 * 1024,
hash: 'md5',
});
form.onPart = function (part) {
if (part.filename && part.name == 'image')
form.handlePart(part);
else if (!part.filename && validFields.indexOf(part.name) >= 0)
form.handlePart(part);
};
var self = this;
form.once('error', function (err) {
self.failure(Muggle('Upload request problem.', err));
});
form.once('aborted', function (err) {
self.failure(Muggle('Upload was aborted.', err));
});
this.lastProgress = 0;
form.on('progress', this.upload_progress_status.bind(this));
try {
form.parse(req, this.parse_form.bind(this));
}
catch (err) {
self.failure(err);
}
};
IU.upload_progress_status = function (received, total) {
var percent = Math.floor(100 * received / total);
var increment = (total > (512 * 1024)) ? 10 : 25;
var quantized = Math.floor(percent / increment) * increment;
if (quantized > this.lastProgress) {
this.status(percent + '% received...');
this.lastProgress = quantized;
}
};
IU.parse_form = function (err, fields, files) {
if (err)
return this.failure(Muggle('Invalid upload.', err));
if (!files.image)
return this.failure(Muggle('No image.'));
this.image = files.image;
this.pinky = !!parseInt(fields.op, 10);
var spoiler = parseInt(fields.spoiler, 10);
if (spoiler) {
var sps = config.SPOILER_IMAGES;
if (sps.normal.indexOf(spoiler) < 0
&& sps.trans.indexOf(spoiler) < 0)
return this.failure(Muggle('Bad spoiler.'));
this.image.spoiler = spoiler;
}
this.image.MD5 = index.squish_MD5(this.image.hash);
this.image.hash = null;
var self = this;
this.db.track_temporary(this.image.path, function (err) {
if (err)
winston.warn("Temp tracking error: " + err);
self.process();
});
};
IU.process = function () {
if (this.failed)
return;
var image = this.image;
var filename = image.filename || image.name;
image.ext = path.extname(filename).toLowerCase();
if (image.ext == '.jpeg')
image.ext = '.jpg';
if (image.ext == '.mov')
image.ext = '.mp4';
if (IMAGE_EXTS.indexOf(image.ext) < 0
&& (!config.VIDEO || config.VIDEO_EXTS.indexOf(image.ext) < 0))
return this.failure(Muggle('Invalid image format.'));
image.imgnm = filename.substr(0, 256);
this.status('Verifying...');
if (config.VIDEO_EXTS.indexOf(image.ext) >= 0)
video_still(image.path, image.ext, this.verify_video.bind(this));
else if (image.ext == '.jpg' && jpegtranBin && jheadBin)
jobs.schedule(new AutoRotateJob(image.path), this.verify_image.bind(this));
else
this.verify_image();
};
function AutoRotateJob(src) {
jobs.Job.call(this);
this.src = src;
}
util.inherits(AutoRotateJob, jobs.Job);
AutoRotateJob.prototype.describe_job = function () {
return "jhead+jpegtran auto rotation of " + this.src;
};
AutoRotateJob.prototype.perform_job = function () {
var self = this;
child_process.execFile(jheadBin, ['-autorot', this.src], function (err, stdout, stderr) {
// if it failed, keep calm and thumbnail on
if (err)
winston.warn('jhead: ' + (stderr || err));
self.finish_job(null);
});
};
function StillJob(src, ext) {
jobs.Job.call(this);
this.src = src;
this.ext = ext;
}
util.inherits(StillJob, jobs.Job);
StillJob.prototype.describe_job = function () {
return "FFmpeg video still of " + this.src;
};
StillJob.prototype.perform_job = function () {
var dest = index.media_path('tmp', 'still_'+etc.random_id());
var args = ['-hide_banner', '-loglevel', 'info',
'-i', this.src,
'-f', 'image2', '-vf', 'thumbnail', '-vframes', '1', '-vcodec', 'png',
'-y', dest];
var opts = {env: {AV_LOG_FORCE_NOCOLOR: '1'}};
var self = this;
child_process.execFile(ffmpegBin, args, opts, function (err, stdout, stderr) {
var lines = stderr ? stderr.split('\n') : [];
var first = lines[0];
if (err) {
var msg;
if (/no such file or directory/i.test(first))
msg = "Video went missing.";
else if (/invalid data found when/i.test(first))
msg = "Invalid video file.";
else if (/^ffmpeg version/i.test(first))
msg = "Server's ffmpeg is too old.";
else {
msg = "Unknown video reading error.";
winston.warn("Unknown ffmpeg output: "+first);
}
fs.unlink(dest, function (err) {
self.finish_job(Muggle(msg, stderr));
});
return;
}
self.test_format(first, stderr, function (format_err, has_audio, dur) {
if (err) {
fs.unlink(dest, function (_unlink_err) {
self.finish_job(Muggle(format_err));
});
return;
}
self.finish_job(null, {
still_path: dest,
has_audio: has_audio,
duration: dur,
});
});
});
};
StillJob.prototype.test_format = function (first, full, cb) {
/* Could have false positives due to chapter titles. Bah. */
var has_audio = /stream\s*#0.*audio:/i.test(full);
/* Spoofable? */
var dur = /duration: (\d\d):(\d\d):(\d\d)/i.exec(full);
if (dur) {
var m = parseInt(dur[2], 10), s = parseInt(dur[3], 10);
if (dur[1] != '00' || m > 2)
return cb('Video exceeds 3 minutes.');
dur = (m ? m + 'm' : '') + s + 's';
if (dur == '0s')
dur = '1s';
}
else {
winston.warn("Could not parse duration:\n" + full);
}
if (/stream #1/i.test(full))
return cb('Video contains more than one stream.');
if (this.ext == '.webm') {
if (!/matroska,webm/i.test(first))
return cb('Video stream is not WebM.');
cb(null, has_audio, dur);
}
else if (this.ext == '.mp4') {
if (!/mp4,/i.test(first))
return cb('Video stream is not mp4.');
cb(null, has_audio, dur);
}
else {
cb('Unsupported video format.');
}
}
function video_still(src, ext, cb) {
jobs.schedule(new StillJob(src, ext), cb);
}
IU.verify_video = function (err, info) {
if (err)
return this.failure(err);
var self = this;
this.db.track_temporary(info.still_path, function (err) {
if (err)
winston.warn("Tracking error: " + err);
if (info.has_audio && !config.AUDIO)
return self.failure(Muggle('Audio is not allowed.'));
// pretend it's a PNG for the next steps
var image = self.image;
image.video = image.ext.replace('.', '');
image.video_path = image.path;
image.path = info.still_path;
image.ext = '.png';
if (info.has_audio) {
image.audio = true;
if (config.AUDIO_SPOILER)
image.spoiler = config.AUDIO_SPOILER;
}
if (info.duration)
image.duration = info.duration;
self.verify_image();
});
};
IU.verify_image = function (err) {
if (err)
winston.error(err);
var image = this.image;
this.tagged_path = image.ext.replace('.', '') + ':' + image.path;
var checks = {
stat: fs.stat.bind(fs, image.video_path || image.path),
dims: identify.bind(null, this.tagged_path),
};
if (image.ext == '.png')
checks.apng = detect_APNG.bind(null, image.path);
var self = this;
async.parallel(checks, function (err, rs) {
if (err)
return self.failure(Muggle('Wrong image type.', err));
image.size = rs.stat.size;
image.dims = [rs.dims.width, rs.dims.height];
if (rs.apng)
image.apng = 1;
self.verified();
});
};
IU.verified = function () {
if (this.failed)
return;
var desc = this.image.video ? 'Video' : 'Image';
var w = this.image.dims[0], h = this.image.dims[1];
if (!w || !h)
return this.failure(Muggle('Bad image dimensions.'));
if (config.IMAGE_PIXELS_MAX && w * h > config.IMAGE_PIXELS_MAX)
return this.failure(Muggle('Way too many pixels.'));
if (w > config.IMAGE_WIDTH_MAX && h > config.IMAGE_HEIGHT_MAX)
return this.failure(Muggle(desc+' is too wide and too tall.'));
if (w > config.IMAGE_WIDTH_MAX)
return this.failure(Muggle(desc+' is too wide.'));
if (h > config.IMAGE_HEIGHT_MAX)
return this.failure(Muggle(desc+' is too tall.'));
var self = this;
perceptual_hash(this.tagged_path, this.image, function (err, hash) {
if (err)
return self.failure(err);
self.image.hash = hash;
self.db.check_duplicate(hash, function (err) {
if (err)
return self.failure(err);
self.deduped();
});
});
};
IU.fill_in_specs = function (specs, kind) {
specs.src = this.tagged_path;
specs.ext = this.image.ext;
specs.dest = this.image.path + '_' + kind;
this.image[kind + '_path'] = specs.dest;
};
IU.deduped = function (err) {
if (this.failed)
return;
var image = this.image;
var specs = get_thumb_specs(image, this.pinky, 1);
var w = image.dims[0], h = image.dims[1];
/* Determine whether we really need a thumbnail */
var sp = image.spoiler;
if (!sp && image.size < 30*1024
&& ['.jpg', '.png'].indexOf(image.ext) >= 0
&& !image.apng && !image.video
&& w <= specs.dims[0] && h <= specs.dims[1]) {
return this.got_nails();
}
this.fill_in_specs(specs, 'thumb');
// was a composited spoiler selected or forced?
if (image.audio && config.AUDIO_SPOILER)
specs.comp = specs.overlay = true;
if (sp && config.SPOILER_IMAGES.trans.indexOf(sp) >= 0)
specs.comp = true;
var self = this;
if (specs.comp) {
this.status(specs.overlay ? 'Overlaying...' : 'Spoilering...');
var comp = composite_src(sp, this.pinky);
image.comp_path = image.path + '_comp';
specs.compDims = specs.overlay ? specs.dims : specs.bound;
image.dims = [w, h].concat(specs.compDims);
specs.composite = comp;
specs.compDest = image.comp_path;
async.parallel([
self.resize_and_track.bind(self, specs, false),
self.resize_and_track.bind(self, specs, true),
], function (err) {
if (err)
return self.failure(err);
self.got_nails();
});
}
else {
image.dims = [w, h].concat(specs.dims);
if (!sp)
this.status('Thumbnailing...');
self.resize_and_track(specs, false, function (err) {
if (err)
return self.failure(err);
if (config.EXTRA_MID_THUMBNAILS)
self.middle_nail();
else
self.got_nails();
});
}
};
IU.middle_nail = function () {
if (this.failed)
return;
var specs = get_thumb_specs(this.image, this.pinky, 2);
this.fill_in_specs(specs, 'mid');
var self = this;
this.resize_and_track(specs, false, function (err) {
if (err)
self.failure(err);
self.got_nails();
});
};
IU.got_nails = function () {
if (this.failed)
return;
var image = this.image;
if (image.video_path) {
// stop pretending this is just a still image
image.path = image.video_path;
image.ext = '.' + image.video;
delete image.video_path;
}
var time = Date.now();
image.src = time + image.ext;
var base = path.basename;
var tmps = {src: base(image.path)};
if (image.thumb_path) {
image.thumb = time + '.jpg';
tmps.thumb = base(image.thumb_path);
}
if (image.mid_path) {
image.mid = time + '.jpg';
tmps.mid = base(image.mid_path);
}
if (image.comp_path) {
image.composite = time + 's' + image.spoiler + '.jpg';
tmps.comp = base(image.comp_path);
delete image.spoiler;
}
this.record_image(tmps);
};
function composite_src(spoiler, pinky) {
var file = 'spoiler' + (pinky ? 's' : '') + spoiler + '.png';
return path.join(config.SPOILER_DIR, file);
}
IU.read_image_filesize = function (callback) {
var self = this;
fs.stat(this.image.path, function (err, stat) {
if (err)
callback(Muggle('Internal filesize error.', err));
else if (stat.size > config.IMAGE_FILESIZE_MAX)
callback(Muggle('File is too large.'));
else
callback(null, stat.size);
});
};
function which(name, callback) {
child_process.exec('which ' + name, function (err, stdout, stderr) {
if (err)
callback(err);
else
callback(null, stdout.trim());
});
}
/* Look up imagemagick paths */
var identifyBin, convertBin;
which('identify', function (err, bin) { if (err) throw err; identifyBin = bin; });
which('convert', function (err, bin) { if (err) throw err; convertBin = bin; });
var ffmpegBin;
if (config.VIDEO) {
which('ffmpeg', function (err, bin) { if (err) throw err; ffmpegBin = bin; });
}
/* optional JPEG auto-rotation */
var jpegtranBin, jheadBin;
which('jpegtran', function (err, bin) { if (!err && bin) jpegtranBin = bin; });
which('jhead', function (err, bin) { if (!err && bin) jheadBin = bin; });
function identify(taggedName, callback) {
var m = taggedName.match(/^(\w{3,4}):/);
var args = ['-format', '%Wx%H', taggedName + '[0]'];
child_process.execFile(identifyBin, args, function (err,stdout,stderr){
if (err) {
var msg = "Bad image.";
if (stderr.match(/no such file/i))
msg = "Image went missing.";
else if (stderr.match(/improper image header/i)) {
var kind = m && m[1];
kind = kind ? 'a ' + kind.toUpperCase()
: 'an image';
msg = 'File is not ' + kind + '.';
}
else if (stderr.match(/no decode delegate/i))
msg = "Unsupported file type.";
return callback(Muggle(msg, stderr));
}
var line = stdout.trim();
var m = line.match(/(\d+)x(\d+)/);
if (!m)
callback(Muggle("Couldn't read image dimensions."));
else
callback(null, {width: parseInt(m[1], 10),
height: parseInt(m[2], 10)});
});
}
function ConvertJob(args, src) {
jobs.Job.call(this);
this.args = args;
this.src = src;
}
util.inherits(ConvertJob, jobs.Job);
ConvertJob.prototype.perform_job = function () {
var self = this;
child_process.execFile(convertBin, this.args,
function (err, stdout, stderr) {
self.finish_job(err ? (stderr || err) : null);
});
};
ConvertJob.prototype.describe_job = function () {
return "ImageMagick conversion of " + this.src;
};
function convert(args, src, callback) {
jobs.schedule(new ConvertJob(args, src), callback);
}
function perceptual_hash(src, image, callback) {
var tmp = index.media_path('tmp',
'hash' + etc.random_id() + '.gray');
var args = [src + '[0]'];
if (image.dims.width > 1000 || image.dims.height > 1000)
args.push('-sample', '800x800');
// do you believe in magic?
args.push('-background', 'white', '-mosaic', '+matte',
'-scale', '16x16!',
'-type', 'grayscale', '-depth', '8',
tmp);
convert(args, src, function (err) {
if (err)
return callback(Muggle('Hashing error.', err));
var bin = path.join(__dirname, 'perceptual');
child_process.execFile(bin, [tmp],
function (err, stdout, stderr) {
fs.unlink(tmp, function (err) {
if (err)
winston.warn("Deleting " +
tmp + ": " + err);
});
if (err)
return callback(Muggle('Hashing error.',
stderr || err));
var hash = stdout.trim();
if (hash.length != 64)
return callback(Muggle('Hashing problem.'));
callback(null, hash);
});
});
}
function detect_APNG(fnm, callback) {
var bin = path.join(__dirname, 'findapng');
child_process.execFile(bin, [fnm], function (err, stdout, stderr) {
if (err)
return callback(Muggle('APNG detector problem.',
stderr || err));
else if (stdout.match(/^APNG/))
return callback(null, true);
else if (stdout.match(/^PNG/))
return callback(null, false);
else
return callback(Muggle('APNG detector acting up.',
stderr || err));
});
}
function setup_image_params(o) {
// only the first time!
if (o.setup) return;
o.setup = true;
o.src += '[0]'; // just the first frame of the animation
o.dest = o.format + ':' + o.dest;
if (o.compDest)
o.compDest = o.format + ':' + o.compDest;
o.flatDims = o.dims[0] + 'x' + o.dims[1];
if (o.compDims)
o.compDims = o.compDims[0] + 'x' + o.compDims[1];
o.quality += ''; // coerce to string
}
function build_im_args(o, args) {
// avoid OOM killer
var args = ['-limit', 'memory', '32', '-limit', 'map', '64'];
var dims = o.dims;
// resample from twice the thumbnail size
// (avoid sampling from the entirety of enormous 6000x6000 images etc)
var samp = dims[0]*2 + 'x' + dims[1]*2;
if (o.ext == '.jpg')
args.push('-define', 'jpeg:size=' + samp);
setup_image_params(o);
args.push(o.src);
if (o.ext != '.jpg')
args.push('-sample', samp);
// gamma-correct yet shitty downsampling
args.push('-gamma', '0.454545', '-filter', 'box');
return args;
}
function resize_image(o, comp, callback) {
var args = build_im_args(o);
var dims = comp ? o.compDims : o.flatDims;
var dest = comp ? o.compDest : o.dest;
// in the composite case, zoom to fit. otherwise, force new size
args.push('-resize', dims + (comp ? '^' : '!'));
// add background
args.push('-gamma', '2.2');
if (o.bg)
args.push('-background', o.bg);
if (comp)
args.push(o.composite, '-layers', 'flatten', '-extent', dims);
else if (o.bg)
args.push('-layers', 'mosaic', '+matte');
// disregard metadata, acquire artifacts
args.push('-strip', '-quality', o.quality);
args.push(dest);
convert(args, o.src, function (err) {
if (err) {
winston.warn(err);
callback(Muggle("Resizing error.", err));
}
else
callback(null, dest);
});
}
IU.resize_and_track = function (o, comp, cb) {
var self = this;
resize_image(o, comp, function (err, fnm) {
if (err)
return cb(err);
// HACK: strip IM type tag
var m = /^\w{3,4}:(.+)$/.exec(fnm);
if (m)
fnm = m[1];
self.db.track_temporary(fnm, cb);
});
};
function image_files(image) {
var files = [];
if (image.path)
files.push(image.path);
if (image.thumb_path)
files.push(image.thumb_path);
if (image.mid_path)
files.push(image.mid_path);
if (image.comp_path)
files.push(image.comp_path);
return files;
}
IU.failure = function (err) {
var err_desc = 'Unknown image processing error.'
if (err instanceof Muggle) {
err_desc = err.most_precise_error_message();
err = err.deepest_reason();
}
/* Don't bother logging PEBKAC errors */
if (!(err instanceof Muggle))
winston.error(err);
this.respond(500, err_desc);
if (!this.failed) {
this.client_call('error', err_desc);
this.failed = true;
}
if (this.image) {
var files = image_files(this.image);
files.forEach(function (file) {
fs.unlink(file, function (err) {
if (err)
winston.warn("Deleting " +
file + ": " + err);
});
});
this.db.lose_temporaries(files, function (err) {
if (err)
winston.warn("Tracking failure: " + err);
});
}
this.db.disconnect();
};
IU.record_image = function (tmps) {
if (this.failed)
return;
var view = {};
var self = this;
index.image_attrs.forEach(function (key) {
if (key in self.image)
view[key] = self.image[key];
});
if (this.image.composite) {
view.realthumb = view.thumb;
view.thumb = this.image.composite;
}
view.pinky = this.pinky;
var image_id = etc.random_id().toFixed();
var alloc = {image: view, tmps: tmps};
this.db.record_image_alloc(image_id, alloc, function (err) {
if (err)
return this.failure("Image storage failure.");
self.client_call('alloc', image_id);
self.db.disconnect();
self.respond(202, 'OK');
if (index.is_standalone()) {
var where = view.src;
var size = Math.ceil(view.size / 1000) + 'kb';
winston.info('upload: ' + where + ' ' + size);
}
});
};
function run_daemon() {
var cd = config.DAEMON;
var is_unix_socket = (typeof cd.LISTEN_PORT == 'string');
if (is_unix_socket) {
try { fs.unlinkSync(cd.LISTEN_PORT); } catch (e) {}
}
var server = require('http').createServer(new_upload);
server.listen(cd.LISTEN_PORT);
if (is_unix_socket) {
fs.chmodSync(cd.LISTEN_PORT, '777'); // TEMP
}
index._make_media_dir(null, 'tmp', function (err) {});
winston.info('Imager daemon listening on '
+ (cd.LISTEN_HOST || '')
+ (is_unix_socket ? '' : ':')
+ (cd.LISTEN_PORT + '.'));
}
if (require.main == module) (function () {
if (!index.is_standalone())
throw new Error("Please enable DAEMON in imager/config.js");
var onegai = new imagerDb.Onegai;
onegai.delete_temporaries(function (err) {
onegai.disconnect();
if (err)
throw err;
process.nextTick(run_daemon);
});
})();

@ -0,0 +1,170 @@
var config = require('./config'),
events = require('events'),
fs = require('fs'),
Muggle = require('../etc').Muggle,
tail = require('../tail'),
util = require('util'),
winston = require('winston');
var IMG_EXPIRY = 60;
var STANDALONE = !!config.DAEMON;
function redis_client() {
if (STANDALONE) {
return require('redis').createClient(config.DAEMON.REDIS_PORT);
}
else {
return require('../db').redis_client();
}
}
exports.connect = redis_client;
function Onegai() {
events.EventEmitter.call(this);
}
util.inherits(Onegai, events.EventEmitter);
exports.Onegai = Onegai;
var O = Onegai.prototype;
O.connect = function () {
if (STANDALONE) {
if (!global.imagerRedis)
global.imagerRedis = redis_client();
return global.imagerRedis;
}
return global.redis;
};
O.disconnect = function () {};
O.track_temporary = function (path, cb) {
var m = this.connect();
var self = this;
m.sadd('temps', path, function (err, tracked) {
if (err)
return cb(err);
if (tracked > 0) {
setTimeout(self.del_temp.bind(self, path),
(IMG_EXPIRY+1) * 1000);
}
cb(null);
});
};
O.lose_temporaries = function (files, cb) {
this.connect().srem('temps', files, cb);
};
O.del_temp = function (path) {
this.cleanup_image_alloc(path, function (err, deleted) {
if (err) {
winston.warn('unlink ' + path + ': '
+ err);
}
});
};
// if an image doesn't get used in a post in a timely fashion, delete it
O.cleanup_image_alloc = function (path, cb) {
var r = this.connect();
r.srem('temps', path, function (err, n) {
if (err)
return winston.warn(err);
if (n) {
fs.unlink(path, function (err) {
if (err)
return cb(err);
cb(null, true);
});
}
else {
cb(null, false); // wasn't found
}
});
};
// catch any dangling images on server startup
O.delete_temporaries = function (callback) {
var r = this.connect();
r.smembers('temps', function (err, temps) {
if (err)
return callback(err);
tail.forEach(temps, function (temp, cb) {
fs.unlink(temp, function (err) {
if (err)
winston.warn('temp: ' + err);
else
winston.info('del temp ' + temp);
cb(null);
});
}, function (err) {
if (err)
return callback(err);
r.del('temps', callback);
});
});
};
O.check_duplicate = function (hash, callback) {
this.connect().get('hash:'+hash, function (err, num) {
if (err)
callback(err);
else if (num)
callback(Muggle('Duplicate of >>' + num + '.'));
else
callback(false);
});
};
O.record_image_alloc = function (id, alloc, callback) {
var r = this.connect();
r.setex('image:' + id, IMG_EXPIRY, JSON.stringify(alloc), callback);
};
O.obtain_image_alloc = function (id, callback) {
var m = this.connect().multi();
var key = 'image:' + id;
m.get(key);
m.setnx('lock:' + key, '1');
m.expire('lock:' + key, IMG_EXPIRY);
m.exec(function (err, rs) {
if (err)
return callback(err);
if (rs[1] != 1)
return callback(Muggle("Image in use."));
if (!rs[0])
return callback(Muggle("Image lost."));
var alloc = JSON.parse(rs[0]);
alloc.id = id;
callback(null, alloc);
});
};
exports.is_standalone = function () { return STANDALONE; };
O.commit_image_alloc = function (alloc, cb) {
// We should already hold the lock at this point.
var key = 'image:' + alloc.id;
var m = this.connect().multi();
m.del(key);
m.del('lock:' + key);
m.exec(cb);
};
O.client_message = function (client_id, msg) {
this.connect().publish('client:' + client_id, JSON.stringify(msg));
};
O.relay_client_messages = function () {
var r = redis_client();
r.psubscribe('client:*');
var self = this;
r.once('psubscribe', function () {
self.emit('relaying');
r.on('pmessage', function (pat, chan, message) {
var id = parseInt(chan.match(/^client:(\d+)$/)[1], 10);
self.emit('message', id, JSON.parse(message));
});
});
};

@ -0,0 +1,64 @@
#include <png.h>
#include <stdio.h>
#include <string.h>
#define FAIL(format, args...) do { fprintf(stderr, format "\n", ## args); \
goto done; } while (0)
static int apng = 0;
static int read_chunk(png_structp png_ptr, png_unknown_chunkp chunk) {
(void) png_ptr;
if (strncmp((const char *) chunk, "acTL", 4) == 0)
apng = 1;
return 0;
}
int main(int argc, char *argv[]) {
FILE *fp;
const char *filename;
unsigned char header[8];
png_structp png_ptr = NULL;
png_infop info_ptr = NULL;
int result = 1;
if (argc != 2) {
fprintf(stderr, "Usage: %s <png>\n", argv[0]);
return -1;
}
filename = argv[1];
fp = fopen(filename, "rb");
if (!fp) {
perror(filename);
return -1;
}
if (fread(header, 8, 1, fp) != 1)
FAIL("%s: Couldn't read header.", filename);
if (png_sig_cmp(header, 0, 8))
FAIL("%s: Not a PNG.", filename);
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
NULL, NULL, NULL);
if (!png_ptr)
FAIL("Couldn't set up PNG reader.");
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
FAIL("Couldn't set up PNG info reader.");
if (setjmp(png_jmpbuf(png_ptr)))
goto done;
png_init_io(png_ptr, fp);
png_set_sig_bytes(png_ptr, 8);
png_set_read_user_chunk_fn(png_ptr, NULL, &read_chunk);
png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_NEVER, NULL, 0);
png_read_info(png_ptr, info_ptr);
puts(apng ? "APNG" : "PNG");
result = 0;
done:
if (png_ptr)
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
fclose(fp);
return result;
}

@ -0,0 +1,227 @@
var async = require('async'),
config = require('./config'),
child_process = require('child_process'),
db = require('./db'),
etc = require('../etc'),
fs = require('fs'),
hooks = require('../hooks'),
path = require('path'),
winston = require('winston');
exports.Onegai = db.Onegai;
exports.config = config;
var image_attrs = ('src thumb dims size MD5 hash imgnm spoiler realthumb vint'
+ ' apng mid audio video duration').split(' ');
exports.image_attrs = image_attrs;
exports.send_dead_image = function (kind, filename, resp) {
filename = dead_path(kind, filename);
var stream = fs.createReadStream(filename);
stream.once('error', function (err) {
if (err.code == 'ENOENT') {
resp.writeHead(404);
resp.end('Image not found');
}
else {
winston.error(err);
resp.end();
}
});
stream.once('open', function () {
var h = {
'Cache-Control': 'no-cache, no-store',
'Expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
};
try {
h['Content-Type'] = require('mime').lookup(filename);
} catch (e) {}
resp.writeHead(200, h);
stream.pipe(resp);
});
};
hooks.hook_sync('extractPost', function (post) {
if (!is_image(post))
return;
var image = {};
image_attrs.forEach(function (key) {
if (key in post) {
image[key] = post[key];
delete post[key];
}
});
if (image.dims.split)
image.dims = image.dims.split(',').map(parse_number);
image.size = parse_number(image.size);
delete image.hash;
post.image = image;
});
function parse_number(n) {
return parseInt(n, 10);
}
hooks.hook_sync('inlinePost', function (info) {
var post = info.dest, image = info.src.image;
if (!image)
return;
image_attrs.forEach(function (key) {
if (key in image)
post[key] = image[key];
});
});
function publish(alloc, cb) {
var mvs = [];
var haveComp = alloc.tmps.comp && alloc.image.realthumb;
for (var kind in alloc.tmps) {
var src = media_path('tmp', alloc.tmps[kind]);
// both comp and thumb go in thumb/
var destDir = (kind == 'comp') ? 'thumb' : kind;
// hack for stupid thumb/realthumb business
var destKey = kind;
if (haveComp) {
if (kind == 'thumb')
destKey = 'realthumb';
else if (kind == 'comp')
destKey = 'thumb';
}
var dest = media_path(destDir, alloc.image[destKey]);
mvs.push(etc.cpx.bind(etc, src, dest));
}
async.parallel(mvs, cb);
}
function validate_alloc(alloc) {
if (!alloc || !alloc.image || !alloc.tmps)
return;
for (var dir in alloc.tmps) {
var fnm = alloc.tmps[dir];
if (!/^[\w_]+$/.test(fnm)) {
winston.warn("Suspicious filename: "
+ JSON.stringify(fnm));
return;
}
}
return true;
}
hooks.hook("buryImage", function (info, callback) {
if (!info.src)
return callback(null);
/* Just in case */
var m = /^\d+\w*\.\w+$/;
if (!info.src.match(m))
return callback(etc.Muggle('Invalid image.'));
var mvs = [mv.bind(null, 'src', info.src)];
function try_thumb(path, t) {
if (!t)
return;
if (!t.match(m))
return callback(etc.Muggle('Invalid thumbnail.'));
mvs.push(mv.bind(null, path, t));
}
try_thumb('thumb', info.thumb);
try_thumb('thumb', info.realthumb);
try_thumb('mid', info.mid);
async.parallel(mvs, callback);
function mv(p, nm, cb) {
etc.movex(media_path(p, nm), dead_path(p, nm), cb);
}
});
function is_image(image) {
return image && (image.src || image.vint);
}
function media_path(dir, filename) {
return path.join(config.MEDIA_DIRS[dir], filename);
}
exports.media_path = media_path;
function dead_path(dir, filename) {
return path.join(config.MEDIA_DIRS.dead, dir, filename);
}
function make_dir(base, key, cb) {
var dir = base ? path.join(base, key) : config.MEDIA_DIRS[key];
etc.checked_mkdir(dir, cb);
}
exports._make_media_dir = make_dir;
exports.make_media_dirs = function (cb) {
var keys = ['src', 'thumb', 'vint', 'dead'];
if (!is_standalone())
keys.push('tmp');
if (config.EXTRA_MID_THUMBNAILS)
keys.push('mid');
async.forEach(keys, make_dir.bind(null, null), function (err) {
if (err)
return cb(err);
var dead = config.MEDIA_DIRS.dead;
var keys = ['src', 'thumb'];
if (config.EXTRA_MID_THUMBNAILS)
keys.push('mid');
async.forEach(keys, make_dir.bind(null, dead), cb);
});
}
exports.serve_image = function (req, resp) {
var m = /^\/(src|thumb|mid|vint)(\/\d+\.\w+)$/.exec(req.url);
if (!m)
return false;
var root = config.MEDIA_DIRS[m[1]];
if (!root)
return false;
require('send')(req, m[2], {root: root}).pipe(resp);
return true;
};
exports.squish_MD5 = function (hash) {
if (typeof hash == 'string')
hash = new Buffer(hash, 'hex');
return hash.toString('base64').replace(/\//g, '_').replace(/=*$/, '');
};
exports.obtain_image_alloc = function (id, cb) {
var onegai = new db.Onegai;
onegai.obtain_image_alloc(id, function (err, alloc) {
onegai.disconnect();
if (err)
return cb(err);
if (validate_alloc(alloc))
cb(null, alloc);
else
cb("Invalid image alloc");
});
};
exports.commit_image_alloc = function (alloc, cb) {
publish(alloc, function (err) {
if (err)
return cb(err);
var o = new db.Onegai;
o.commit_image_alloc(alloc, function (err) {
o.disconnect();
cb(err);
});
});
};
exports.note_hash = function (hash, num) {
if (!config.DUPLICATE_COOLDOWN)
return;
var key = 'hash:' + hash;
db.connect().setex(key, config.DUPLICATE_COOLDOWN, num, function (err) {
if (err)
winston.warn("note hash: " + err);
});
};
var is_standalone = exports.is_standalone = db.is_standalone;

@ -0,0 +1,85 @@
var events = require('events'),
util = require('util'),
winston = require('winston');
var JOB_LIMIT = 1;
var JOB_TIMEOUT = 30 * 1000;
var JOB_QUEUE = [];
var JOBS_RUNNING = 0;
function schedule(job, cb) {
if (job && job.jobRunning)
winston.warn("Job "+job.describe_job()+" already running!");
else if (job && JOB_QUEUE.indexOf(job) >= 0)
winston.warn("Job "+job.describe_job()+" already scheduled!");
else if (job) {
JOB_QUEUE.push(job);
if (cb) {
/* Sucks */
job.once('finish', cb);
job.once('timeout', cb.bind(null, "Timed out."));
}
}
while (JOB_QUEUE.length && JOBS_RUNNING < JOB_LIMIT)
JOB_QUEUE.shift().start_job();
}
exports.schedule = schedule;
function Job() {
events.EventEmitter.call(this);
}
util.inherits(Job, events.EventEmitter);
exports.Job = Job;
Job.prototype.start_job = function () {
if (this.jobRunning) {
winston.warn(this.describe_job() + " already started!");
return;
}
JOBS_RUNNING++;
this.jobRunning = true;
this.jobTimeout = setTimeout(this.timeout_job.bind(this), JOB_TIMEOUT);
setTimeout(this.perform_job.bind(this), 0);
};
Job.prototype.finish_job = function (p1, p2) {
if (!this.jobRunning) {
winston.warn("Attempted to finish stopped job: "
+ this.describe_job());
return;
}
clearTimeout(this.jobTimeout);
this.jobTimeout = 0;
this.jobRunning = false;
JOBS_RUNNING--;
if (JOBS_RUNNING < 0)
winston.warn("Negative job count: " + JOBS_RUNNING);
/* use `arguments` later */
this.emit('finish', p1, p2);
schedule(null);
};
Job.prototype.timeout_job = function () {
var desc = this.describe_job();
if (!this.jobRunning) {
winston.warn("Job " + desc + " timed out though finished?!");
return;
}
winston.error(desc + " timed out.");
this.jobTimeout = 0;
this.jobRunning = false;
JOBS_RUNNING--;
if (JOBS_RUNNING < 0)
winston.warn("Negative job count: " + JOBS_RUNNING);
this.emit('timeout');
schedule(null);
};
Job.prototype.describe_job = function () {
return "<anonymous job>";
};

@ -0,0 +1,39 @@
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
const char *filename;
FILE *f;
unsigned char buf[256], c, *p;
unsigned int i, mean;
int bit;
if (argc != 2) {
fprintf(stderr, "Usage: %s img.gray\n", argv[0]);
return -1;
}
filename = argv[1];
f = fopen(filename, "rb");
if(!f) {
perror(filename);
return -1;
}
if (fread(buf, sizeof buf, 1, f) != 1) {
perror(filename);
fclose(f);
return -1;
}
fclose(f);
mean = 0;
for (i = 0; i < sizeof buf; i++)
mean += buf[i];
mean /= sizeof buf;
p = buf;
for (i = 0; i < sizeof buf / 4; i++) {
c = 0;
for (bit = 3; bit >= 0; bit--)
c |= (*p++ > mean) << bit;
putchar("0123456789abcdef"[c]);
}
return 0;
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,42 @@
// Backbone.js 0.9.10
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
(function(){var n=this,B=n.Backbone,h=[],C=h.push,u=h.slice,D=h.splice,g;g="undefined"!==typeof exports?exports:n.Backbone={};g.VERSION="0.9.10";var f=n._;!f&&"undefined"!==typeof require&&(f=require("underscore"));g.$=n.jQuery||n.Zepto||n.ender;g.noConflict=function(){n.Backbone=B;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var v=/\s+/,q=function(a,b,c,d){if(!c)return!0;if("object"===typeof c)for(var e in c)a[b].apply(a,[e,c[e]].concat(d));else if(v.test(c)){c=c.split(v);e=0;for(var f=c.length;e<
f;e++)a[b].apply(a,[c[e]].concat(d))}else return!0},w=function(a,b){var c,d=-1,e=a.length;switch(b.length){case 0:for(;++d<e;)(c=a[d]).callback.call(c.ctx);break;case 1:for(;++d<e;)(c=a[d]).callback.call(c.ctx,b[0]);break;case 2:for(;++d<e;)(c=a[d]).callback.call(c.ctx,b[0],b[1]);break;case 3:for(;++d<e;)(c=a[d]).callback.call(c.ctx,b[0],b[1],b[2]);break;default:for(;++d<e;)(c=a[d]).callback.apply(c.ctx,b)}},h=g.Events={on:function(a,b,c){if(!q(this,"on",a,[b,c])||!b)return this;this._events||(this._events=
{});(this._events[a]||(this._events[a]=[])).push({callback:b,context:c,ctx:c||this});return this},once:function(a,b,c){if(!q(this,"once",a,[b,c])||!b)return this;var d=this,e=f.once(function(){d.off(a,e);b.apply(this,arguments)});e._callback=b;this.on(a,e,c);return this},off:function(a,b,c){var d,e,t,g,j,l,k,h;if(!this._events||!q(this,"off",a,[b,c]))return this;if(!a&&!b&&!c)return this._events={},this;g=a?[a]:f.keys(this._events);j=0;for(l=g.length;j<l;j++)if(a=g[j],d=this._events[a]){t=[];if(b||
c){k=0;for(h=d.length;k<h;k++)e=d[k],(b&&b!==e.callback&&b!==e.callback._callback||c&&c!==e.context)&&t.push(e)}this._events[a]=t}return this},trigger:function(a){if(!this._events)return this;var b=u.call(arguments,1);if(!q(this,"trigger",a,b))return this;var c=this._events[a],d=this._events.all;c&&w(c,b);d&&w(d,arguments);return this},listenTo:function(a,b,c){var d=this._listeners||(this._listeners={}),e=a._listenerId||(a._listenerId=f.uniqueId("l"));d[e]=a;a.on(b,"object"===typeof b?this:c,this);
return this},stopListening:function(a,b,c){var d=this._listeners;if(d){if(a)a.off(b,"object"===typeof b?this:c,this),!b&&!c&&delete d[a._listenerId];else{"object"===typeof b&&(c=this);for(var e in d)d[e].off(b,c,this);this._listeners={}}return this}}};h.bind=h.on;h.unbind=h.off;f.extend(g,h);var r=g.Model=function(a,b){var c,d=a||{};this.cid=f.uniqueId("c");this.attributes={};b&&b.collection&&(this.collection=b.collection);b&&b.parse&&(d=this.parse(d,b)||{});if(c=f.result(this,"defaults"))d=f.defaults({},
d,c);this.set(d,b);this.changed={};this.initialize.apply(this,arguments)};f.extend(r.prototype,h,{changed:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},sync:function(){return g.sync.apply(this,arguments)},get:function(a){return this.attributes[a]},escape:function(a){return f.escape(this.get(a))},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e,g,p,j,l,k;if(null==a)return this;"object"===typeof a?(e=a,c=b):(e={})[a]=b;c||(c={});
if(!this._validate(e,c))return!1;g=c.unset;p=c.silent;a=[];j=this._changing;this._changing=!0;j||(this._previousAttributes=f.clone(this.attributes),this.changed={});k=this.attributes;l=this._previousAttributes;this.idAttribute in e&&(this.id=e[this.idAttribute]);for(d in e)b=e[d],f.isEqual(k[d],b)||a.push(d),f.isEqual(l[d],b)?delete this.changed[d]:this.changed[d]=b,g?delete k[d]:k[d]=b;if(!p){a.length&&(this._pending=!0);b=0;for(d=a.length;b<d;b++)this.trigger("change:"+a[b],this,k[a[b]],c)}if(j)return this;
if(!p)for(;this._pending;)this._pending=!1,this.trigger("change",this,c);this._changing=this._pending=!1;return this},unset:function(a,b){return this.set(a,void 0,f.extend({},b,{unset:!0}))},clear:function(a){var b={},c;for(c in this.attributes)b[c]=void 0;return this.set(b,f.extend({},a,{unset:!0}))},hasChanged:function(a){return null==a?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._changing?
this._previousAttributes:this.attributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return null==a||!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=a.success;a.success=function(a,d,e){if(!a.set(a.parse(d,e),e))return!1;b&&b(a,d,e)};return this.sync("read",this,a)},save:function(a,b,c){var d,e,g=this.attributes;
null==a||"object"===typeof a?(d=a,c=b):(d={})[a]=b;if(d&&(!c||!c.wait)&&!this.set(d,c))return!1;c=f.extend({validate:!0},c);if(!this._validate(d,c))return!1;d&&c.wait&&(this.attributes=f.extend({},g,d));void 0===c.parse&&(c.parse=!0);e=c.success;c.success=function(a,b,c){a.attributes=g;var k=a.parse(b,c);c.wait&&(k=f.extend(d||{},k));if(f.isObject(k)&&!a.set(k,c))return!1;e&&e(a,b,c)};a=this.isNew()?"create":c.patch?"patch":"update";"patch"===a&&(c.attrs=d);a=this.sync(a,this,c);d&&c.wait&&(this.attributes=
g);return a},destroy:function(a){a=a?f.clone(a):{};var b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};a.success=function(a,b,e){(e.wait||a.isNew())&&d();c&&c(a,b,e)};if(this.isNew())return a.success(this,null,a),!1;var e=this.sync("delete",this,a);a.wait||d();return e},url:function(){var a=f.result(this,"urlRoot")||f.result(this.collection,"url")||x();return this.isNew()?a:a+("/"===a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},
isNew:function(){return null==this.id},isValid:function(a){return!this.validate||!this.validate(this.attributes,a)},_validate:function(a,b){if(!b.validate||!this.validate)return!0;a=f.extend({},this.attributes,a);var c=this.validationError=this.validate(a,b)||null;if(!c)return!0;this.trigger("invalid",this,c,b||{});return!1}});var s=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);void 0!==b.comparator&&(this.comparator=b.comparator);this.models=[];this._reset();this.initialize.apply(this,
arguments);a&&this.reset(a,f.extend({silent:!0},b))};f.extend(s.prototype,h,{model:r,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},sync:function(){return g.sync.apply(this,arguments)},add:function(a,b){a=f.isArray(a)?a.slice():[a];b||(b={});var c,d,e,g,p,j,l,k,h,m;l=[];k=b.at;h=this.comparator&&null==k&&!1!=b.sort;m=f.isString(this.comparator)?this.comparator:null;c=0;for(d=a.length;c<d;c++)(e=this._prepareModel(g=a[c],b))?(p=this.get(e))?b.merge&&(p.set(g===
e?e.attributes:g,b),h&&(!j&&p.hasChanged(m))&&(j=!0)):(l.push(e),e.on("all",this._onModelEvent,this),this._byId[e.cid]=e,null!=e.id&&(this._byId[e.id]=e)):this.trigger("invalid",this,g,b);l.length&&(h&&(j=!0),this.length+=l.length,null!=k?D.apply(this.models,[k,0].concat(l)):C.apply(this.models,l));j&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=l.length;c<d;c++)(e=l[c]).trigger("add",e,this,b);j&&this.trigger("sort",this,b);return this},remove:function(a,b){a=f.isArray(a)?a.slice():[a];
b||(b={});var c,d,e,g;c=0;for(d=a.length;c<d;c++)if(g=this.get(a[c]))delete this._byId[g.id],delete this._byId[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:this.length},b));return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},
b));return a},shift:function(a){var b=this.at(0);this.remove(b,a);return b},slice:function(a,b){return this.models.slice(a,b)},get:function(a){if(null!=a)return this._idAttr||(this._idAttr=this.model.prototype.idAttribute),this._byId[a.id||a.cid||a[this._idAttr]||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){if(!this.comparator)throw Error("Cannot sort a set without a comparator");
a||(a={});f.isString(this.comparator)||1===this.comparator.length?this.models=this.sortBy(this.comparator,this):this.models.sort(f.bind(this.comparator,this));a.silent||this.trigger("sort",this,a);return this},pluck:function(a){return f.invoke(this.models,"get",a)},update:function(a,b){b=f.extend({add:!0,merge:!0,remove:!0},b);b.parse&&(a=this.parse(a,b));var c,d,e,g,h=[],j=[],l={};f.isArray(a)||(a=a?[a]:[]);if(b.add&&!b.remove)return this.add(a,b);d=0;for(e=a.length;d<e;d++)c=a[d],g=this.get(c),
b.remove&&g&&(l[g.cid]=!0),(b.add&&!g||b.merge&&g)&&h.push(c);if(b.remove){d=0;for(e=this.models.length;d<e;d++)c=this.models[d],l[c.cid]||j.push(c)}j.length&&this.remove(j,b);h.length&&this.add(h,b);return this},reset:function(a,b){b||(b={});b.parse&&(a=this.parse(a,b));for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);b.previousModels=this.models.slice();this._reset();a&&this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=
a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=a.success;a.success=function(a,d,e){a[e.update?"update":"reset"](d,e);b&&b(a,d,e)};return this.sync("read",this,a)},create:function(a,b){b=b?f.clone(b):{};if(!(a=this._prepareModel(a,b)))return!1;b.wait||this.add(a,b);var c=this,d=b.success;b.success=function(a,b,f){f.wait&&c.add(a,f);d&&d(a,b,f)};a.save(null,b);return a},parse:function(a){return a},clone:function(){return new this.constructor(this.models)},_reset:function(){this.length=0;this.models.length=
0;this._byId={}},_prepareModel:function(a,b){if(a instanceof r)return a.collection||(a.collection=this),a;b||(b={});b.collection=this;var c=new this.model(a,b);return!c._validate(a,b)?!1:c},_removeReference:function(a){this===a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"===a||"remove"===a)&&c!==this||("destroy"===a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],null!=b.id&&(this._byId[b.id]=
b)),this.trigger.apply(this,arguments))},sortedIndex:function(a,b,c){b||(b=this.comparator);var d=f.isFunction(b)?b:function(a){return a.get(b)};return f.sortedIndex(this.models,a,d,c)}});f.each("forEach each map collect reduce foldl inject reduceRight foldr find detect filter select reject every all some any include contains invoke max min toArray size first head take initial rest tail drop last without indexOf shuffle lastIndexOf isEmpty chain".split(" "),function(a){s.prototype[a]=function(){var b=
u.call(arguments);b.unshift(this.models);return f[a].apply(f,b)}});f.each(["groupBy","countBy","sortBy"],function(a){s.prototype[a]=function(b,c){var d=f.isFunction(b)?b:function(a){return a.get(b)};return f[a](this.models,d,c)}});var y=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},E=/\((.*?)\)/g,F=/(\(\?)?:\w+/g,G=/\*\w+/g,H=/[\-{}\[\]+?.,\\\^$|#\s]/g;f.extend(y.prototype,h,{initialize:function(){},route:function(a,b,c){f.isRegExp(a)||
(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));this.trigger("route",b,d);g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b);return this},_bindRoutes:function(){if(this.routes)for(var a,b=f.keys(this.routes);null!=(a=b.pop());)this.route(a,this.routes[a])},_routeToRegExp:function(a){a=a.replace(H,"\\$&").replace(E,"(?:$1)?").replace(F,
function(a,c){return c?a:"([^/]+)"}).replace(G,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl");"undefined"!==typeof window&&(this.location=window.location,this.history=window.history)},z=/^[#\/]|\s+$/g,I=/^\/+|\/+$/g,J=/msie [\w.]+/,K=/\/$/;m.started=!1;f.extend(m.prototype,h,{interval:50,getHash:function(a){return(a=(a||this).location.href.match(/#(.*)$/))?a[1]:""},getFragment:function(a,
b){if(null==a)if(this._hasPushState||!this._wantsHashChange||b){a=this.location.pathname;var c=this.root.replace(K,"");a.indexOf(c)||(a=a.substr(c.length))}else a=this.getHash();return a.replace(z,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this.root=this.options.root;this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=!(!this.options.pushState||
!this.history||!this.history.pushState);a=this.getFragment();var b=document.documentMode,b=J.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b);this.root=("/"+this.root+"/").replace(I,"/");b&&this._wantsHashChange&&(this.iframe=g.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a));if(this._hasPushState)g.$(window).on("popstate",this.checkUrl);else if(this._wantsHashChange&&"onhashchange"in window&&!b)g.$(window).on("hashchange",this.checkUrl);
else this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,this.interval));this.fragment=a;a=this.location;b=a.pathname.replace(/[^\/]$/,"$&/")===this.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),this.location.replace(this.root+this.location.search+"#"+this.fragment),!0;this._wantsPushState&&(this._hasPushState&&b&&a.hash)&&(this.fragment=this.getHash().replace(z,""),this.history.replaceState({},document.title,
this.root+this.fragment+a.search));if(!this.options.silent)return this.loadUrl()},stop:function(){g.$(window).off("popstate",this.checkUrl).off("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a===this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a===this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},
loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};a=this.getFragment(a||"");if(this.fragment!==a){this.fragment=a;var c=this.root+a;if(this._hasPushState)this.history[b.replace?"replaceState":"pushState"]({},document.title,c);else if(this._wantsHashChange)this._updateHash(this.location,a,b.replace),this.iframe&&a!==this.getFragment(this.getHash(this.iframe))&&
(b.replace||this.iframe.document.open().close(),this._updateHash(this.iframe.location,a,b.replace));else return this.location.assign(c);b.trigger&&this.loadUrl(a)}},_updateHash:function(a,b,c){c?(c=a.href.replace(/(javascript:|#).*$/,""),a.replace(c+"#"+b)):a.hash="#"+b}});g.history=new m;var A=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},L=/^(\S+)\s*(.*)$/,M="model collection el id attributes className tagName events".split(" ");
f.extend(A.prototype,h,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();this.stopListening();return this},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof g.$?a:g.$(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=f.result(this,"events"))){this.undelegateEvents();for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);
if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(L),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);if(""===d)this.$el.on(e,c);else this.$el.on(e,d,c)}}},undelegateEvents:function(){this.$el.off(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},f.result(this,"options"),a));f.extend(this,f.pick(a,M));this.options=a},_ensureElement:function(){if(this.el)this.setElement(f.result(this,"el"),!1);else{var a=f.extend({},f.result(this,"attributes"));
this.id&&(a.id=f.result(this,"id"));this.className&&(a["class"]=f.result(this,"className"));a=g.$("<"+f.result(this,"tagName")+">").attr(a);this.setElement(a,!1)}}});var N={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=N[a];f.defaults(c||(c={}),{emulateHTTP:g.emulateHTTP,emulateJSON:g.emulateJSON});var e={type:d,dataType:"json"};c.url||(e.url=f.result(b,"url")||x());if(null==c.data&&b&&("create"===a||"update"===a||"patch"===a))e.contentType="application/json",
e.data=JSON.stringify(c.attrs||b.toJSON(c));c.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(c.emulateHTTP&&("PUT"===d||"DELETE"===d||"PATCH"===d)){e.type="POST";c.emulateJSON&&(e.data._method=d);var h=c.beforeSend;c.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d);if(h)return h.apply(this,arguments)}}"GET"!==e.type&&!c.emulateJSON&&(e.processData=!1);var m=c.success;c.success=function(a){m&&m(b,a,c);b.trigger("sync",b,a,c)};
var j=c.error;c.error=function(a){j&&j(b,a,c);b.trigger("error",b,a,c)};a=c.xhr=g.ajax(f.extend(e,c));b.trigger("request",b,a,c);return a};g.ajax=function(){return g.$.ajax.apply(g.$,arguments)};r.extend=s.extend=y.extend=A.extend=m.extend=function(a,b){var c=this,d;d=a&&f.has(a,"constructor")?a.constructor:function(){return c.apply(this,arguments)};f.extend(d,c,b);var e=function(){this.constructor=d};e.prototype=c.prototype;d.prototype=new e;a&&f.extend(d.prototype,a);d.__super__=c.prototype;return d};
var x=function(){throw Error('A "url" property or function must be specified');}}).call(this);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -0,0 +1,61 @@
/*jshint eqnull:true */
/*!
* jQuery Cookie Plugin v1.1
* https://github.com/carhartl/jquery-cookie
*
* Copyright 2011, Klaus Hartl
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://www.opensource.org/licenses/mit-license.php
* http://www.opensource.org/licenses/GPL-2.0
*/
(function($, document) {
var pluses = /\+/g;
function raw(s) {
return s;
}
function decoded(s) {
return decodeURIComponent(s.replace(pluses, ' '));
}
$.cookie = function(key, value, options) {
// key and at least value given, set cookie...
if (arguments.length > 1 && (!/Object/.test(Object.prototype.toString.call(value)) || value == null)) {
options = $.extend({}, $.cookie.defaults, options);
if (value == null) {
options.expires = -1;
}
if (typeof options.expires === 'number') {
var days = options.expires, t = options.expires = new Date();
t.setDate(t.getDate() + days);
}
value = String(value);
return (document.cookie = [
encodeURIComponent(key), '=', options.raw ? value : encodeURIComponent(value),
options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
options.path ? '; path=' + options.path : '',
options.domain ? '; domain=' + options.domain : '',
options.secure ? '; secure' : ''
].join(''));
}
// key and possibly options given, get cookie...
options = value || $.cookie.defaults || {};
var decode = options.raw ? raw : decoded;
var cookies = document.cookie.split('; ');
for (var i = 0, parts; (parts = cookies[i] && cookies[i].split('=')); i++) {
if (decode(parts.shift()) === key) {
return decode(parts.join('='));
}
}
return null;
};
$.cookie.defaults = {};
})(jQuery, document);

@ -0,0 +1,7 @@
(function($,document){var pluses=/\+/g;function raw(s){return s;}
function decoded(s){return decodeURIComponent(s.replace(pluses,' '));}
$.cookie=function(key,value,options){if(arguments.length>1&&(!/Object/.test(Object.prototype.toString.call(value))||value==null)){options=$.extend({},$.cookie.defaults,options);if(value==null){options.expires=-1;}
if(typeof options.expires==='number'){var days=options.expires,t=options.expires=new Date();t.setDate(t.getDate()+days);}
value=String(value);return(document.cookie=[encodeURIComponent(key),'=',options.raw?value:encodeURIComponent(value),options.expires?'; expires='+options.expires.toUTCString():'',options.path?'; path='+options.path:'',options.domain?'; domain='+options.domain:'',options.secure?'; secure':''].join(''));}
options=value||$.cookie.defaults||{};var decode=options.raw?raw:decoded;var cookies=document.cookie.split('; ');for(var i=0,parts;(parts=cookies[i]&&cookies[i].split('='));i++){if(decode(parts.shift())===key){return decode(parts.join('='));}}
return null;};$.cookie.defaults={};})(jQuery,document);

@ -0,0 +1,117 @@
/*
jQuery `input` special event v1.1
<a class="linkclass" href="http://whattheheadsaid.com/projects/input-special-event">http://whattheheadsaid.com/projects/input-special-event</a>
(c) 2010-2011 Andy Earnshaw
MIT license
<a class="linkclass" href="http://www.opensource.org/licenses/mit-license.php">www.opensource.org/licenses/mit-license.php</a>
*/
(function($, udf) {
var ns = ".inputEvent ",
// A bunch of data strings that we use regularly
dataBnd = "bound.inputEvent",
dataVal = "value.inputEvent",
dataDlg = "delegated.inputEvent",
txtinput = "txtinput",
// Set up our list of events
bindTo = [
"input", "textInput", "propertychange", "paste", "cut", "keydown", "drop",
""].join(ns),
// Events required for delegate, mostly for IE support
dlgtTo = [ "focusin", "mouseover", "dragstart", "" ].join(ns),
// Elements supporting text input, not including contentEditable
supported = {TEXTAREA:udf, INPUT:udf},
// Events that fire before input value is updated
delay = { paste:udf, cut:udf, keydown:udf, drop:udf, textInput:udf };
$.event.special.txtinput = {
setup: function(data, namespaces, handler) {
var timer,
bndCount,
// Get references to the element
elem = this,
$elem = $(this),
triggered = false;
if (elem.tagName in supported) {
bndCount = $.data(elem, dataBnd) || 0;
if (!bndCount)
$elem.bind(bindTo, handler);
$.data(elem, dataBnd, ++bndCount);
$.data(elem, dataVal, elem.value);
} else {
$elem.bind(dlgtTo, function (e) {
var target = e.target;
if (target.tagName in supported && !$.data(elem, dataDlg)) {
bndCount = $.data(target, dataBnd) || 0;
if (!bndCount)
target.bind(bindTo, handler);
// make sure we increase the count only once for each bound ancestor
$.data(elem, dataDlg, true);
$.data(target, dataBnd, ++bndCount);
$.data(target, dataVal, target.value);
}
});
}
function handler (e) {
var elem = e.target;
// Clear previous timers because we only need to know about 1 change
window.clearTimeout(timer), timer = null;
// Return if we've already triggered the event
if (triggered)
return;
// paste, cut, keydown and drop all fire before the value is updated
if (e.type in delay && !timer) {
// ...so we need to delay them until after the event has fired
timer = window.setTimeout(function () {
if (elem.value !== $.data(elem, dataVal)) {
$(elem).trigger(txtinput);
$.data(elem, dataVal, elem.value);
}
}, 0);
}
else if (e.type == "propertychange") {
if (e.originalEvent.propertyName == "value") {
$(elem).trigger(txtinput);
$.data(elem, dataVal, elem.value);
triggered = true;
window.setTimeout(function () {
triggered = false;
}, 0);
}
}
else {
$(elem).trigger(txtinput);
$.data(elem, dataVal, elem.value);
triggered = true;
window.setTimeout(function () {
triggered = false;
}, 0);
}
}
},
teardown: function () {
var elem = $(this);
elem.unbind(dlgtTo);
elem.find("input, textarea").andSelf().each(function () {
bndCount = $.data(this, dataBnd, ($.data(this, dataBnd) || 1)-1);
if (!bndCount)
elem.unbind(bindTo);
});
}
};
// Setup our jQuery shorthand method
$.fn.input = function (handler) {
return handler ? this.bind(txtinput, handler) : this.trigger(txtinput);
}
})(jQuery);

@ -0,0 +1 @@
(function(a,d){var e="bound.inputEvent",f="value.inputEvent",k="delegated.inputEvent",g="txtinput",n="input,textInput,propertychange,paste,cut,keydown,drop,".split(",").join(".inputEvent "),p=["focusin","mouseover","dragstart",""].join(".inputEvent "),q={TEXTAREA:d,INPUT:d},r={paste:d,cut:d,keydown:d,drop:d,textInput:d};a.event.special.txtinput={setup:function(h,d,o){function o(b){var c=b.target;window.clearTimeout(l);l=null;j||(b.type in r&&!l?l=window.setTimeout(function(){c.value!==a.data(c,f)&&(a(c).trigger(g),a.data(c,f,c.value))},0):"propertychange"==b.type?"value"==b.originalEvent.propertyName&&(a(c).trigger(g),a.data(c,f,c.value),j=!0,window.setTimeout(function(){j=!1},0)):(a(c).trigger(g),a.data(c,f,c.value),j=!0,window.setTimeout(function(){j=!1},0)))}var l,m,i=this,h=a(this),j=!1;i.tagName in q?((m=a.data(i,e)||0)||h.bind(n,o),a.data(i,e,++m),a.data(i,f,i.value)):h.bind(p,function(b){b=b.target;b.tagName in q&&!a.data(i,k)&&((m=a.data(b,e)||0)||b.bind(n,o),a.data(i,k,!0),a.data(b,e,++m),a.data(b,f,b.value))})},teardown:function(){var h=a(this);h.unbind(p);h.find("input, textarea").andSelf().each(function(){(bndCount=a.data(this,e,(a.data(this,e)||1)-1))||h.unbind(n)})}};a.fn.input=function(a){return a?this.bind(g,a):this.trigger(g)}})(jQuery);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -0,0 +1,11 @@
local key = KEYS[1]
local body_key = KEYS[2]
local liveposts = KEYS[3]
local body = redis.call('get', body_key)
if body then
redis.call('hset', key, 'body', body)
redis.call('del', body_key)
end
redis.call('hdel', key, 'state')
redis.call('srem', liveposts, key)

@ -0,0 +1,61 @@
local function read_post(key, body_key)
local post = redis.call('hgetall', key) -- flat bulk reply, not hash
if post and redis.call('hexists', key, 'body') == 0 then
local body = redis.call('get', body_key)
if body then
post[#post+1] = 'body'
post[#post+1] = body
end
end
return post
end
local abbrev = tonumber(ARGV[1])
local thread_key = KEYS[1]
local thread_body_key = KEYS[2]
local thread_posts_key = KEYS[3]
local liveposts_key = KEYS[4]
-- first, read the 'pre'liminary-thread, raw and without replies
local pre = read_post(thread_key, thread_body_key)
if not pre then
return false
end
-- build the global set of live posts
-- okay if I make thread:#:posts a set, could skip this step and use `sinter`
-- except that liveposts contains keys, not numbers, argh!
local liveposts = {}
for _, key in ipairs(redis.call('smembers', liveposts_key)) do
local num = key:match('^post:(%d+)$') -- do not convert to integer!
if num then
liveposts[num] = true
end
end
local start = 0
local total = 0
if abbrev > 0 then
start = -abbrev
total = total + redis.call('llen', thread_posts_key)
end
-- request the list of replies
local replies = redis.call('lrange', thread_posts_key, start, -1) or {}
-- fully fetch all currently-editing replies
-- (we will fetch finished replies later since they aren't time-critical)
--
-- pretty sure this breaks EVAL rules (we use keys not passed in KEYS[])
-- but hey, if we ever use redis cluster, we could use `post:n{op}` keys
local active = {}
for _, id in ipairs(replies) do
if liveposts[id] then
local key = 'post:' .. id
local post = read_post(key, key..':body')
active[#active+1] = id
active[#active+1] = post
end
end
return {pre, replies, active, total}

@ -0,0 +1,162 @@
var async = require('async'),
child_process = require('child_process'),
config = require('./config'),
fs = require('fs'),
imagerConfig = require('./imager/config'),
reportConfig = require('./report/config'),
streamBuffers = require('stream-buffers'),
util = require('util');
function make_client(inputs, out, cb) {
var defines = {};
for (var k in config)
defines[k] = JSON.stringify(config[k]);
for (var k in imagerConfig)
defines[k] = JSON.stringify(imagerConfig[k]);
for (var k in reportConfig)
defines[k] = JSON.stringify(reportConfig[k]);
// UGH
var configDictLookup = {
config: config,
imagerConfig: imagerConfig,
reportConfig: reportConfig,
};
function lookup_config(dictName, key) {
var dict = configDictLookup[dictName];
if (key.indexOf('SECURE') >= 0 || key.indexOf('PRIVATE') >= 0)
throw new Error("Refusing " + key + " in client code!");
return dict[key];
}
var config_re = /\b(\w+onfig)\.(\w+)\b/;
function convert(file, cb) {
if (/^lib\//.test(file))
return cb("lib/* should be in VENDOR_DEPS");
if (/^config\.js/.test(file))
return cb("config.js shouldn't be in client");
fs.readFile(file, 'UTF-8', function (err, fullFile) {
if (err)
return cb(err);
var lines = fullFile.split('\n');
var waitForDrain = false;
for (var j = 0; j < lines.length; j++) {
var line = lines[j];
if (/^var\s+DEFINES\s*=\s*exports\s*;\s*$/.test(line))
continue;
if (/^var\s+(\w+onfig|common|_)\s*=\s*require.*$/.test(line))
continue;
m = line.match(/^DEFINES\.(\w+)\s*=\s*(.+);$/);
if (m) {
defines[m[1]] = m[2];
continue;
}
m = line.match(/^exports\.(\w+)\s*=\s*(\w+)\s*;\s*$/);
if (m && m[1] == m[2])
continue;
m = line.match(/^exports\.(\w+)\s*=\s*(.*)$/);
if (m)
line = 'var ' + m[1] + ' = ' + m[2];
// XXX: risky
line = line.replace(/\bcommon\.\b/g, '');
while (true) {
var m = line.match(config_re);
if (!m)
break;
var cfg = lookup_config(m[1], m[2]);
if (cfg === undefined) {
return cb("No such "+m[1]+" var "+m[2]);
}
// Bleh
if (cfg instanceof RegExp)
cfg = cfg.toString();
else
cfg = JSON.stringify(cfg);
line = line.replace(config_re, cfg);
}
for (var src in defines) {
if (line.indexOf(src) < 0)
continue;
var regexp = new RegExp('(?:DEFINES\.)?\\b' + src
+ '\\b', 'g');
line = line.replace(regexp, defines[src]);
}
waitForDrain = !out.write(line+'\n', 'UTF-8');
}
if (waitForDrain)
out.once('drain', function () { cb(null); });
else
cb(null);
}); // readFile
}
// kick off
async.eachSeries(inputs, convert, cb);
};
exports.make_client = make_client;
function make_minified(files, out, cb) {
var buf = new streamBuffers.WritableStreamBuffer();
buf.once('error', cb);
make_client(files, buf, function (err) {
if (err)
return cb(err);
var src = buf.getContentsAsString('utf-8');
if (!src || !src.length)
return cb('make_minified: no client JS was generated');
minify(src);
});
function minify(src) {
var UglifyJS = require('uglify-es');
var ugly;
try {
ugly = UglifyJS.minify(src, {
mangle: false,
});
}
catch (e) {
return cb(e);
}
out.write(ugly.code, cb);
}
};
exports.make_minified = make_minified;
function make_maybe_minified(files, out, cb) {
if (config.DEBUG)
make_client(files, out, cb);
else
make_minified(files, out, cb);
}
exports.make_maybe_minified = make_maybe_minified;
if (require.main === module) {
var files = [];
for (var i = 2; i < process.argv.length; i++) {
var arg = process.argv[i];
if (arg[0] != '-') {
files.push(arg);
continue;
}
else {
util.error('Unrecognized option ' + arg);
process.exit(1);
}
}
make_maybe_minified(files, process.stdout, function (err) {
if (err) throw err;
});
}

@ -0,0 +1,43 @@
{
"name": "doushio",
"version": "0.3.1",
"description": "Real-time imageboard",
"keywords": [
"realtime",
"imageboard",
"redis"
],
"homepage": "https://github.com/lalcmellkmal/doushio",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/lalcmellkmal/doushio.git"
},
"scripts": {
"postinstall": "make",
"start": "node server/server.js"
},
"dependencies": {
"async": "2.1.4",
"chart.js": "^2.7.2",
"formidable": "1.0.17",
"jsoncompress": "^0.1.3",
"minimist": "1.2.0",
"nan": "2.5.0",
"recaptcha2": "^1.3.2",
"redis": "2.6.3",
"request": "2.79",
"sockjs": "0.3.18",
"stream-buffers": "^3.0.1",
"tmp": "0.0.31",
"uglify-es": "^3.3.9",
"uglify-js": "2.7.5",
"winston": "2.3.0"
},
"optionalDependencies": {
"node-expat": "2.3.15",
"nodemailer": "2.7.0",
"nodemailer-smtp-transport": "2.7.0",
"send": "0.14.1"
}
}

@ -0,0 +1,164 @@
var async = require('async'),
config = require('./config'),
crypto = require('crypto'),
etc = require('./etc'),
fs = require('fs'),
make_client = require('./make_client').make_maybe_minified,
pathJoin = require('path').join,
stream = require('stream'),
tmp_file = require('tmp').file,
util = require('util');
const PUBLIC_JS = pathJoin('www', 'js');
function HashingStream(out) {
stream.Writable.call(this);
this._hash = crypto.createHash('MD5');
this._outStream = out;
}
util.inherits(HashingStream, stream.Writable);
HashingStream.prototype._write = function (chunk, encoding, cb) {
this._hash.update(chunk);
this._outStream.write(chunk, encoding, cb);
};
HashingStream.prototype.end = function (cb) {
if (arguments.length > 1)
throw new Error("TODO multi-arg HashingStream.end");
var self = this;
stream.Writable.prototype.end.call(this, function () {
self._outStream.end(function () {
if (cb)
cb();
});
});
};
function end_and_move_js(stream, dir, prefix, cb) {
stream.end(function () {
var fnm;
if (config.DEBUG) {
fnm = prefix + '-debug.js';
}
else {
var hash = stream._hash.digest('hex').slice(0, 10);
fnm = prefix + '-' + hash + '.min.js';
}
var tmp = stream._tmpFilename;
etc.move(tmp, pathJoin(dir, fnm), function (err) {
if (err)
return cb(err);
cb(null, fnm);
});
});
};
function make_hashing_stream(cb) {
// ideally the stream would be returned immediately and handle
// this step internally...
var opts = {dir: '.build', postfix: '.gen.js', mode: 0644};
tmp_file(opts, function (err, tmp, fd) {
if (err)
return cb(err);
var out = fs.createWriteStream(null, {fd: fd});
out.once('error', cb);
if (config.DEBUG) {
out._tmpFilename = tmp;
cb(null, out);
}
else {
var stream = new HashingStream(out);
stream._tmpFilename = tmp;
cb(null, stream);
}
});
}
function build_vendor_js(cb) {
var deps = require('./deps');
make_hashing_stream(function (err, stream) {
if (err)
return cb(err);
async.eachSeries(deps.VENDOR_DEPS, function (file, cb) {
fs.readFile(file, function (err, buf) {
if (err)
return cb(err);
stream.write(buf, cb);
});
}, function (err) {
if (err)
return cb(err);
end_and_move_js(stream, PUBLIC_JS, 'vendor', cb);
});
});
}
function build_client_js(cb) {
var deps = require('./deps');
make_hashing_stream(function (err, stream) {
if (err)
return cb(err);
make_client(deps.CLIENT_DEPS, stream, function (err) {
if (err)
return cb(err);
end_and_move_js(stream, PUBLIC_JS, 'client', cb);
});
});
}
function build_mod_client_js(cb) {
var deps = require('./deps');
make_hashing_stream(function (err, stream) {
if (err)
return cb(err);
make_client(deps.MOD_CLIENT_DEPS, stream, function (err) {
if (err)
return cb(err);
end_and_move_js(stream, 'state', 'mod', cb);
});
});
}
function commit_assets(metadata, cb) {
tmp_file({dir: '.build', postfix: '.json'}, function (err, tmp, fd) {
if (err)
return cb(err);
var stream = fs.createWriteStream(null, {fd: fd});
stream.once('error', cb);
stream.end(JSON.stringify(metadata) + '\n', function () {
etc.move(tmp, pathJoin('state', 'scripts.json'), cb);
});
});
}
function rebuild(cb) {
etc.checked_mkdir('state', function (err) {
etc.checked_mkdir('.build', function (err) {
if (err) return cb(err);
async.parallel({
vendor: build_vendor_js,
client: build_client_js,
mod: build_mod_client_js,
}, function (err, hashes) {
if (err)
return cb(err);
commit_assets(hashes, cb);
});
});
});
}
exports.rebuild = rebuild;
exports.refresh_deps = function () {
delete require.cache[pathJoin(__dirname, 'deps.js')];
};
if (require.main === module) {
rebuild(function (err) {
if (err) throw err;
});
}

@ -0,0 +1,248 @@
(function () {
var siteKey = reportConfig.RECAPTCHA_SITE_KEY;
var REPORTS = {};
var PANEL;
var CAPTCHA_CTR = 0;
if (siteKey)
menuOptions.push('Report');
var Report = Backbone.Model.extend({
defaults: {
status: 'setup',
hideAfter: true,
},
request_new: function () {
this.set({
status: 'setup',
error: ''
});
if (this.get('captchaId')) {
this.reset();
return;
}
// inject a new div into captchaHolder
let id = 'captcha_' + CAPTCHA_CTR++;
this.trigger('createDiv', id);
// render a new captcha on it
let params = {
sitekey: siteKey,
theme: 'dark',
callback: response => {
this.set({
status: 'ready',
error: '',
response: response
});
},
'expired-callback': () => {
this.set({
status: 'error',
error: 'reCAPTCHA expired.'
});
},
'error-callback': () => {
this.set({
status: 'error',
error: 'reCAPTCHA error.'
});
}
};
setTimeout(() => {
debugger;
window.grecaptcha.render(id, params);
}, 10);
},
did_report: function () {
delete REPORTS[this.id];
setTimeout(() => this.trigger('destroy'), 1500);
if (this.get('hideAfter'))
this.get('post').set('hide', true);
},
reset: function () {
let captchaId = this.get('captchaId');
if (captchaId) {
grecaptcha.reset(captchaId);
this.set('captchaId', null);
}
},
});
var ReportPanel = Backbone.View.extend({
id: 'report-panel',
tagName: 'form',
className: 'modal',
events: {
submit: 'submit',
'click .close': 'remove',
'click .hideAfter': 'hide_after_changed',
},
initialize: function () {
this.$captchaHolder = $('<div>', {
id: 'captcha',
css: {'min-width': 304, 'min-height': 78}
});
this.$message = $('<div class="message"/>');
this.$submit = $('<input>', {type: 'submit', val: 'Report'});
var $hideAfter = $('<input>', {
'class': 'hideAfter',
type: 'checkbox',
checked: this.model.get('hideAfter'),
});
var $hideLabel = $('<label>and hide</label>')
.append($hideAfter);
var num = this.model.id;
this.$el
.append('Reporting post ')
.append($('<a/>', {href: '#'+num, text: '>>'+num}))
.append('<a class="close" href="#">x</a>')
.append(this.$message)
.append(this.$captchaHolder)
.append(this.$submit)
.append(' ', $hideLabel);
/* HACK */
if (window.x_csrf) {
this.model.set('hideAfter', false);
$hideLabel.remove();
}
this.listenTo(this.model, {
'change:error': this.error_changed,
'change:status': this.status_changed,
createDiv: this.create_div,
destroy: this.remove,
});
},
render: function () {
this.error_changed();
this.status_changed();
return this;
},
create_div: function (id) {
this.$captchaHolder.empty().append($('<div/>', {id: id}));
},
submit: function (event) {
event.preventDefault();
let status = this.model.get('status');
if (status == 'ready' && this.model.get('response')) {
send([REPORT_POST, this.model.id, this.model.get('response')]);
this.model.set({
status: 'reporting',
response: null
});
}
else if (status == 'error') {
this.model.request_new();
}
},
error_changed: function () {
this.$message.text(this.model.get('error'));
},
status_changed: function () {
var status = this.model.get('status');
let submit = 'Report';
if (status == 'reporting')
submit = 'Reporting...';
if (status == 'error')
submit = 'Another.';
this.$submit
.prop('disabled', status != 'ready' && status != 'error')
.toggle(status != 'done')
.val(submit);
if (status == 'done')
this.$('label').remove();
var msg;
if (status == 'done')
msg = 'Report submitted!';
else if (status == 'setup')
msg = 'Obtaining reCAPTCHA...';
else if (status == 'error')
msg = 'E';
else if (status == 'ready' && this.model.get('error'))
msg = 'E';
this.$message.text(msg=='E' ? this.model.get('error') : msg);
this.$message.toggleClass('error', msg == 'E');
// not strictly view logic, but only relevant when visible
if (status == 'done')
this.model.did_report();
},
hide_after_changed: function (e) {
this.model.set('hideAfter', e.target.checked);
},
remove: function () {
this.model.reset();
Backbone.View.prototype.remove.call(this);
if (PANEL == this) {
PANEL = null;
}
return false;
},
});
var ajaxJs = 'https://www.google.com/recaptcha/api.js?onload=on_init_captcha&render=explicit';
var CAPTCHA_LOADED = false;
window.on_init_captcha = () => { CAPTCHA_LOADED = true; };
menuHandlers.Report = function (post) {
var num = post.id;
var model = REPORTS[num];
if (!model)
REPORTS[num] = model = new Report({id: num, post: post});
if (PANEL) {
if (PANEL.model === model) {
PANEL.focus();
return;
}
PANEL.remove();
}
PANEL = new ReportPanel({model: model});
PANEL.render().$el.appendTo('body');
if (CAPTCHA_LOADED) {
model.request_new();
return;
}
$.getScript(ajaxJs, () => {
// why is `grecaptcha` not immediately available?
setTimeout(() => {
if (CAPTCHA_LOADED)
model.request_new();
else
model.set({
status: 'error',
error: "Couldn't load reCATPCHA.",
});
}, 10);
});
};
dispatcher[REPORT_POST] = function (msg, op) {
var num = msg[0], etc = msg[1];
var report = REPORTS[num];
if (report)
report.set(msg[1] || {status: 'done'});
};
})();

@ -0,0 +1,17 @@
module.exports = {
MAIL_FROM: "Reports <reports@doushio.com>",
MAIL_TO: ['lalc@doushio.com'],
MAIL_THREAD_URL_BASE: 'http://your.board.index/',
MAIL_MEDIA_URL: 'http://your.board.index/media/',
SMTP: {
service: 'Gmail',
auth: {
user: "reports@doushio.com",
pass: "",
},
},
RECAPTCHA_SITE_KEY: '',
RECAPTCHA_SECRET_KEY: '',
};

@ -0,0 +1,183 @@
var caps = require('../server/caps'),
config = require('./config'),
common = require('../common'),
db = require('../db'),
mainConfig = require('../config'),
msgcheck = require('../server/msgcheck'),
nodemailer = require('nodemailer'),
okyaku = require('../server/okyaku'),
Recaptcha2 = require('recaptcha2'),
smtpTransport = require('nodemailer-smtp-transport'),
winston = require('winston');
var SMTP = nodemailer.createTransport(smtpTransport(config.SMTP));
var VALIDATOR;
if (!!config.RECAPTCHA_SITE_KEY) {
VALIDATOR = new Recaptcha2({
siteKey: config.RECAPTCHA_SITE_KEY,
secretKey: config.RECAPTCHA_SECRET_KEY,
});
exports.enabled = true;
}
var safe = common.safe;
function report(reporter_ident, op, num, cb) {
var board = caps.can_access_thread(reporter_ident, op);
if (!board)
return cb("Post does not exist.");
var reporter = maybe_mnemonic(reporter_ident.ip) || '???';
var yaku = new db.Yakusoku(board, {auth: 'Moderator'});
var reader = new db.Reader(yaku);
var kind = op == num ? 'thread' : 'post';
reader.get_posts(kind, [num], {}, function (err, posts) {
if (err || !posts[0]) {
if (err)
console.error(err);
send_report(reporter, board, op, num, '', [], cb);
return;
}
var post = posts[0];
var name = (post.name || common.ANON)
if (name.length > 23)
name = name.slice(0, 20) + '...';
if (post.trip)
name += ' # ' + post.trip;
if (post.ip)
name += ' # ' + maybe_mnemonic(post.ip);
var body = 'Offender: ' + name;
var html = ['Offender: ', safe('<b>'), name, safe('</b>')];
var img;
if (post.image && !post.hideimg)
img = image_preview(post.image);
if (img) {
body += '\nThumbnail: ' + img.src;
html.push(safe('<br><br><img src="'), img.src,
safe('" width="'), img.width,
safe('" height="'), img.height,
safe('" title="'), img.title, safe('">'));
}
send_report(reporter, board, op, num, body, html, cb);
});
}
function send_report(reporter, board, op, num, body, html, cb) {
var noun;
var url = config.MAIL_THREAD_URL_BASE + board + '/' + op + '?reported';
if (op == num) {
noun = 'Thread';
}
else {
noun = 'Post';
url += '#' + num;
}
body = body ? (body + '\n\n' + url) : url;
if (html.length)
html.push(safe('<br><br>'));
html.push(safe('<a href="'), url, safe('">'), '>>'+num, safe('</a>'));
var opts = {
from: config.MAIL_FROM,
to: config.MAIL_TO.join(', '),
subject: noun + ' #' + num + ' reported by ' + reporter,
text: body,
html: common.flatten(html).join(''),
};
SMTP.sendMail(opts, function (err, resp) {
if (err)
return cb(err);
cb(null);
});
}
function image_preview(info) {
if (!info.dims)
return;
var tw = info.dims[2], th = info.dims[3];
if (info.mid) {
tw *= 2;
th *= 2;
}
if (!tw || !th) {
tw = info.dims[0];
th = info.dims[1];
}
if (!tw || !th)
return;
var mediaURL = config.MAIL_MEDIA_URL;
if (!mediaURL)
mediaURL = require('../imager/config').MEDIA_URL;
var src;
if (info.mid)
src = mediaURL + '/mid/' + info.mid;
else if (info.realthumb || info.thumb)
src = mediaURL + '/thumb/' + (info.realthumb || info.thumb);
else
return;
var title = common.readable_filesize(info.size);
return {src: src, width: tw, height: th, title: title};
}
function maybe_mnemonic(ip) {
if (ip && mainConfig.IP_MNEMONIC) {
var authcommon = require('../admin/common');
ip = authcommon.ip_mnemonic(ip);
}
return ip;
}
okyaku.dispatcher[common.REPORT_POST] = function (msg, client) {
if (!msgcheck.check(['id', 'string'], msg))
return false;
var num = msg[0];
var op = db.OPs[num];
if (!op || !caps.can_access_thread(client.ident, op))
return reply_error("Post does not exist.");
const response = msg[1];
if (!response)
return reply_error("Pretty please?");
if (response.length > 10000)
return reply_error("tl;dr");
VALIDATOR.validate(response, client.ident.ip).then(function () {
var op = db.OPs[num];
if (!op)
return reply_error("Post does not exist.");
report(client.ident, op, num, function (err) {
if (err) {
winston.error(err);
return reply_error("Couldn't send report.");
}
// success!
client.send([op, common.REPORT_POST, num]);
});
}, function (err) {
let readable = VALIDATOR.translateErrors(err);
if (Array.isArray(readable))
readable = readable.join('; ');
reply_error(readable);
});
return true;
function reply_error(err) {
if (!err)
err = 'Unknown reCAPTCHA error.';
var op = db.OPs[num] || 0;
var msg = {status: 'error', error: err};
client.send([op, common.REPORT_POST, num, msg]);
return true;
}
};

@ -0,0 +1,200 @@
var common = require('../common'),
hooks = require('../hooks'),
config = require('../config'),
diskspace = require('diskspace');
var pyu=config.PYU_START;
var rollLimit = 5;
var ds = -1;
var working = false;
function _cds()
{
diskspace.check(config.MOUNTPOINT, function(e, result) {
if(result)
ds = Math.round((result.used / result.total) * 100);
else
ds = -1;
working=false;
});
}
const loli = require("hashloli");
var lolis = [];
var reqd = [];
loli.defaultConfig.number=3;
loli.defaultConfig.tags = ["-rape"];
loli.defaultConfig.page = 4000;
loli.defaultConfig.range = 20;
const maxLolis = 10;
const maxLolisLong = 100;
function getLoli() {
if(lolis[0])
var ret = lolis.pop();
if(!ret || lolis.length<maxLolis)
loli.randomise(function(datas) {
if(!datas) return;
datas.map(function(vv) {
let v = vv.file_url;
if(v && !reqd.includes(v)) {
lolis.push({file: v, preview: vv.preview_url});
reqd.push(v);
} else if(reqd[0]) reqd.pop();
while(reqd.length >= maxLolisLong)
reqd.pop();
});
// console.log("AAAA "+JSON.stringify(lolis));
});
return ret;
}
getLoli();
//_cds();
exports.roll_dice = function (frag, post, extra) {
var ms = frag.split(common.dice_re);
var dice = [];
for (var i = 1; i < ms.length && dice.length < rollLimit; i += 2) {
if (ms[i] == '#loli') {
let cute = getLoli();
if(cute)
dice.push([cute.file, cute.preview]);
else
dice.push(["/404", "/s/error.jpg"]);
}
else if(ms[i] == '#?')
{
if(!post.body) dice.push([-1]);
else {
var options=[];
var sbody = post.body.split("\n");
for(var j=sbody.length-2;j>=0;j--)
{
var cur = sbody[j].trim();
if(cur.length<1 || /#\?$/.test(cur)) continue;
if(/\?$/.test(cur)) options.push(cur.slice(0,-1));
else break;
}
var f = options.length;
if(options.length<1) dice.push([0]);
else dice.push([options[Math.floor(Math.random() * f)]]);
}
}
else if(ms[i] == '#du')
{
working = true;
if( dice.length + (post.dice ? post.dice.length : 0) < rollLimit)
_cds();
else working = false;
// while(working) ; //SPIN SPIN SPINNN~~~~
dice.push([ds]);
}
else if(ms[i] == '#pyu') {
if( dice.length + (post.dice ? post.dice.length : 0) < rollLimit)
pyu += 1;
dice.push([pyu]);
}
else if(ms[i] == '#pcount') {
dice.push([pyu]);
}
else {
var info = common.parse_dice(ms[i]);
if (!info)
continue;
var f = info.faces;
var rolls = [f];
for (var j = 0; j < info.n; j++)
rolls.push(Math.floor(Math.random() * f) + 1);
if (info.bias)
rolls.push({bias: info.bias})
dice.push(rolls);
}
}
if (dice.length) {
// Would prefer an appending scheme for adding new rolls but
// there's no hash value append redis command...
// I don't want to spill into a separate redis list.
// Overwriting the whole log every time is quadratic though.
// Enforcing a roll limit to deter that and for sanity
var exist = post.dice ? post.dice.length : 0;
if (dice.length + exist > rollLimit)
dice = dice.slice(0, Math.max(0, rollLimit - exist));
if (dice.length) {
extra.new_dice = dice;
dice = post.dice ? post.dice.concat(dice) : dice;
post.dice = dice;
}
}
};
function inline_dice(post, dice) {
if (dice && dice.length) {
dice = JSON.stringify(dice);
post.dice = dice.substring(1, dice.length - 1);
}
}
hooks.hook('attachToPost', function (attached, cb) {
var new_dice = attached.extra.new_dice;
if (new_dice) {
attached.attach.dice = new_dice;
inline_dice(attached.writeKeys, attached.post.dice);
}
cb(null);
});
hooks.hook_sync('inlinePost', function (info) {
inline_dice(info.dest, info.src.dice);
});
hooks.hook_sync('extractPost', function (post) {
if (!post.dice)
return;
try {
post.dice = JSON.parse('[' + post.dice + ']');
}
catch (e) {
delete post.dice;
}
});
// This is looking rather boilerplatey
hooks.hook('clientSynced', function (info, cb) {
var op = info.op, client = info.client;
if (op) {
client.db.get_fun(op, function (err, js) {
if (err)
return cb(err);
if (js)
client.send([op, common.EXECUTE_JS, js]);
cb(null);
});
}
else
cb(null);
});
hooks.hook('clientSynced', function (info, cb) {
var client = info.client;
client.db.get_banner(function (err, banner) {
if (err)
return cb(err);
if (!banner)
return cb(null);
var msg = banner.message;
if (msg)
client.send([banner.op, common.UPDATE_BANNER, msg]);
cb(null);
});
});

@ -0,0 +1,258 @@
var _ = require('../lib/underscore'),
common = require('../common'),
config = require('../config'),
crypto = require('crypto'),
formidable = require('formidable'),
querystring = require('querystring'),
RES = require('./state').resources,
request = require('request'),
winston = require('winston');
function connect() {
return global.redis;
}
exports.login = function (req, resp) {
var ip = req.ident.ip;
// if login cookie present, redirect to board (preferably, go back. but will that be easy?)
var r = connect();
function fail(error) {
respond_error(resp, error)
}
if (req.query.state) {
var state = req.query.state;
r.get('github:'+state, function (err, savedIP) {
if (err) {
winston.error("Couldn't read login: " + err);
fail("Couldn't read login attempt.");
return;
}
if (!savedIP) {
winston.info("Expired login attempt from " + ip);
fail("Login attempt expired. Please try again.");
return;
}
if (savedIP != ip) {
winston.warn("IP changed from " + savedIP + " to " + ip);
fail("Your IP changed during login. Please try again.");
return;
}
if (req.query.error == 'access_denied') {
fail("User did not approve GitHub app access.");
return;
}
if (req.query.error) {
// escaping out of paranoia (though respond_error emits JSON)
var err = common.escape_html(req.query.error);
winston.error("OAuth error: " + err);
if (req.query.error_description) {
err = common.escape_html(req.query.error_description);
winston.error("Desc: " + err);
}
fail("OAuth login failure: " + err);
return;
}
var code = req.query.code;
if (!code) {
fail("OAuth code missing!");
return;
}
request_access_token(req.query.code, state, function (err, token) {
if (err) {
winston.error("Github access token: " + err);
fail("Couldn't obtain token from GitHub. Try again.");
return;
}
request_username(token, function (err, username) {
if (err) {
winston.error("Username: " + err);
fail("Couldn't read username. Try again.");
return;
}
r.del('github:'+state, function (err) {});
if (/^popup:/.test(state))
req.popup_HACK = true;
verify_auth(req, resp, username);
});
});
});
return;
}
// new login attempt; TODO rate limit
var nonce = random_str();
if (req.query.popup !== undefined)
nonce = 'popup:' + nonce;
r.setex('github:'+nonce, 60, ip, function (err) {
if (err) {
winston.error("Couldn't save login: " + err);
fail("Couldn't persist login attempt.");
return;
}
var params = {
client_id: config.GITHUB_CLIENT_ID,
state: nonce,
allow_signup: 'false',
};
var url = 'https://github.com/login/oauth/authorize?' +
querystring.stringify(params);
resp.writeHead(303, {Location: url});
resp.end('Redirect to GitHub Login');
});
}
function request_access_token(code, state, cb) {
var payload = {
client_id: config.GITHUB_CLIENT_ID,
client_secret: config.GITHUB_CLIENT_SECRET,
code: code,
state: state,
};
var opts = {
url: 'https://github.com/login/oauth/access_token',
body: payload,
json: true,
};
request.post(opts, function (err, tokenResp, packet) {
if (err || !packet || typeof packet.access_token != 'string') {
cb(err || "No access token in response");
}
else {
cb(null, packet.access_token);
}
});
}
function request_username(token, cb) {
var opts = {
url: 'https://api.github.com/user',
headers: {Authorization: 'token ' + token, 'User-Agent': 'Doushio-Auth'},
json: true,
};
request.get(opts, function (err, usernameResp, packet) {
if (err || !packet || typeof packet.login != 'string') {
cb(err || "Invalid username response");
}
else {
cb(null, packet.login);
}
});
}
function verify_auth(req, resp, user) {
if (!user)
return respond_error(resp, 'Invalid username.');
var ip = req.ident.ip;
var packet = {ip: ip, user: user, date: Date.now()};
if (config.ADMIN_GITHUBS.indexOf(user) >= 0) {
winston.info("@" + user + " logging in as admin from " + ip);
packet.auth = 'Admin';
exports.set_cookie(req, resp, packet);
}
else if (config.MODERATOR_GITHUBS.indexOf(user) >= 0) {
winston.info("@" + user + " logging in as moderator from " + ip);
packet.auth = 'Moderator';
exports.set_cookie(req, resp, packet);
}
else {
winston.error("Login attempt by @" + user + " from " + ip);
return respond_error(resp, 'Check your privilege.');
}
};
exports.set_cookie = function (req, resp, info) {
var pass = random_str();
info.csrf = random_str();
var m = connect().multi();
m.hmset('session:'+pass, info);
m.expire('session:'+pass, config.LOGIN_SESSION_TIME);
m.exec(function (err) {
if (err)
return oauth_error(resp, err);
respond_ok(req, resp, make_cookie('a', pass, info.expires));
});
};
function extract_login_cookie(chunks) {
if (!chunks || !chunks.a)
return false;
return /^[a-zA-Z0-9+\/]{20}$/.test(chunks.a) ? chunks.a : false;
}
exports.extract_login_cookie = extract_login_cookie;
exports.check_cookie = function (cookie, callback) {
var r = connect();
r.hgetall('session:' + cookie, function (err, session) {
if (err)
return callback(err);
else if (_.isEmpty(session))
return callback('Not logged in.');
callback(null, session);
});
};
exports.logout = function (req, resp) {
if (req.method != 'POST') {
resp.writeHead(200, {'Content-Type': 'text/html'});
resp.end('<!doctype html><title>Logout</title><form method=post>' +
'<input type=submit value=Logout></form>');
return;
}
var r = connect();
var cookie = extract_login_cookie(req.cookies);
if (!cookie) {
console.log('no cookie');
return respond_error(resp, "No login cookie for logout.");
}
r.hgetall('session:' + cookie, function (err, session) {
if (err)
return respond_error(resp, "Logout error.");
r.del('session:' + cookie);
respond_ok(req, resp, 'a=; expires=Thu, 01 Jan 1970 00:00:00 GMT');
});
};
function respond_error(resp, message) {
resp.writeHead(200, {'Content-Type': 'application/json'});
resp.end(JSON.stringify({status: 'error', message: message}));
}
function respond_ok(req, resp, cookie) {
var headers = {'Set-Cookie': cookie};
if (/json/.test(req.headers.accept)) {
headers['Content-Type'] = 'application/json';
resp.writeHead(200, headers);
resp.end(JSON.stringify({status: 'okay'}));
}
else if (req.popup_HACK) {
headers['Content-Type'] = 'text/html';
resp.writeHead(200, headers);
resp.end('<!doctype html><title>OK</title>Logged in!' +
'<script>window.opener.location.reload(); window.close();</script>');
}
else {
headers.Location = config.DEFAULT_BOARD + '/';
resp.writeHead(303, headers);
resp.end("OK! Redirecting.");
}
}
function make_expiry() {
var expiry = new Date(Date.now()
+ config.LOGIN_SESSION_TIME*1000).toUTCString();
/* Change it to the expected dash-separated format */
var m = expiry.match(/^(\w+,\s+\d+)\s+(\w+)\s+(\d+\s+[\d:]+\s+\w+)$/);
return m ? m[1] + '-' + m[2] + '-' + m[3] : expiry;
}
function make_cookie(key, val) {
var header = key + '=' + val + '; Expires=' + make_expiry();
var domain = config.LOGIN_COOKIE_DOMAIN;
if (domain)
header += '; Domain=' + domain;
return header;
}
function random_str() {
return crypto.randomBytes(15).toString('base64');
}

@ -0,0 +1,220 @@
var async = require('async'),
authcommon = require('../admin/common'),
check = require('./msgcheck').check,
common = require('../common'),
config = require('../config'),
db = require('../db'),
hooks = require('../hooks');
var RANGES = require('./state').dbCache.ranges;
function can_access_board(ident, board) {
if (board == 'graveyard' && can_administrate(ident))
return true;
if (board == config.STAFF_BOARD && !can_moderate(ident))
return false;
if (ident.ban || ident.suspension)
return false;
if (!temporal_access_check(ident, board))
return false;
return db.is_board(board);
}
exports.can_access_board = can_access_board;
exports.can_access_thread = function (ident, op) {
var tags = db.tags_of(op);
if (!tags)
return false;
for (var i = 0; i < tags.length; i++)
if (can_access_board(ident, tags[i]))
return tags[i];
return false;
};
function temporal_access_check(ident, board) {
var info = {ident: ident, board: board, access: true};
hooks.trigger_sync('temporalAccessCheck', info);
return info.access;
}
exports.temporal_access_check = temporal_access_check;
exports.can_ever_access_board = function (ident, board) {
if (can_access_board(ident, board))
return true;
if (!temporal_access_check(ident, board))
return true;
return false;
};
function can_moderate(ident) {
return (ident.auth === 'Admin' || ident.auth === 'Moderator');
}
exports.can_moderate = can_moderate;
function can_administrate(ident) {
return ident.auth === 'Admin';
}
exports.can_administrate = can_administrate;
function denote_priv(info) {
if (info.data.priv)
info.header.push(' (priv)');
}
function dead_media_paths(paths) {
paths.src = '../dead/src/';
paths.thumb = '../dead/thumb/';
paths.mid = '../dead/mid/';
}
exports.augment_oneesama = function (oneeSama, opts) {
var ident = opts.ident;
oneeSama.ident = ident;
if (can_moderate(ident))
oneeSama.hook('headerName', authcommon.append_mnemonic);
if (can_administrate(ident)) {
oneeSama.hook('headerName', denote_priv);
oneeSama.hook('headerName', authcommon.denote_hidden);
}
if (can_administrate(ident) && opts.board == 'graveyard')
oneeSama.hook('mediaPaths', dead_media_paths);
};
exports.mod_handler = function (func) {
return function (nums, client) {
if (!can_moderate(client.ident))
return false;
var opts = nums.shift();
if (!check({when: 'string'}, opts) || !check('id...', nums))
return false;
if (!(opts.when in authcommon.delayDurations))
return false;
var delay = authcommon.delayDurations[opts.when];
if (!delay)
func(nums, client);
else
setTimeout(func.bind(null, nums, client), delay*1000);
return true;
};
};
function parse_ip(ip) {
var m = ip.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)(?:\/(\d+))?$/);
if (!m)
return false;
// damn you signed int32s!
var num = 0;
for (var i = 4, shift = 1; i > 0; i--) {
num += parseInt(m[i], 10) * shift;
shift *= 256;
}
var info = {full: ip, num: num};
if (m[5]) {
var bits = parseInt(m[5], 10);
if (bits > 0 && bits <= 32) {
info.mask = 0x100000000 - Math.pow(2, 32 - bits);
info.num &= info.mask;
}
}
return info;
}
function parse_ranges(ranges) {
if (!ranges)
return [];
ranges = ranges.map(function (o) {
if (typeof o == 'object') {
o.ip = parse_ip(o.ip);
return o;
}
else
return {ip: parse_ip(o)};
});
ranges.sort(function (a, b) { return a.ip.num - b.ip.num; });
return ranges;
}
function range_lookup(ranges, num) {
if (!ranges)
return null;
/* Ideally would have a tree lookup here or something */
var result = null;
for (var i = 0; i < ranges.length; i++) {
var box = ranges[i].ip;
/* sint32 issue here doesn't matter for realistic ranges */
if ((box.mask ? (num & box.mask) : num) === box.num)
result = ranges[i];
/* don't break out of loop */
}
return result;
}
hooks.hook('reloadHot', function (hot, cb) {
var r = global.redis;
async.forEach(authcommon.suspensionKeys, function (key, cb) {
global.redis.smembers('hot:' + key, function (err, ranges) {
if (err)
return cb(err);
if (key == 'suspensions')
ranges = parse_suspensions(ranges);
var up = key.toUpperCase();
hot[up] = (hot[up] || []).concat(ranges || []);
RANGES[key] = parse_ranges(hot[up]);
cb(null);
});
}, cb);
});
function parse_suspensions(suspensions) {
if (!suspensions)
return [];
var parsed = [];
suspensions.forEach(function (s) {
try {
parsed.push(JSON.parse(s));
}
catch (e) {
winston.error("Bad suspension JSON: " + s);
}
});
return parsed;
}
exports.lookup_ident = function (ip, country) {
var ident = {ip, country, readOnly: config.READ_ONLY};
if (country
&& config.RESTRICTED_COUNTRIES
&& config.RESTRICTED_COUNTRIES.indexOf(country) >= 0) {
ident.readOnly = true;
}
var num = parse_ip(ip).num;
var ban = range_lookup(RANGES.bans, num);
if (ban) {
ident.ban = ban.ip.full;
return ident;
}
ban = range_lookup(RANGES.timeouts, num);
if (ban) {
ident.ban = ban.ip.full;
ident.timeout = true;
return ident;
}
var suspension = range_lookup(RANGES.suspensions, num);
if (suspension) {
ident.suspension = suspension;
return ident;
}
var priv = range_lookup(RANGES.boxes, num);
if (priv)
ident.priv = priv.ip.full;
var slow = range_lookup(RANGES.slows, num);
if (slow)
ident.slow = slow;
return ident;
};

@ -0,0 +1,30 @@
#!/usr/bin/env node
var config = require('../config'),
opts = require('./opts'),
path = require('path');
opts.parse_args();
opts.load_defaults();
var lock = config.PID_FILE;
var cfg = config.DAEMON;
if (cfg) {
require('daemon').kill(lock, function (err) {
if (err)
throw err;
});
}
else {
/* non-daemon version for hot reloads */
require('fs').readFile(lock, function (err, pid) {
pid = parseInt(pid, 10);
if (err || !pid)
return console.warn('No pid.');
require('child_process').exec('kill -HUP ' + pid,
function (err) {
if (err) throw err;
if (process.argv.indexOf('--silent') < 2)
console.log('Sent HUP.');
});
});
}

@ -0,0 +1,65 @@
function check(schema, msg) {
/* Primitives */
if (schema === 'id' || schema === 'nat')
return typeof msg == 'number' && (msg || msg === 0) &&
msg >= (schema == 'id' ? 1 : 0) &&
Math.round(msg) === msg;
else if (schema === 'string')
return typeof msg == 'string';
else if (schema === 'boolean')
return typeof msg == 'boolean';
/* Arrays */
if (schema instanceof Array) {
if (!(msg instanceof Array) || msg.length != schema.length)
return false;
for (var i = 0; i < schema.length; i++)
if (!check(schema[i], msg[i]))
return false;
return true;
}
else if (schema === 'id...') {
if (!(msg instanceof Array) || !msg.length)
return false;
return msg.every(check.bind(null, 'id'));
}
else if (msg instanceof Array)
return false;
/* Hashes */
if (typeof schema == 'object') {
if (typeof msg != 'object' || msg === null || msg instanceof Array)
return false;
for (var k in schema) {
var spec = schema[k];
/* optional key */
if (typeof spec == 'string' && /^opt /.test(spec)) {
if (!(k in msg))
continue;
spec = spec.slice(4);
}
else if (!(k in msg))
return false; /* otherwise mandatory */
if (!check(spec, msg[k]))
return false;
}
return true;
}
else if (schema === 'id=>nat') {
if (typeof msg != 'object' || msg instanceof Array)
return false;
for (var k in msg) {
if (!/^[1-9]\d*$/.test(k))
return false;
if (!check('nat', msg[k]))
return false;
}
return true;
}
throw new Error("Unknown schema: " + schema);
}
exports.check = check;

@ -0,0 +1,177 @@
var caps = require('./caps'),
common = require('../common'),
events = require('events'),
Muggle = require('../etc').Muggle,
STATE = require('./state'),
util = require('util'),
winston = require('winston');
var dispatcher = exports.dispatcher = {};
function Okyaku(socket, ip, country) {
events.EventEmitter.call(this);
this.socket = socket;
this.ident = caps.lookup_ident(ip, country);
this.watching = {};
this.ip = ip;
this.country = country;
var clients = STATE.clientsByIP[ip];
if (clients)
clients.push(this);
else
clients = STATE.clientsByIP[ip] = [this];
STATE.emitter.emit('change:clientsByIP', ip, clients);
}
util.inherits(Okyaku, events.EventEmitter);
exports.Okyaku = Okyaku;
var OK = Okyaku.prototype;
OK.send = function (msg) {
this.socket.write(JSON.stringify([msg]));
};
OK.on_update = function (op, kind, msg) {
// Special cases for operations that overwrite a client's state
if (this.post && kind == common.DELETE_POSTS) {
var nums = JSON.parse(msg)[0].slice(2);
if (nums.indexOf(this.post.num) >= 0)
this.post = null;
}
else if (this.post && kind == common.DELETE_THREAD) {
if (this.post.num == op || this.post.op == op)
this.post = null;
}
if (this.blackhole && HOLED_UPDATES.indexOf(kind) >= 0)
return;
this.socket.write(msg);
};
const HOLED_UPDATES = [common.DELETE_POSTS, common.DELETE_THREAD];
OK.on_thread_sink = function (thread, err) {
/* TODO */
winston.error(thread + ' sank: ' + err);
};
const WORMHOLES = [common.SYNCHRONIZE, common.FINISH_POST];
OK.on_message = function (data) {
var msg;
try { msg = JSON.parse(data); }
catch (e) {}
var type = common.INVALID;
if (msg) {
if (this.post && typeof msg == 'string')
type = common.UPDATE_POST;
else if (msg.constructor == Array)
type = msg.shift();
}
if (!this.synced && type != common.SYNCHRONIZE)
type = common.INVALID;
if (this.blackhole && WORMHOLES.indexOf(type) < 0)
return;
var func = dispatcher[type];
if (!func || !func(msg, this)) {
this.kotowaru(Muggle("Bad protocol.", new Error(
"Invalid message: " + JSON.stringify(data))));
}
};
var ip_expiries = {};
OK.on_close = function () {
var ip = this.ip;
var clientList = STATE.clientsByIP[ip];
if (clientList) {
var i = clientList.indexOf(this);
if (i >= 0) {
clientList.splice(i, 1);
STATE.emitter.emit('change:clientsByIP',ip,clientList);
}
if (!clientList.length) {
// Expire this list after a short delay
if (ip_expiries[ip])
clearTimeout(ip_expiries[ip]);
ip_expiries[ip] = setTimeout(function () {
var list = STATE.clientsByIP[ip];
if (list && list.length === 0)
delete STATE.clientsByIP[ip];
delete ip_expiries[ip];
}, 5000);
}
}
if (this.id) {
delete STATE.clients[this.id];
this.id = null;
}
this.synced = false;
var db = this.db;
if (db) {
db.kikanai();
if (this.post)
this.finish_post(function (err) {
if (err)
winston.warn('finishing post: ' + err);
db.disconnect();
});
else
db.disconnect();
}
this.emit('close');
};
OK.kotowaru = function (error) {
if (this.blackhole)
return;
var msg = 'Server error.';
if (error instanceof Muggle) {
msg = error.most_precise_error_message();
error = error.deepest_reason();
}
winston.error('Error by ' + JSON.stringify(this.ident) + ': '
+ (error || msg));
this.send([0, common.INVALID, msg]);
this.synced = false;
};
OK.finish_post = function (callback) {
/* TODO: Should we check this.uploading? */
var self = this;
this.db.finish_post(this.post, function (err) {
if (err)
callback(err);
else {
if (self.post) {
self.last_num = self.post.num;
self.post = null;
}
callback(null);
}
});
};
exports.scan_client_caps = function () {
for (var ip in STATE.clientsByIP) {
STATE.clientsByIP[ip].forEach(function (okyaku) {
if (!okyaku.id || !okyaku.board)
return;
var ident = caps.lookup_ident(ip, okyaku.country);
if (ident.timeout) {
okyaku.blackhole = true;
return;
}
if (!caps.can_access_board(ident, okyaku.board)) {
try {
okyaku.socket.close();
}
catch (e) { /* bleh */ }
}
});
}
};

@ -0,0 +1,33 @@
var config = require('../config'),
minimist = require('minimist'),
path = require('path');
function usage() {
process.stderr.write(
"Usage: node server/server.js\n"
+ " --host <host> --port <port>\n"
+ " --pid <pid file location>\n"
+ "\n"
+ "<port> can also be a unix domain socket path.\n"
);
process.exit(1);
}
exports.parse_args = function () {
var argv = minimist(process.argv.slice(2));
if ('h' in argv || 'help' in argv)
return usage();
if (argv.port)
config.LISTEN_PORT = argv.port;
if (argv.host)
config.LISTEN_HOST = argv.host;
if (argv.pid)
config.PID_FILE = argv.pid;
};
exports.load_defaults = function () {
if (!config.PID_FILE)
config.PID_FILE = path.join(__dirname, '.server.pid');
};

@ -0,0 +1,223 @@
var caps = require('./caps'),
common = require('../common'),
config = require('../config'),
db = require('../db'),
imager = require('../imager'),
STATE = require('./state'),
web = require('./web');
var RES = STATE.resources;
var escape = common.escape_html;
function tamashii(num) {
var op = db.OPs[num];
if (op && caps.can_access_thread(this.ident, op))
this.callback(this.post_ref(num, op));
else
this.callback('>>' + num);
}
exports.write_thread_html = function (reader, req, out, opts) {
var oneeSama = new common.OneeSama(tamashii);
oneeSama.tz_offset = parse_timezone(req.cookies.timezone);
opts.ident = req.ident;
caps.augment_oneesama(oneeSama, opts);
var cookies = web.parse_cookie(req.headers.cookie);
if (common.thumbStyles.indexOf(cookies.thumb) >= 0)
oneeSama.thumbStyle = cookies.thumb;
var lastN = cookies.lastn && parseInt(cookies.lastn, 10);
if (!lastN || !common.reasonable_last_n(lastN))
lastN = config.THREAD_LAST_N;
oneeSama.lastN = lastN;
var hidden = {};
if (cookies.hide && !caps.can_moderate(req.ident)) {
cookies.hide.slice(0, 200).split(',').forEach(function (num) {
num = parseInt(num, 10);
if (num)
hidden[num] = null;
});
}
var write_see_all_link;
reader.on('thread', function (op_post, omit, image_omit) {
if (op_post.num in hidden)
return;
op_post.omit = omit;
var full = oneeSama.full = !!opts.fullPosts;
oneeSama.op = opts.fullLinks ? false : op_post.num;
var first = oneeSama.monomono(op_post, full && 'full');
first.pop();
out.write(first.join(''));
write_see_all_link = omit && function (first_reply_num) {
var o = common.abbrev_msg(omit, image_omit);
if (opts.loadAllPostsLink) {
var url = '' + op_post.num;
if (first_reply_num)
url += '#' + first_reply_num;
o += ' '+common.action_link_html(url,
'See all');
}
out.write('\t<span class="omit">'+o+'</span>\n');
};
reader.once('endthread', close_section);
});
reader.on('post', function (post) {
if (post.num in hidden || post.op in hidden)
return;
if (write_see_all_link) {
write_see_all_link(post.num);
write_see_all_link = null;
}
out.write(oneeSama.mono(post));
});
function close_section() {
out.write('</section><hr>\n');
}
};
function make_link_rels(board, bits) {
var path = imager.config.MEDIA_URL + 'css/';
var base = 'base.css?v=' + STATE.hot.BASE_CSS_VERSION;
bits.push(['stylesheet', path + base]);
var theme = STATE.hot.BOARD_CSS[board];
var theme_css = theme + '.css?v=' + STATE.hot.THEME_CSS_VERSION;
bits.push(['stylesheet', path + theme_css, 'theme']);
bits.push(['stylesheet', path + 'gravitas.css?v=1']);
return bits.map(function (p) {
var html = '\t<link rel="'+p[0]+'" href="'+p[1]+'"';
if (p[2])
html += ' id="' + p[2] + '"';
return html + '>\n';
}).join('');
}
exports.write_board_head = function (out, initScript, board, nav) {
var indexTmpl = RES.indexTmpl;
var title = STATE.hot.TITLES[board] || escape(board);
var metaDesc = "Real-time imageboard";
var i = 0;
out.write(indexTmpl[i++]);
out.write(title);
out.write(indexTmpl[i++]);
out.write(escape(metaDesc));
out.write(indexTmpl[i++]);
out.write(make_board_meta(board, nav));
out.write(initScript);
out.write(indexTmpl[i++]);
if (RES.navigationHtml)
out.write(RES.navigationHtml);
out.write(indexTmpl[i++]);
out.write(title);
out.write(indexTmpl[i++]);
};
exports.write_thread_head = function (out, initScript, board, op, opts) {
var indexTmpl = RES.indexTmpl;
var title = '/'+escape(board)+'/ - ';
if (opts.subject)
title += escape(opts.subject) + ' (#' + op + ')';
else
title += '#' + op;
var metaDesc = "Real-time imageboard thread";
var i = 0;
out.write(indexTmpl[i++]);
out.write(title);
out.write(indexTmpl[i++]);
out.write(escape(metaDesc));
out.write(indexTmpl[i++]);
out.write(make_thread_meta(board, op, opts.abbrev));
out.write(initScript);
out.write(indexTmpl[i++]);
if (RES.navigationHtml)
out.write(RES.navigationHtml);
out.write(indexTmpl[i++]);
out.write('Thread #' + op);
out.write(indexTmpl[i++]);
var buttons = bottomHTML + ' ' + personaHTML;
out.write(buttons + '\n<hr>\n');
};
function make_board_meta(board, info) {
var bits = [];
if (info.cur_page >= 0)
bits.push(['index', '.']);
if (info.prev_page)
bits.push(['prev', info.prev_page]);
if (info.next_page)
bits.push(['next', info.next_page]);
return make_link_rels(board, bits);
}
function make_thread_meta(board, num, abbrev) {
var bits = [['index', '.']];
if (abbrev)
bits.push(['canonical', num]);
return make_link_rels(board, bits);
}
exports.make_pagination_html = function (info) {
var bits = ['<nav class="pagination">'], cur = info.cur_page;
if (cur >= 0)
bits.push('<a href=".">live</a>');
else
bits.push('<strong>live</strong>');
var start = 0, end = info.pages, step = 1;
if (info.ascending) {
start = end - 1;
end = step = -1;
}
for (var i = start; i != end; i += step) {
if (i != cur)
bits.push('<a href="page' + i + '">' + i + '</a>');
else
bits.push('<strong>' + i + '</strong>');
}
if (info.next_page)
bits.push(' <input type="button" value="Next"> ');
bits.push('<a id="persona" href="#persona">ID</a></nav>');
return bits.join('');
};
var returnHTML = common.action_link_html('.', 'Return').replace(
'span', 'span id="bottom"');
var bottomHTML = common.action_link_html('#bottom', 'Bottom');
var personaHTML = common.action_link_html('#persona', 'Identity', 'persona');
exports.write_page_end = function (out, ident, returnLink) {
if (returnLink)
out.write(returnHTML);
else if (RES.navigationHtml)
out.write('<br><br>' + RES.navigationHtml);
var last = RES.indexTmpl.length - 1;
out.write(RES.indexTmpl[last]);
if (ident) {
if (caps.can_administrate(ident))
out.write('<script src="../admin.js"></script>\n');
else if (caps.can_moderate(ident))
out.write('<script src="../mod.js"></script>\n');
}
};
function parse_timezone(tz) {
if (!tz && tz != 0)
return null;
tz = parseInt(tz, 10);
if (isNaN(tz) || tz < -24 || tz > 24)
return null;
return tz;
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,210 @@
var _ = require('../lib/underscore'),
async = require('async'),
config = require('../config'),
crypto = require('crypto'),
fs = require('fs'),
hooks = require('../hooks'),
path = require('path'),
pipeline = require('../pipeline'),
vm = require('vm');
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
exports.emitter = new (require('events').EventEmitter);
exports.dbCache = {
OPs: {},
opTags: {},
threadSubs: {},
YAKUMAN: 0,
funThread: 0,
addresses: {},
ranges: {},
connTokenSecretKey: null,
};
var HOT = exports.hot = {};
var RES = exports.resources = {};
exports.clients = {};
exports.clientsByIP = {};
function reload_hot_config(cb) {
fs.readFile('hot.js', 'UTF-8', function (err, js) {
if (err)
cb(err);
var hot = {};
try {
vm.runInNewContext(js, hot);
}
catch (e) {
return cb(e);
}
if (!hot || !hot.hot)
return cb('Bad hot config.');
// Overwrite the original object just in case
Object.keys(HOT).forEach(function (k) {
delete HOT[k];
});
_.extend(HOT, hot.hot);
read_exits('exits.txt', function () {
hooks.trigger('reloadHot', HOT, cb);
});
});
}
// load the encryption key for connToken
hooks.hook('reloadHot', function (hot, cb) {
var r = global.redis;
var key = 'ctoken-secret-key';
r.get(key, function (err, secretHex) {
if (err) return cb(err);
if (secretHex) {
var secretBytes = new Buffer(secretHex, 'hex');
if (secretBytes.length != 32)
return cb('ctoken secret key is invalid');
HOT.connTokenSecretKey = secretBytes;
return cb(null);
}
// generate a new one
var secretKey = crypto.randomBytes(32);
r.setnx(key, secretKey.toString('hex'), function (err, wasSet) {
if (err) return cb(err);
if (wasSet)
HOT.connTokenSecretKey = secretKey;
else
assert(!!HOT.connTokenSecretKey);
cb(null);
});
});
});
function reload_scripts(cb) {
var json = path.join('state', 'scripts.json');
fs.readFile(json, 'UTF-8', function (err, json) {
if (err)
cb(err);
var js;
try {
js = JSON.parse(json);
}
catch (e) {
return cb(e);
}
if (!js || !js.vendor || !js.client)
return cb('Bad state/scripts.json.');
HOT.VENDOR_JS = js.vendor;
HOT.CLIENT_JS = js.client;
var modJs = path.join('state', js.mod);
fs.readFile(modJs, 'UTF-8', function (err, modSrc) {
if (err)
return cb(err);
RES.modJs = modSrc;
cb(null);
});
});
}
function reload_resources(cb) {
var deps = require('../deps');
read_templates(function (err, tmpls) {
if (err)
return cb(err);
_.extend(RES, expand_templates(tmpls));
hooks.trigger('reloadResources', RES, cb);
});
}
function read_templates(cb) {
function read(dir, file) {
return fs.readFile.bind(fs, path.join(dir, file), 'UTF-8');
}
async.parallel({
index: read('tmpl', 'index.html'),
filter: read('tmpl', 'filter.html'),
curfew: read('tmpl', 'curfew.html'),
suspension: read('tmpl', 'suspension.html'),
notFound: read('www', '404.html'),
serverError: read('www', '50x.html'),
}, cb);
}
function expand_templates(res) {
var templateVars = _.clone(HOT);
_.extend(templateVars, require('../imager/config'));
_.extend(templateVars, config);
function tmpl(data) {
var expanded = _.template(data, templateVars);
return {tmpl: expanded.split(/\$[A-Z]+/),
src: expanded};
}
var ex = {
navigationHtml: make_navigation_html(),
filterTmpl: tmpl(res.filter).tmpl,
curfewTmpl: tmpl(res.curfew).tmpl,
suspensionTmpl: tmpl(res.suspension).tmpl,
notFoundHtml: res.notFound,
serverErrorHtml: res.serverError,
};
var index = tmpl(res.index);
ex.indexTmpl = index.tmpl;
var hash = crypto.createHash('md5').update(index.src);
ex.indexHash = hash.digest('hex').slice(0, 8);
return ex;
}
exports.reload_hot_resources = function (cb) {
pipeline.refresh_deps();
async.series([
reload_hot_config,
pipeline.rebuild,
reload_scripts,
reload_resources,
], cb);
}
function make_navigation_html() {
if (!HOT.INTER_BOARD_NAVIGATION)
return '';
var bits = ['<nav>['];
config.BOARDS.forEach(function (board, i) {
if (board == config.STAFF_BOARD)
return;
if (i > 0)
bits.push(' / ');
bits.push('<a href="../'+board+'/">'+board+'</a>');
});
bits.push(']</nav>');
return bits.join('');
}
function read_exits(file, cb) {
fs.readFile(file, 'UTF-8', function (err, lines) {
if (err)
return cb(err);
var exits = [], dest = HOT.BANS;
lines.split(/\n/g).forEach(function (line) {
var m = line.match(/^(?:^#\d)*(\d+\.\d+\.\d+\.\d+)/);
if (!m)
return;
var exit = m[1];
if (dest.indexOf(exit) < 0)
dest.push(exit);
});
cb(null);
});
}

@ -0,0 +1,581 @@
var _ = require('../lib/underscore'),
auth = require('./auth'),
caps = require('./caps'),
config = require('../config'),
formidable = require('formidable'),
hooks = require('../hooks'),
Stream = require('stream'),
url_parse = require('url').parse,
util = require('util'),
winston = require('winston');
var send;
if (config.SERVE_STATIC_FILES)
send = require('send');
var escape = require('../common').escape_html;
var routes = [];
var resources = [];
var server = require('http').createServer(function (req, resp) {
var ip = req.connection.remoteAddress;
var country;
if (config.TRUST_X_FORWARDED_FOR)
ip = parse_forwarded_for(req.headers['x-forwarded-for']) || ip;
if (config.CLOUDFLARE) {
ip = req.headers['cf-connecting-ip'] || ip;
country = req.headers['cf-ipcountry'];
}
if (!ip) {
resp.writeHead(500, {'Content-Type': 'text/plain'});
resp.end("Your IP could not be determined. "
+ "This server is misconfigured.");
return;
}
req.ident = caps.lookup_ident(ip, country);
if (req.ident.timeout)
return timeout(resp);
if (req.ident.ban)
return render_500(resp);
if (req.ident.slow)
return slow_request(req, resp);
handle_request(req, resp);
});
exports.server = server;
function handle_request(req, resp) {
var method = req.method.toLowerCase();
var parsed = url_parse(req.url, true);
req.url = parsed.pathname;
req.query = parsed.query;
req.cookies = parse_cookie(req.headers.cookie);
var numRoutes = routes.length;
for (var i = 0; i < numRoutes; i++) {
var route = routes[i];
if (method != route.method)
continue;
var m = req.url.match(route.pattern);
if (m) {
route.handler(req, resp, m);
if (config.DEBUG)
winston.verbose(route.method.toUpperCase() + ' ' + req.url);
return;
}
}
if (method == 'get' || method == 'head')
for (var i = 0; i < resources.length; i++)
if (handle_resource(req, resp, resources[i]))
return;
if (config.SERVE_IMAGES) {
if (require('../imager').serve_image(req, resp))
return;
}
if (config.SERVE_STATIC_FILES) {
send(req, req.url, {root: 'www/'}).pipe(resp);
return;
}
render_404(resp);
if (config.DEBUG)
winston.verbose('404 ' + req.url + ' fallthrough');
}
function handle_resource(req, resp, resource) {
var m = req.url.match(resource.pattern);
if (!m)
return false;
var args = [req];
if (resource.headParams)
args.push(m);
args.push(resource_second_handler.bind(null, req, resp, resource));
var cookie = auth.extract_login_cookie(req.cookies);
if (cookie) {
auth.check_cookie(cookie, function (err, ident) {
if (err && !resource.authPassthrough) {
if (config.DEBUG)
winston.verbose('DENY ' + req.url + ' (' + err + ')');
return forbidden(resp, 'No cookie.');
}
else if (!err)
_.extend(req.ident, ident);
resource.head.apply(null, args);
});
}
else if (!resource.authPassthrough) {
if (config.DEBUG)
winston.verbose('DENY ' + req.url);
render_404(resp);
}
else
resource.head.apply(null, args);
return true;
}
function resource_second_handler(req, resp, resource, err, act, arg) {
var method = req.method.toLowerCase();
var log = config.DEBUG;
if (err) {
if (err == 404) {
if (log)
winston.verbose('404 ' + req.url);
return render_404(resp);
}
else if (err != 500)
winston.error(err);
else if (log)
winston.verbose('500 ' + req.url);
return render_500(resp);
}
else if (act == 'ok') {
if (log)
winston.verbose(method.toUpperCase() + ' ' + req.url + ' 200');
if (method == 'head') {
var headers = (arg && arg.headers) || noCacheHeaders;
resp.writeHead(200, headers);
resp.end();
if (resource.tear_down)
resource.tear_down.call(arg);
}
else {
if (resource.tear_down) {
if (!arg)
arg = {};
arg.finished = function () {
resource.tear_down.call(arg);
};
}
resource.get.call(arg, req, resp);
}
}
else if (act == 304) {
resp.writeHead(304);
resp.end();
if (log)
winston.verbose('304 ' + req.url);
}
else if (act == 'redirect' || (act >= 300 && act < 400)) {
var headers = {Location: arg};
if (act == 'redirect')
act = 303;
if (log)
winston.verbose(act + ' ' + req.url + ' to ' + arg);
if (act == 303.1) {
act = 303;
headers['X-Robots-Tag'] = 'nofollow';
}
resp.writeHead(act, headers);
resp.end();
}
else if (act == 'redirect_js') {
if (log)
winston.verbose('303.js ' + req.url + ' to ' + arg);
if (method == 'head') {
resp.writeHead(303, {Location: arg});
resp.end();
}
else
redirect_js(resp, arg);
}
else
throw new Error("Unknown resource handler: " + act);
}
exports.route_get = function (pattern, handler) {
routes.push({method: 'get', pattern: pattern,
handler: auth_passthrough.bind(null, handler)});
};
exports.resource = function (pattern, head, get, tear_down) {
if (head === true)
head = function (req, cb) { cb(null, 'ok'); };
var res = {pattern: pattern, head: head, authPassthrough: true};
res.headParams = (head.length == 3);
if (get)
res.get = get;
if (tear_down)
res.tear_down = tear_down;
resources.push(res);
};
exports.resource_auth = function (pattern, head, get, finished) {
if (head === true)
head = function (req, cb) { cb(null, 'ok'); };
var res = {pattern: pattern, head: head, authPassthrough: false};
res.headParams = (head.length == 3);
if (get)
res.get = get;
if (finished)
res.finished = finished;
resources.push(res);
};
function parse_forwarded_for(ff) {
if (!ff)
return null;
var ips = ff.split(',');
if (!ips.length)
return null;
var last = ips[ips.length - 1].trim();
/* check that it looks like some kind of IPv4/v6 address */
if (!/^[\da-fA-F.:]{3,45}$/.test(last))
return null;
return last;
}
exports.parse_forwarded_for = parse_forwarded_for;
function auth_passthrough(handler, req, resp, params) {
var cookie = auth.extract_login_cookie(req.cookies);
if (!cookie) {
handler(req, resp, params);
return;
}
auth.check_cookie(cookie, function (err, ident) {
if (!err)
_.extend(req.ident, ident);
handler(req, resp, params);
});
}
exports.route_get_auth = function (pattern, handler) {
routes.push({method: 'get', pattern: pattern,
handler: auth_checker.bind(null, handler, false)});
};
function auth_checker(handler, is_post, req, resp, params) {
if (is_post) {
var form = new formidable.IncomingForm();
form.maxFieldsSize = 50 * 1024;
form.type = 'urlencoded';
try {
form.parse(req, function (err, fields) {
if (err) {
resp.writeHead(500, noCacheHeaders);
resp.end(preamble + escape(err));
return;
}
req.body = fields;
check_it();
});
}
catch (e) {
winston.error('formidable threw: ' + e);
return forbidden(resp, 'Bad request.');
}
}
else
check_it();
function check_it() {
cookie = auth.extract_login_cookie(req.cookies);
if (!cookie)
return forbidden(resp, 'No cookie.');
auth.check_cookie(cookie, ack);
}
function ack(err, session) {
if (err)
return forbidden(resp, err);
if (is_post && session.csrf != req.body.csrf)
return forbidden(resp, "Possible CSRF.");
_.extend(req.ident, session);
handler(req, resp, params);
}
}
function forbidden(resp, err) {
resp.writeHead(401, noCacheHeaders);
resp.end(preamble + escape(err));
}
exports.route_post = function (pattern, handler) {
// auth_passthrough conflicts with formidable
// (by the time the cookie check comes back, formidable can't
// catch the form data)
// We don't need the auth here anyway currently thanks to client_id
routes.push({method: 'post', pattern: pattern, handler: handler});
};
exports.route_post_auth = function (pattern, handler) {
routes.push({method: 'post', pattern: pattern,
handler: auth_checker.bind(null, handler, true)});
};
var noCacheHeaders = {'Content-Type': 'text/html; charset=UTF-8',
'Expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'Cache-Control': 'no-cache, no-store',
'X-Frame-Options': 'sameorigin',
'X-XSS-Protection': '1',
};
var preamble = '<!doctype html><meta charset=utf-8>';
exports.noCacheHeaders = noCacheHeaders;
exports.notFoundHtml = preamble + '<title>404</title>404';
exports.serverErrorHtml = preamble + '<title>500</title>Server error';
hooks.hook('reloadResources', function (res, cb) {
exports.notFoundHtml = res.notFoundHtml;
exports.serverErrorHtml = res.serverErrorHtml;
cb(null);
});
function render_404(resp) {
resp.writeHead(404, noCacheHeaders);
resp.end(exports.notFoundHtml);
};
exports.render_404 = render_404;
function render_500(resp) {
resp.writeHead(500, noCacheHeaders);
resp.end(exports.serverErrorHtml);
}
exports.render_500 = render_500;
function slow_request(req, resp) {
var n = Math.floor(1000 + Math.random() * 500);
if (Math.random() < 0.1)
n *= 10;
setTimeout(function () {
if (resp.finished)
return;
if (resp.socket && resp.socket.destroyed)
return resp.end();
handle_request(req, new Debuff(resp));
}, n);
}
function timeout(resp) {
var n = Math.random();
n = Math.round(9000 + n*n*50000);
setTimeout(function () {
if (resp.socket && !resp.socket.destroyed)
resp.socket.destroy();
resp.end();
}, n);
}
function redirect(resp, uri, code) {
var headers = {Location: uri};
for (var k in noCacheHeaders)
headers[k] = noCacheHeaders[k];
resp.writeHead(code || 303, headers);
resp.end(preamble + '<title>Redirect</title>'
+ '<a href="' + encodeURI(uri) + '">Proceed</a>.');
}
exports.redirect = redirect;
var redirectJsTmpl = require('fs').readFileSync('tmpl/redirect.html');
function redirect_js(resp, uri) {
resp.writeHead(200, noCacheHeaders);
resp.write(preamble + '<title>Redirecting...</title>');
resp.write('<script>var dest = "' + encodeURI(uri) + '";</script>');
resp.end(redirectJsTmpl);
}
exports.redirect_js = redirect_js;
exports.dump_server_error = function (resp, err) {
resp.writeHead(500, noCacheHeaders);
resp.write(preamble + '<title>Server error</title>\n<pre>');
resp.write(escape(util.inspect(err)));
resp.end('</pre>');
};
function parse_cookie(header) {
var chunks = {};
(header || '').split(';').forEach(function (part) {
var bits = part.match(/^([^=]*)=(.*)$/);
if (bits)
try {
chunks[bits[1].trim()] = decodeURIComponent(
bits[2].trim());
}
catch (e) {}
});
return chunks;
}
exports.parse_cookie = parse_cookie;
exports.prefers_json = function (accept) {
/* Not technically correct but whatever */
var mimes = (accept || '').split(',');
for (var i = 0; i < mimes.length; i++) {
if (/json/i.test(mimes[i]))
return true;
else if (/(html|xml|plain|image)/i.test(mimes[i]))
return false;
}
return false;
};
function Debuff(stream) {
Stream.call(this);
this.out = stream;
this.buf = [];
this.timer = 0;
this.writable = true;
this.destroyed = false;
this.closing = false;
this._flush = this._flush.bind(this);
this.on_close = this.destroy.bind(this);
this.on_error = this.on_error.bind(this);
stream.once('close', this.on_close);
stream.on('error', this.on_error);
this.timeout = setTimeout(this.destroy.bind(this), 120*1000);
}
util.inherits(Debuff, Stream);
var D = Debuff.prototype;
D.writeHead = function () {
if (!this._check())
return false;
this.buf.push({_head: [].slice.call(arguments)});
this._queue();
return true;
};
D.write = function (data, encoding) {
if (!this._check())
return false;
if (encoding)
this.buf.push({_enc: encoding, _data: data});
else
this.buf.push(data);
this._queue();
return true;
};
D.end = function (data, encoding) {
if (!this._check())
return;
if (encoding)
this.buf.push({_enc: encoding, _data: data});
else if (data)
this.buf.push(data);
this._queue();
this.closing = true;
this.cleanEnd = true;
};
D._check = function () {
if (!this.writable)
return false;
if (!this.out.writable) {
this.destroy();
return false;
}
if (this.out.sock && this.out.sock.destroyed) {
this.destroy();
return false;
}
return true;
};
D._queue = function () {
if (this.timer)
return;
if (Math.random() < 0.05)
return;
var wait = 500 + Math.floor(Math.random() * 5000);
if (Math.random() < 0.5)
wait *= 2;
this.timer = setTimeout(this._flush, wait);
};
D._flush = function () {
var limit = 500 + Math.floor(Math.random() * 1000);
if (Math.random() < 0.05)
limit *= 3;
var count = 0;
while (this.out.writable && this.buf.length && count < limit) {
var o = this.buf.shift();
if (o._head) {
this.out.writeHead.apply(this.out, o._head);
this.statusCode = this.out.statusCode;
continue;
}
var enc;
if (o._enc && o._data) {
enc = o.enc;
o = o._data;
}
if (!o.length)
continue;
var n = limit - count;
if (typeof o == 'string' && o.length > n) {
this.buf.unshift(o.slice(n));
o = o.slice(0, n);
}
count += o.length;
if (!this.out.write(o, enc))
break;
}
this.timer = 0;
if (this.out.writable && this.buf.length)
this._queue();
else if (this.closing) {
if (this.cleanEnd) {
this.out.end();
this._clean_up();
this.emit('close');
}
else {
this.destroy();
}
}
else
this.emit('drain');
};
D.destroy = function () {
if (this.destroyed)
return;
this._clean_up();
this.cleanEnd = false;
this.emit('close');
};
D.destroySoon = function () {
if (!this.timer)
return this.destroy();
this.writable = false;
this.closing = true;
};
D.on_error = function (err) {
if (!this.destroyed)
this._clean_up();
this.cleanEnd = false;
this.emit('error', err);
};
D._clean_up = function () {
this.writable = false;
this.destroyed = true;
this.closing = false;
this.out.removeListener('close', this.on_close);
this.out.removeListener('error', this.on_error);
if (this.timer) {
clearTimeout(this.timer);
this.timer = 0;
}
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = 0;
}
if (!this.out.finished) {
this.out.destroy();
}
};
D.getHeader = function (name) { return this.out.getHeader(name); };
D.setHeader = function (k, v) { this.out.setHeader(k, v); };
D.removeHeader = function (name) { return this.out.removeHeader(name); };
D.addTrailers = function (headers) { this.out.addTrailers(headers); };

@ -0,0 +1,28 @@
// avoids stack overflow for long lists
exports.forEach = function (array, func, callback) {
step(0);
function step(i) {
if (i >= array.length)
return callback(null);
func(array[i], function (err) {
if (err)
return callback(err);
setImmediate(step, i + 1);
});
}
};
exports.map = function (array, func, callback) {
var results = [];
step(0);
function step(i) {
if (i >= array.length)
return callback(null, results);
func(array[i], function (err, res) {
if (err)
return callback(err);
results.push(res);
setImmediate(step, i + 1);
});
}
};

@ -0,0 +1,15 @@
<!doctype html>
<head>
<meta charset="utf-8">
<title>$BOARD</title>
<script></script>
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0">
<link rel="stylesheet" href="{{MEDIA_URL}}css/curfew.css?v=3">
</head>
<body>
<div></div>
<audio id="bgm" src="{{MEDIA_URL}}bgm/rustle.ogg" preload="auto" autoplay></audio>
</body>
<script>var END = $END;</script>
<script src="{{MEDIA_URL}}js/jquery-1.10.2.min.js"></script>
<script src="{{MEDIA_URL}}js/curfew.js?v=4"></script>

@ -0,0 +1,43 @@
<!doctype html>
<meta charset="utf-8">
<title>Admin</title>
<style>
img { border: 2px solid white; }
img.ui-selecting { border: 2px solid #f88; }
img.ui-selected { border: 2px solid #f22; }
</style>
<article>
$CONTENT
</article>
<script src="{{MEDIA_URL}}js/jquery-1.10.2.min.js"></script>
<script src="{{MEDIA_URL}}js/jquery-ui-1.8.16.min.js"></script>
<script>
var $del_button = $('<input type=button value=Delete>');
function delete_selected() {
// fgsfds
var threads = [];
$('img.ui-selected').each(function () {
threads.push(parseInt($(this).prop('alt')));
});
$del_button.val('Deleting ' + threads.length + '...');
$.ajax({
type: 'POST',
url: 'admin',
data: {threads: threads.join(), csrf: $CSRF},
success: function (data) {
$del_button.val('Deleted. Refresh.');
},
error: function (xhr, text, err) {
// TODO
alert(text);
$del_button.val('Failed.');
},
});
}
$(document).ready(function () {
$('article').selectable();
$('body').append($del_button.click(delete_selected));
});
</script>

@ -0,0 +1,24 @@
<!doctype html>
<head>
<meta charset="utf-8">
<title>$TITLE</title>
<script></script>
<meta name="description" content="$METADESC">
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0">
$META <!--[if lt IE 9]><script src="{{MEDIA_URL}}js/ie.js"></script><![endif]-->
<script src="{{MEDIA_URL}}js/setup.js?v=6"></script>
</head>
<a id="feedback" href="mailto:{{EMAIL}}" target="_blank" ref="noopener noreferrer">Feedback</a>
$NAV
<h1>$TITLE</h1>
<span id="sync">Not synched.</span>
<fieldset>
<label for="xD" class="ident">ID:</label> <input id="xD" name="xD" autocomplete="nope" autocorrect="off"><br>
<label for="pogchamp" class="ident">Meru:</label> <input id="pogchamp" name="pogchamp" autocomplete="nope" autocorrect="off"><br>
</fieldset>
$THREADS
$NAV
<script src="{{MEDIA_URL}}js/jquery-1.10.2.min.js"></script>
<script src="{{MEDIA_URL}}js/sockjs-1.1.1.min.js"></script>
<script src="{{MEDIA_URL}}js/{{VENDOR_JS}}"></script>
<script src="{{MEDIA_URL}}js/{{CLIENT_JS}}" charset="UTF-8"></script>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save