init 4chan

This commit is contained in:
skidoodle 2025-04-17 09:20:34 +02:00
commit c850337f1e
No known key found for this signature in database
390 changed files with 195936 additions and 0 deletions

161
plugins/broomcloset.php Normal file
View file

@ -0,0 +1,161 @@
<?php
// broom closet - yotsuba plugin
// implementation of janitor discussion system.
/* Features:
* - "latest" mode - display last post number, etc, so that it can be polled by a script.
* - force posting with logged-in moderator/janitor name.
* - never expire posts.
* - allow no-file posts.
* - emit PHP instead of html in order to do admin-validation checking.
* - give everyone a capcode, and give janitors a tooltip saying which board they're in charge of.
*/
// Config enforcement... (too late to change it now)
if( NO_TEXTONLY == 1 ) die( 'Config NO_TEXTONLY should be turned off!' );
if( PHP_EXT == '.html' ) die( 'Config PHP_EXT should end in .php!' );
if( PAGE_MAX > 0 ) die( 'Config PAGE_MAX should be 0!' );
/* register_callback('mode_default_case', 'broomcloset_mode');
register_callback('regist_before', 'broomcloset_regist');
register_callback('trim_db_before', 'broomcloset_trim');
register_callback('head_before', 'broomcloset_head');
register_callback('form_after', 'broomcloset_form');
register_callback('post_before', 'broomcloset_post');
register_callback('capcode', 'broomcloset_capcode');
*/
// add the 'latest' mode
function broomcloset_latest()
{
//if (!valid('janitor_board')) die('');
$query = mysql_board_call( "SELECT * FROM `" . SQLLOG . "` ORDER BY no DESC LIMIT 1" );
if( $row = mysql_fetch_assoc( $query ) ) {
foreach( $row as &$val ) $val = addslashes( $val );
echo <<<EOJSON
{"no":{$row['no']}}
EOJSON;
}
die( '' );
}
function refresh_mod_cache()
{
global $mod_cache;
if( !isset( $mod_cache ) ) {
$admin_salt = file_get_contents('/www/keys/2014_admin.salt');
if (!$admin_salt) {
die('Internal Server Error (rmc0)');
}
$query = mysql_global_call( "SELECT id,username,allow,level from mod_users" );
$mod_cache = array();
while( list( $id, $username, $allow, $level ) = mysql_fetch_row( $query ) ) {
if( $allow ) {
$hashed_bits = hash_hmac('sha256', $username, $admin_salt, true);
$username = base64_encode($hashed_bits);
$mod_cache[$username] = array();
$board = '';
if( $level == 'janitor' ) {
$level = 'Janitor';
$color = '#4169E1';
$board = str_replace( ',janitor', '', $allow );
} elseif( $id == 2 ) {
$level = 'Admin';
$color = '#FF0000';
} elseif( $level == 'manager' ) { // disabled until mootapproval
$level = 'Manager';
$color = '#FF0080';
} else {
$level = 'Mod';
$color = '#800080';
}
$mod_cache[$username]['level'] = $level;
$mod_cache[$username]['color'] = $color;
$mod_cache[$username]['id'] = $id;
if( $board )
$mod_cache[$username]['board'] = $board;
}
}
}
}
function broomcloset_name( $name )
{
global $mod_cache;
refresh_mod_cache();
if( !isset( $mod_cache[$name] ) ) { // user not found
return 'Anonymous';
}
return 'Anonymous ## ' . $mod_cache[$name]['level'];
}
function broomcloset_style( $name )
{
global $mod_cache;
refresh_mod_cache();
if( !isset( $mod_cache[$name] ) ) { // user not found
return ' style="color:#aaa"';
}
if( $mod_cache[$name]['board'] ) {
$tooltip = " style='color: {$mod_cache[$name]['color']}'";
} else {
$tooltip = " style='color: {$mod_cache[$name]['color']}'";
}
return $tooltip;
}
// auto-set name
function broomcloset_new_post( $caller )
{
// set textonly to 1 - this is ok even if they're posting a picture
// now imgboard won't complain about no picture EVER
$caller['textonly'] = 1;
$caller['name'] = $_COOKIE['4chan_auser'];
if( !has_level( 'janitor' ) ) die;
}
function broomcloset_form( $dat )
{ // modify the form to hide name, email, and textonly
$newform = str_replace( '<tr><td></td><td class="postblock" align="left"><b>Name</b></td><td><input type=text name=name size="28"><span id="tdname"></span></td></tr>', '<input type=hidden name=name>', $dat );
$newform = str_replace( '<tr><td></td><td class="postblock" align="left"><b>E-mail</b></td><td><input type=text name=email size="28"><span id="tdemail"></span></td></tr>', '<input type=hidden name=email>', $newform );
$newform = str_replace( '[<label><input type=checkbox name=textonly value=on>No File</label>]', '', $newform );
$newform = str_replace( 'name=sub size="35">', 'name=sub size="35"><span id="tdname"></span><span id="tdemail"></span>', $newform ); // move admin ext. placeholders next to subject
return $newform;
}
// this function is last because it screws up syntax coloring in my editor :(
function broomcloset_head( $dat )
{
$dat .= <<<'BUTTCODE'
<?php if( !isset( $_COOKIE['4chan_auser'] ) || !isset( $_COOKIE['apass'] ) ) { http_response_code(403); die(); }
require_once 'lib/admin.php';
require_once 'lib/auth.php';
header('Content-Security-Policy: connect-src *.4chan.org *.4cdn.org');
header('X-Content-Security-Policy: connect-src *.4chan.org *.4cdn.org');
auth_user();
if( !has_level('janitor') ) { http_response_code(403); die(); } ?>
BUTTCODE;
return $dat;
}

42
plugins/enhance_q.php Normal file
View file

@ -0,0 +1,42 @@
<?php
/**
* Enhance meta - Yotsuba plugin
* Enhances meta with various fabulous bits and bobs.
*/
function meta_is_thread_flagged( $resline )
{
global $log;
$rep = 0;
$posts = array('admin' => '', 'developer' => '', 'mod' => '', 'manager' => '');
while( list( $resrow ) = each( $resline ) ) {
if( !$log[ $resrow ][ 'no' ] ) {
break;
}
if( $log[ $resrow ][ 'capcode' ] === 'none' ) {
continue;
}
$capcode = ( $log[ $resrow ][ 'capcode' ] == 'admin_highlight' ) ? 'admin' : $log[$resrow]['capcode'];
$no = $log[$resrow]['no'];
$posts[$capcode] .= "$no,";
}
unset( $posts['none'] );
foreach( $posts as $key => $value ) {
if( $posts[$key] != '' ) {
$posts[$key] = substr($posts[$key], 0, -1);
}
}
return array( $posts['admin'], $posts['developer'], $posts['mod'], $posts['manager'] );
}
?>

250
plugins/robot9000.php Normal file
View file

@ -0,0 +1,250 @@
<?php
# Parameters:
# com: user supplied comment field (before or after wordfilters? dunno)
# md5: md5 of the supplied image. null if no image.
# ip: the IP of the user, in integer (packed) form
#
# Return value: A string. If the string is "OK", the post should go through.
# If the string is anything else, abort posting and display that message
# to the user.
#
# Integration info;
# This should run before wordfilters (including >>num) and duplicate(md5) detection.
# It doesn't know about bans, so those need to be done seperately, but it
# doesn't care if that is before or after.
# It really should run after valid file checks (jpg/png/gif, >0x0, etc) but that's not critical.
#
# Synchronization: There is (in theory) a minor race condition because the tables are not locked.
# It's not exploitable for any useful purpose, and it's blocked by the floodcheck
#
# Changelog:
# 2008/02/20 04:20: Added changelog, fixed $txt error that killed all posts
# 2008/02/20 04:56: Fixed the signal-ratio filter to handle the stupid HTML
# 2008/02/20 05:21: Added a check for repeated characters
# 2008/02/20 07:05: Added a check for long spams
# 2008/02/20 13:02: Rearranged the filters for better results.
# 2008/02/20 23:58: Fixed a bug that broke posts with two quotes far apart
# 2008/02/21 01:57: Fixed a dumb bug with the number filter..
# 2008/02/21 02:06: Adding content-percentage info to the content filter.
# 2008/02/21 02:15: Adjusted long-text filter.
# 2008/02/21 02:18: Removed long-text filter.
# 2008/02/22 16:43: Added mute-expiring.
# 2008/02/22 17:54: Fixed mute-expiring.
# 2008/02/22 18:13: Added #nextnow and #muteinfo secret mod capcodes
# 2008/02/22 18:21: Fixed #muteinfo for mods.
# 2015/10/24 16:36: Cleanup the code and put the robot back.
# $email, $sub, $name fields aren't used anymore.
# removed $mod parameter.
# 2020/11/16 08:09: Update text hashes for every post to prune stale entries
define('R9K_SIGNAL_RATIO', 0.1);
define('R9K_MAX_DURATION', 31536000); // one year
define('R9K_DATE_FORMAT', '%m/%d/%y %H:%M:%S');
define('R9K_DEMUTE_PERIOD', 86400); // one day
define('R9K_SNR_MIN_LEN', 10); // minimum txt length for signal ratio check
define('R9K_OK', 'OK');
define('R9K_DB_ERROR', 'Database error.');
define('R9K_EMPTY_COM', 'Textless posts are not allowed.');
define('R9K_ASCII_ONLY', 'Non-ASCII text is not allowed.');
define('R9K_MUTED', "You're muted! You cannot post until %s, %s from now");
define('R9K_MUTE_ERROR', "You have been muted for %s, because %s");
define('R9K_LOW_SNR', 'your comment was too low in content (%0.2f%% content).');
define('R9K_DUP_TXT', 'your comment was not original.');
define('R9K_DUP_IMG', 'your image was not original.');
function r9k_process($com, $md5, $ip) {
// Blank file
if ($md5 == 'd41d8cd98f00b204e9800998ecf8427e') {
$md5 = null;
}
if ($com === ''){
return R9K_EMPTY_COM;
}
if (preg_match('/[\\x80-\\xFF]/', $com)) {
return R9K_ASCII_ONLY;
}
$table_mutes = ROBOT9000_MUTES;
$table_posts = ROBOT9000_POSTS;
$ip = (int)$ip;
$mute = false;
$demute = false;
$timeout_power = 0;
$query = <<<SQL
SELECT timeout_power,
UNIX_TIMESTAMP(mute_until) as mute_until,
UNIX_TIMESTAMP(next_expire) as next_expire
FROM `$table_mutes` WHERE ip = $ip
SQL;
$res = mysql_board_call($query);
if (!$res) {
//return R9K_OK;
return R9K_DB_ERROR;
}
$row = mysql_fetch_assoc($res);
if ($row) {
$now = time();
$timeout_power = $row['timeout_power'];
if ($row['mute_until'] > $now) {
$duration = r9k_pretty_duration($row['mute_until'] - $now);
$when = strftime(R9K_DATE_FORMAT, $row['mute_until']);
return sprintf(R9K_MUTED, $when, $duration);
}
if ($row['next_expire'] < $now){
$demute = true;
}
}
$txt = strtolower($com);
// Strip HTML
$stxt=preg_replace('/<.*?>/s','', $txt);
// Original byte length
$olength = strlen($stxt);
// Strip >>123 quotelinks
$stxt = preg_replace('/&gt;&gt;\d+/', '', $stxt);
// Strip html entities
$stxt = preg_replace('/&#?\w+;/', '', $stxt);
// Strip non-alnum chars
$stxt = preg_replace('/[^a-z\d-]+/', '', $stxt);
// Trim leading and trailing numeric characters
$stxt = preg_replace('/^\d*(.*)\d*$/', '\1', $stxt);
// Compress repeated characters: aaa -> a
$stxt = preg_replace('/(.)\\1{2,}/', '\\1', $stxt);
// Check signal ratio
if (strlen($txt) > R9K_SNR_MIN_LEN) {
$ratio = strlen($stxt) / $olength;
if ($ratio < R9K_SIGNAL_RATIO) {
$mute = sprintf(R9K_LOW_SNR, $ratio * 100.0);
}
}
if ($mute === false) {
$txt_hash = md5($stxt);
// Check if hashes match
$query = "SELECT text, image FROM `$table_posts` WHERE text = '%s'";
/*
if ($md5) {
$query .= " OR image = '%s'";
$res = mysql_board_call($query, $txt_hash, $md5);
}
else {*/
$res = mysql_board_call($query, $txt_hash);
//}
if (!$res) {
//return R9K_OK;
return R9K_DB_ERROR;
}
// Post is good. Insert hashes.
if (mysql_num_rows($res) < 1) {
$query = "INSERT INTO `$table_posts` (text) VALUES('%s')";
mysql_board_call($query, $txt_hash);
}
// Duplicates found.
else {
//$row = mysql_fetch_assoc($res);
//if ($row['text'] === $txt_hash) {
$mute = R9K_DUP_TXT;
//}
//else if ($md5 && $row['image'] === $md5) {
// $mute = R9K_DUP_IMG;
//}
// Update the hash with a new timestamp
$query = "UPDATE `$table_posts` SET created_on = NOW() WHERE text = '%s' LIMIT 1";
mysql_board_call($query, $txt_hash);
}
}
// Muted
if ($mute !== false) {
++$timeout_power;
$mute_duration = pow(2, $timeout_power);
if ($mute_duration > R9K_MAX_DURATION) {
$timeout_power--;
$mute_duration = R9K_MAX_DURATION;
}
$next_expire = R9K_DEMUTE_PERIOD;
$query = <<<SQL
INSERT INTO `$table_mutes` (ip, timeout_power, mute_until, next_expire)
VALUES ($ip, $timeout_power, DATE_ADD(NOW(), INTERVAL $mute_duration SECOND),
DATE_ADD(NOW(), INTERVAL $mute_duration SECOND))
ON DUPLICATE KEY
UPDATE timeout_power = $timeout_power, mute_until = VALUES(mute_until),
next_expire = VALUES(next_expire)
SQL;
$res = mysql_board_call($query);
return sprintf(R9K_MUTE_ERROR, r9k_pretty_duration($mute_duration), $mute);
}
// Not muted
else {
if ($demute === true) {
$next_expire = R9K_DEMUTE_PERIOD;
$query = <<<SQL
UPDATE `$table_mutes` SET
timeout_power = IF(timeout_power > 0, timeout_power - 1, 0),
next_expire = DATE_ADD(NOW(), INTERVAL $next_expire SECOND)
WHERE ip = $ip
SQL;
$res = mysql_board_call($query);
}
return R9K_OK;
}
}
function r9k_pretty_duration($secs){
$w = (int)($secs / 604800);
$d = (int)($secs / 86400) % 7;
$h = (int)($secs / 3600) % 24;
$m = ((int)($secs / 60)) % 60;
$s = ((int)$secs) % 60;
$out = array();
$pairs = array(
array($w, 'week'),
array($d, 'day'),
array($h, 'hour'),
array($m, 'minute'),
array($s, 'second')
);
foreach($pairs as $v){
if ($v[0] !== 0) {
$out[] = $v[0] . ' ' . $v[1] . ($v[0] === 1 ? '' : 's');
}
}
return implode(' ', $out);
}

View file

@ -0,0 +1,28 @@
<?
// yotsuba plugin API
// add a function $function to the list of callbacks named by $hook.
function register_callback($hook, $function) {
global $HOOKS;
if( !isset($HOOKS[$hook]) )
$HOOKS[$hook] = array($function);
else
array_push($HOOKS[$hook], $function);
}
// run all the callback functions associated with the name $hook.
// they will be passed $args.
function run_callback($hook, $args) {
global $HOOKS;
if(isset($HOOKS[$hook]))
foreach($HOOKS[$hook] as $function)
if(function_exists($function))
$function($args);
}
// load plugins
if(defined('PLUGIN_DIR'))
foreach(explode(',', PLUGINS) as $plugin)
include_once PLUGIN_DIR . trim($plugin) . '.php';
?>