forked from flanchan/doushio
commit
0e3b24a641
After Width: | Height: | Size: 172 KiB |
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
|
@ -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(' ', $('<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(' ', 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);
|
||||
}
|
||||
|
||||
})();
|
@ -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);
|
@ -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 & 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…
Reference in new issue