<commit_before><?php namespace App;

use App\BoardSetting;
use App\Post;
use App\PostCite;
use App\Option;
use App\Role;
use App\Stats;
use App\StatsUnique;
use App\User;
use App\UserRole;
use App\Contracts\PermissionUser;
use App\Services\ContentFormatter;
use App\Support\IP;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletingTrait;

use InfinityNext\BrennanCaptcha\Captcha;

use DB;
use Cache;
use Request;

use Event;
use App\Events\BoardWasCreated;
use App\Events\BoardWasReassigned;

class Board extends Model {
	
	/**
	 * The RegEx used to check the validity of a board uri.
	 *
	 * @var string
	 */
	const URI_PATTERN = "^[a-z0-9]{1,32}\b$";
	const URI_PATTERN_INNER = "[a-z0-9]{1,32}";
	
	/**
	 * The database table used by the model.
	 *
	 * @var string
	 */
	protected $table = 'boards';
	
	/**
	 * The primary key that is used by ::get()
	 *
	 * @var string
	 */
	protected $primaryKey = 'board_uri';
	
	/**
	 * Denotes our primary key is not an autoincrementing integer.
	 *
	 * @var string
	 */
	public $incrementing = false;
	
	/**
	 * Denotes this instance is the currently "opened" board.
	 *
	 * @var boolean
	 */
	public $applicationSingleton = false;
	
	/**
	 * The accessors to append to the model's array form.
	 *
	 * @var array
	 */
	protected $appends = ['stats_plh', 'stats_pph', 'stats_ppd', 'stats_active_users', 'stats_active_ranges', ];
	
	/**
	 * The attributes that are mass assignable.
	 *
	 * @var array
	 */
	protected $fillable = ['board_uri', 'title', 'description', 'created_at', 'created_by', 'operated_by', 'posts_total', 'is_indexed', 'is_overboard', 'is_worksafe', ];
	
	/**
	 * The attributes excluded from the model's JSON form.
	 *
	 * @var array
	 */
	protected $hidden = ['created_at', 'created_by', 'operated_by', ];
	
	/**
	 * Attributes which are automatically sent through a Carbon instance on load.
	 *
	 * @var array
	 */
	protected $dates = ['created_at', 'updated_at', 'last_post_at', ];
	
	/**
	 * A cache of compiled board settings.
	 *
	 * @static
	 * @var array  of arrays which contain Options with BoardSetting.option_value pivot keys
	 */
	protected static $config = [];
	
	/**
	 * Ties database triggers to the model.
	 *
	 * @static
	 * @return void
	 */
	public static function boot()
	{
		parent::boot();
		
		// Setup event bindings...
		
		// Fire event on board created.
		static::created(function(Board $board) {
			Event::fire(new BoardWasCreated($board, $board->operator));
		});
		
		// Handle board reassignment
		static::saved(function(Board $board) {
			foreach( $board->getDirty() as $attribute => $value)
			{
				if ($attribute === "operated_by")
				{
					Event::fire(new BoardWasReassigned($board, $board->operator));
					break;
				}
			}
		});
		
	}
	
	
	public function assets()
	{
		return $this->hasMany('\App\BoardAsset', 'board_uri');
	}
	
	public function checksums()
	{
		return $this->hasMany('\App\PostChecksum', 'board_uri');
	}
	
	public function bans()
	{
		return $this->hasMany('\App\Ban', 'board_uri');
	}
	
	public function posts()
	{
		return $this->hasMany('\App\Post', 'board_uri');
	}
	
	public function logs()
	{
		return $this->hasMany('\App\Log', 'board_uri');
	}
	
	public function operator()
	{
		return $this->belongsTo('\App\User', 'operated_by', 'user_id');
	}
	
	public function creator()
	{
		return $this->belongsTo('\App\User', 'created_by', 'user_id');
	}
	
	public function threads()
	{
		return $this->hasMany('\App\Post', 'board_uri');
	}
	
	public function roles()
	{
		return $this->hasMany('\App\Role', 'board_uri');
	}
	
	public function settings()
	{
		return $this->hasMany('\App\BoardSetting', 'board_uri');
	}
	
	public function staffAssignments()
	{
		return $this->hasManyThrough('App\UserRole', 'App\Role', 'board_uri', 'role_id');
	}
	
	public function stats()
	{
		return $this->hasMany('\App\Stats', 'board_uri');
	}
	
	public function tags()
	{
		return $this->belongsToMany('App\BoardTag', 'board_tag_assignments', 'board_uri', 'board_tag_id');
	}
	
	public function uniques()
	{
		return $this->hasManyThrough('App\StatsUnique', 'App\Stats', 'board_uri', 'stats_id');
	}
	
	
	public function canAttach(PermissionUser $user)
	{
		if ($this->getConfig('postAttachmentsMax', 1) > 0)
		{
			return $user->canAttachNew($this) || $user->canAttachOld($this);
		}
		
		return false;
	}
	
	public function canBan(PermissionUser $user)
	{
		return $user->canBan($this);
	}
	
	public function canDelete(PermissionUser $user)
	{
		return $user->canDeleteLocally($this);
	}
	
	public function canEditConfig(PermissionUser $user)
	{
		return $user->canEditConfig($this);
	}
	
	public function canPostReply(PermissionUser $user)
	{
		return $user->canPostReply($this);
	}
	
	public function canPostWithAuthor(PermissionUser $user, $asReply)
	{
		// If we don't allow author and user is not board admin
		return $this->getConfig('postsAllowAuthor') || $user->canEditConfig($this);
	}
	
	public function canPostWithSubject(PermissionUser $user, $asReply)
	{
		// If we don't allow subjects and user is not board admin
		$canPostSubject = $this->getConfig('postsAllowSubject') || $user->canEditConfig($this);
		
		// ... and this is not a new thread and we don't require subjects for new threads
		$canPostSubject = $canPostSubject || (!$asReply && $this->getConfig('threadRequireSubject'));
		
		return $canPostSubject;
		
	}
	
	public function canPostThread(PermissionUser $user)
	{
		return $user->canPostThread($this);
	}
	
	public function canPostWithoutCaptcha(PermissionUser $user)
	{
		// Check if site requires captchas.
		if (!site_setting('captchaEnabled'))
		{
			return true;
		}
		
		// Check if this user can bypass captchas.
		if ($user->canPostWithoutCaptcha($this))
		{
			return true;
		}
		
		// Begin to check captchas for last answers.
		$ip = new IP;
		
		$lastCaptcha = Captcha::select('created_at', 'cracked_at')
			->where(function($query) use ($ip) {
				// Find captchas answered by this user.
				$query->where('client_ip', $ip);
				
				// Pull the lifespan of a captcha.
				// This is the number of minutes between successful entries.
				$captchaLifespan = (int) site_setting('captchaLifespanTime', 0);
				
				if ($captchaLifespan > 0)
				{
					$query->whereNotNull('cracked_at');
					$query->where('cracked_at', '>=', \Carbon\Carbon::now()->subMinutes($captchaLifespan));
				}
			})
			->orderBy('cracked_at', 'desc')
			->first();
		
		$requireCaptcha = !($lastCaptcha instanceof Captcha);
		
		if (!$requireCaptcha)
		{
			$captchaLifespan = (int) site_setting('captchaLifespanPosts');
			
			if ($captchaLifespan > 0)
			{
				$postsWithCaptcha = Post::select('created_at')
					->where('author_ip', $ip)
					->where('created_at', '>=', $lastCaptcha->created_at)
					->count();
				
				$requireCaptcha = $postsWithCaptcha >= $captchaLifespan;
			}
		}
		
		return !$requireCaptcha;
	}
	
	public function canPostInLockedThreads(PermissionUser $user)
	{
		return $user->canPostInLockedThreads($this);
	}
	
	
	public function clearCachedThreads()
	{
		switch (env('CACHE_DRIVER'))
		{
			case "file" :
				break;
			
			case "database" :
				DB::table('cache')
					->where('key', 'like', "%board.{$this->board_uri}.thread.%")
					->delete();
				break;
			
			default :
				Cache::tags("board.{$this->board_uri}", "threads")->flush();
				break;
		}
	}
	
	public function clearCachedPages()
	{
		Cache::forget("board.{$this->board_uri}.catalog");
		Cache::forget("board.{$this->board_uri}.pages");
		
		switch (env('CACHE_DRIVER'))
		{
			case "file" :
				for ($i = 1; $i <= $this->getPageCount() * 2; ++$i)
				{
					Cache::forget("board.{$this->board_uri}.page.{$i}");
				}
				break;
			
			case "database" :
				DB::table('cache')
					->where('key', 'like', "%board.{$this->board_uri}.page.%")
					->delete();
				break;
			
			default :
				Cache::tags("board.{$this->board_uri}.pages")->flush();
				break;
		}
	}
	
	/**
	 * 
	 *
	 * @static
	 * @param  \Carbon|Carbon  $carbon  A timestmap within the 0-60 minute block that is to be snapshotted.
	 * @return array  of \App\Stats
	 */
	public static function createStatsSnapshots(\Carbon\Carbon $carbon = null)
	{
		$stats = collect([]);
		
		if (is_null($carbon))
		{
			$carbon = \Carbon\Carbon::now()->subHour()->minute(0)->second(0);
		}
		
		static::chunk(25, function($boards) use ($stats, $carbon)
		{
			foreach ($boards as $board)
			{
				$stats = $stats->merge($board->createStatsSnapshot($carbon));
			}
		});
		
		return $stats;
	}
	
	/**
	 * Generates a snapshot in the database for the previous hour.
	 *
	 * @param  \Carbon\Carbon  $carbon  A timestamp within the 0-60 minute block that is to be snapshotted.
	 * @return array  of new \App\Stats
	 */
	public function createStatsSnapshot(\Carbon\Carbon $carbon)
	{
		$carbonStart = $carbon->minute(0)->second(0);
		$carbonEnd   = (clone $carbonStart);
		$carbonEnd   = $carbonEnd->addHour()->minute(0)->second(0)->subSecond();
		
		$posts = $this->posts()
			->withTrashed()
			->where('created_at', '>=', $carbonStart)
			->where('created_at', '<=', $carbonEnd)
			->select('post_id', 'author_ip', 'reply_to')
			->get();
		
		if ($posts->count() === 0)
		{
			return collect([]);
		}
		
		// Unique IPs.
		$authorsUnique = [];
		// Unique Post IDs.
		$postsUnique   = [];
		// Unique \16 ranges.
		$rangesUnique  = [];
		// Unique Thread Post IDs.
		$threadsUnique = [];
		
		foreach ($posts as $post)
		{
			$postsUnique[$post->post_id] = true;
			
			if (is_null($post->reply_to))
			{
				$threadsUnique[$post->post_id] = false;
			}
			
			if (!is_null($post->author_ip))
			{
				$ip = new IP($post->author_ip);
				
				if (!isset($authorsUnique[$ip->toText()]))
				{
					$authorsUnique[$ip->toText()] = $ip->toLong();
					
					$range = new IP("{$ip->getStart()}/16");
					$rangesUnique[$range->getStart()] = $range->toLong();
				}
			}
		}
		
		
		// Save uniques
		$statsRows = [];
		$uniques   = [
			'authors' => array_values($authorsUnique),
			'posts'   => array_keys($postsUnique),
			'ranges'  => array_values($rangesUnique),
			'threads' => array_keys($threadsUnique),
		];
		
		foreach ($uniques as $statsKey => $uniqueValues)
		{
			$statsBits = [];
			
			foreach ($uniqueValues as $uniqueValue)
			{
				$statsBits[] = [
					'unique' => (int) $uniqueValue,
				];
			}
			
			$statsRow = $this->stats()->updateOrCreate([
				'stats_time' => $carbonStart,
				'stats_type' => $statsKey,
			], [
				'counter'    => count($statsBits),
			]);
			
			if (!$statsRow->exists)
			{
				$statsRow->save();
			}
			
			$statsRow->uniques()->createMany($statsBits);
			
			$statsRows[] = $statsRow;
		}
		
		
		return collect($statsRows);
	}
	
	/**
	 * Fixes post totals on all boards.
	 *
	 * @static
	 * @return void
	 */
	public static function fixPostsTotal()
	{
		static::chunk(50, function($boards)
		{
			foreach ($boards as $board)
			{
				$last_post     = $board->posts()->select('board_id')->withTrashed()->orderBy('board_id', 'desc')->take(1)->first();
				$last_board_id = $last_post ? (int) $last_post->pluck('board_id') : 0;
				$posts_total   = (int) $board->posts_total;
				
				if (max( (int) $posts_total, (int) $last_board_id ) > $posts_total)
				{
					echo $board->board_uri;
				}
				
				$board->posts_total = max( (int) $posts_total, (int) $last_board_id );
				$board->save();
			}
		});
	}
	
	/**
	 * Gets the default album art for an audio file.
	 *
	 * @return string  url
	 */
	public function getAudioArtURL()
	{
		return asset("static/img/assets/audio.gif");<commit_msg>Importer hotfix 21<commit_after><?php namespace App;

use App\BoardSetting;
use App\Post;
use App\PostCite;
use App\Option;
use App\Role;
use App\Stats;
use App\StatsUnique;
use App\User;
use App\UserRole;
use App\Contracts\PermissionUser;
use App\Services\ContentFormatter;
use App\Support\IP;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletingTrait;

use InfinityNext\BrennanCaptcha\Captcha;

use DB;
use Cache;
use Request;

use Event;
use App\Events\BoardWasCreated;
use App\Events\BoardWasReassigned;

class Board extends Model {
	
	/**
	 * The RegEx used to check the validity of a board uri.
	 *
	 * @var string
	 */
	const URI_PATTERN = "^[a-z0-9]{1,32}\b$";
	const URI_PATTERN_INNER = "[a-z0-9]{1,32}";
	
	/**
	 * The database table used by the model.
	 *
	 * @var string
	 */
	protected $table = 'boards';
	
	/**
	 * The primary key that is used by ::get()
	 *
	 * @var string
	 */
	protected $primaryKey = 'board_uri';
	
	/**
	 * Denotes our primary key is not an autoincrementing integer.
	 *
	 * @var string
	 */
	public $incrementing = false;
	
	/**
	 * Denotes this instance is the currently "opened" board.
	 *
	 * @var boolean
	 */
	public $applicationSingleton = false;
	
	/**
	 * The accessors to append to the model's array form.
	 *
	 * @var array
	 */
	protected $appends = ['stats_plh', 'stats_pph', 'stats_ppd', 'stats_active_users', 'stats_active_ranges', ];
	
	/**
	 * The attributes that are mass assignable.
	 *
	 * @var array
	 */
	protected $fillable = ['board_uri', 'title', 'description', 'created_at', 'created_by', 'operated_by', 'posts_total', 'is_indexed', 'is_overboard', 'is_worksafe', ];
	
	/**
	 * The attributes excluded from the model's JSON form.
	 *
	 * @var array
	 */
	protected $hidden = ['created_at', 'created_by', 'operated_by', ];
	
	/**
	 * Attributes which are automatically sent through a Carbon instance on load.
	 *
	 * @var array
	 */
	protected $dates = ['created_at', 'updated_at', 'last_post_at', ];
	
	/**
	 * A cache of compiled board settings.
	 *
	 * @static
	 * @var array  of arrays which contain Options with BoardSetting.option_value pivot keys
	 */
	protected static $config = [];
	
	/**
	 * Ties database triggers to the model.
	 *
	 * @static
	 * @return void
	 */
	public static function boot()
	{
		parent::boot();
		
		// Setup event bindings...
		
		// Fire event on board created.
		static::created(function(Board $board) {
			Event::fire(new BoardWasCreated($board, $board->operator));
		});
		
		// Handle board reassignment
		static::saved(function(Board $board) {
			foreach( $board->getDirty() as $attribute => $value)
			{
				if ($attribute === "operated_by")
				{
					Event::fire(new BoardWasReassigned($board, $board->operator));
					break;
				}
			}
		});
		
	}
	
	
	public function assets()
	{
		return $this->hasMany('\App\BoardAsset', 'board_uri');
	}
	
	public function checksums()
	{
		return $this->hasMany('\App\PostChecksum', 'board_uri');
	}
	
	public function bans()
	{
		return $this->hasMany('\App\Ban', 'board_uri');
	}
	
	public function posts()
	{
		return $this->hasMany('\App\Post', 'board_uri');
	}
	
	public function logs()
	{
		return $this->hasMany('\App\Log', 'board_uri');
	}
	
	public function operator()
	{
		return $this->belongsTo('\App\User', 'operated_by', 'user_id');
	}
	
	public function creator()
	{
		return $this->belongsTo('\App\User', 'created_by', 'user_id');
	}
	
	public function threads()
	{
		return $this->hasMany('\App\Post', 'board_uri');
	}
	
	public function roles()
	{
		return $this->hasMany('\App\Role', 'board_uri');
	}
	
	public function settings()
	{
		return $this->hasMany('\App\BoardSetting', 'board_uri');
	}
	
	public function staffAssignments()
	{
		return $this->hasManyThrough('App\UserRole', 'App\Role', 'board_uri', 'role_id');
	}
	
	public function stats()
	{
		return $this->hasMany('\App\Stats', 'board_uri');
	}
	
	public function tags()
	{
		return $this->belongsToMany('App\BoardTag', 'board_tag_assignments', 'board_uri', 'board_tag_id');
	}
	
	public function uniques()
	{
		return $this->hasManyThrough('App\StatsUnique', 'App\Stats', 'board_uri', 'stats_id');
	}
	
	
	public function canAttach(PermissionUser $user)
	{
		if ($this->getConfig('postAttachmentsMax', 1) > 0)
		{
			return $user->canAttachNew($this) || $user->canAttachOld($this);
		}
		
		return false;
	}
	
	public function canBan(PermissionUser $user)
	{
		return $user->canBan($this);
	}
	
	public function canDelete(PermissionUser $user)
	{
		return $user->canDeleteLocally($this);
	}
	
	public function canEditConfig(PermissionUser $user)
	{
		return $user->canEditConfig($this);
	}
	
	public function canPostReply(PermissionUser $user)
	{
		return $user->canPostReply($this);
	}
	
	public function canPostWithAuthor(PermissionUser $user, $asReply)
	{
		// If we don't allow author and user is not board admin
		return $this->getConfig('postsAllowAuthor') || $user->canEditConfig($this);
	}
	
	public function canPostWithSubject(PermissionUser $user, $asReply)
	{
		// If we don't allow subjects and user is not board admin
		$canPostSubject = $this->getConfig('postsAllowSubject') || $user->canEditConfig($this);
		
		// ... and this is not a new thread and we don't require subjects for new threads
		$canPostSubject = $canPostSubject || (!$asReply && $this->getConfig('threadRequireSubject'));
		
		return $canPostSubject;
		
	}
	
	public function canPostThread(PermissionUser $user)
	{
		return $user->canPostThread($this);
	}
	
	public function canPostWithoutCaptcha(PermissionUser $user)
	{
		// Check if site requires captchas.
		if (!site_setting('captchaEnabled'))
		{
			return true;
		}
		
		// Check if this user can bypass captchas.
		if ($user->canPostWithoutCaptcha($this))
		{
			return true;
		}
		
		// Begin to check captchas for last answers.
		$ip = new IP;
		
		$lastCaptcha = Captcha::select('created_at', 'cracked_at')
			->where(function($query) use ($ip) {
				// Find captchas answered by this user.
				$query->where('client_ip', $ip);
				
				// Pull the lifespan of a captcha.
				// This is the number of minutes between successful entries.
				$captchaLifespan = (int) site_setting('captchaLifespanTime', 0);
				
				if ($captchaLifespan > 0)
				{
					$query->whereNotNull('cracked_at');
					$query->where('cracked_at', '>=', \Carbon\Carbon::now()->subMinutes($captchaLifespan));
				}
			})
			->orderBy('cracked_at', 'desc')
			->first();
		
		$requireCaptcha = !($lastCaptcha instanceof Captcha);
		
		if (!$requireCaptcha)
		{
			$captchaLifespan = (int) site_setting('captchaLifespanPosts');
			
			if ($captchaLifespan > 0)
			{
				$postsWithCaptcha = Post::select('created_at')
					->where('author_ip', $ip)
					->where('created_at', '>=', $lastCaptcha->created_at)
					->count();
				
				$requireCaptcha = $postsWithCaptcha >= $captchaLifespan;
			}
		}
		
		return !$requireCaptcha;
	}
	
	public function canPostInLockedThreads(PermissionUser $user)
	{
		return $user->canPostInLockedThreads($this);
	}
	
	
	public function clearCachedThreads()
	{
		switch (env('CACHE_DRIVER'))
		{
			case "file" :
				break;
			
			case "database" :
				DB::table('cache')
					->where('key', 'like', "%board.{$this->board_uri}.thread.%")
					->delete();
				break;
			
			default :
				Cache::tags("board.{$this->board_uri}", "threads")->flush();
				break;
		}
	}
	
	public function clearCachedPages()
	{
		Cache::forget("board.{$this->board_uri}.catalog");
		Cache::forget("board.{$this->board_uri}.pages");
		
		switch (env('CACHE_DRIVER'))
		{
			case "file" :
				for ($i = 1; $i <= $this->getPageCount() * 2; ++$i)
				{
					Cache::forget("board.{$this->board_uri}.page.{$i}");
				}
				break;
			
			case "database" :
				DB::table('cache')
					->where('key', 'like', "%board.{$this->board_uri}.page.%")
					->delete();
				break;
			
			default :
				Cache::tags("board.{$this->board_uri}.pages")->flush();
				break;
		}
	}
	
	/**
	 * 
	 *
	 * @static
	 * @param  \Carbon|Carbon  $carbon  A timestmap within the 0-60 minute block that is to be snapshotted.
	 * @return array  of \App\Stats
	 */
	public static function createStatsSnapshots(\Carbon\Carbon $carbon = null)
	{
		$stats = collect([]);
		
		if (is_null($carbon))
		{
			$carbon = \Carbon\Carbon::now()->subHour()->minute(0)->second(0);
		}
		
		static::chunk(25, function($boards) use ($stats, $carbon)
		{
			foreach ($boards as $board)
			{
				$stats = $stats->merge($board->createStatsSnapshot($carbon));
			}
		});
		
		return $stats;
	}
	
	/**
	 * Generates a snapshot in the database for the previous hour.
	 *
	 * @param  \Carbon\Carbon  $carbon  A timestamp within the 0-60 minute block that is to be snapshotted.
	 * @return array  of new \App\Stats
	 */
	public function createStatsSnapshot(\Carbon\Carbon $carbon)
	{
		$carbonStart = $carbon->minute(0)->second(0);
		$carbonEnd   = (clone $carbonStart);
		$carbonEnd   = $carbonEnd->addHour()->minute(0)->second(0)->subSecond();
		
		$posts = $this->posts()
			->withTrashed()
			->where('created_at', '>=', $carbonStart)
			->where('created_at', '<=', $carbonEnd)
			->select('post_id', 'author_ip', 'reply_to')
			->get();
		
		if ($posts->count() === 0)
		{
			return collect([]);
		}
		
		// Unique IPs.
		$authorsUnique = [];
		// Unique Post IDs.
		$postsUnique   = [];
		// Unique \16 ranges.
		$rangesUnique  = [];
		// Unique Thread Post IDs.
		$threadsUnique = [];
		
		foreach ($posts as $post)
		{
			$postsUnique[$post->post_id] = true;
			
			if (is_null($post->reply_to))
			{
				$threadsUnique[$post->post_id] = false;
			}
			
			if (!is_null($post->author_ip))
			{
				$ip = new IP($post->author_ip);
				
				if (!isset($authorsUnique[$ip->toText()]))
				{
					$authorsUnique[$ip->toText()] = $ip->toLong();
					
					$range = new IP("{$ip->getStart()}/16");
					$rangesUnique[$range->getStart()] = $range->toLong();
				}
			}
		}
		
		
		// Save uniques
		$statsRows = [];
		$uniques   = [
			'authors' => array_values($authorsUnique),
			'posts'   => array_keys($postsUnique),
			'ranges'  => array_values($rangesUnique),
			'threads' => array_keys($threadsUnique),
		];
		
		foreach ($uniques as $statsKey => $uniqueValues)
		{
			$statsBits = [];
			
			foreach ($uniqueValues as $uniqueValue)
			{
				$statsBits[] = [
					'unique' => (int) $uniqueValue,
				];
			}
			
			$statsRow = $this->stats()->updateOrCreate([
				'stats_time' => $carbonStart,
				'stats_type' => $statsKey,
			], [
				'counter'    => count($statsBits),
			]);
			
			if (!$statsRow->exists)
			{
				$statsRow->save();
			}
			
			$statsRow->uniques()->createMany($statsBits);
			
			$statsRows[] = $statsRow;
		}
		
		
		return collect($statsRows);
	}
	
	/**
	 * Fixes post totals on all boards.
	 *
	 * @static
	 * @return void
	 */
	public static function fixPostsTotal()
	{
		static::chunk(50, function($boards)
		{
			foreach ($boards as $board)
			{
				$last_post     = Post::where('board_uri', $board->board_uri)->select('board_id')->withTrashed()->orderBy('board_id', 'desc')->first();
				$last_board_id = $last_post ? (int) $last_post->board_id: 0;
				$posts_total   = (int) $board->posts_total;
				
				if (max( (int) $posts_total, (int) $last_board_id ) > $posts_total)
				{
					echo "\n{$board->board_uri} -- {$posts_total} < {$last_board_id}!!!!!!!!!\n\n";
				}
				else
				{
					echo $board->board_uri . " -- {$posts_total} >= {$last_board_id}\n";
				}
				
				$board->posts_total = max( (int) $posts_total, (int) $last_board_id );
				$board->save();
			}
		});
	}
	
	/**
	 * Gets the default album art for an audio file.
	 *
	 * @return string  url
	 */
	public function getAudioArtURL()
	{
		return asset("static/img/assets/audio.gif");
{'fraction_non_alphanumeric': 0.16923800162400102, 'fraction_numerical': 0.0034616863968545664, 'mean_word_length': 3.3591654247391953, 'pattern_counts': {'":': 0, '<': 10, '<?xml version=': 0, '>': 246, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2701476', 'n_tokens_mistral': 9463, 'n_tokens_neox': 7898, 'n_words': 1721}