skylib_fluffos_v3/
skylib_fluffos_v3/bin/
skylib_fluffos_v3/bin/db/
skylib_fluffos_v3/fluffos-2.9-ds2.04/
skylib_fluffos_v3/fluffos-2.9-ds2.04/ChangeLog.old/
skylib_fluffos_v3/fluffos-2.9-ds2.04/Win32/
skylib_fluffos_v3/fluffos-2.9-ds2.04/compat/
skylib_fluffos_v3/fluffos-2.9-ds2.04/compat/simuls/
skylib_fluffos_v3/fluffos-2.9-ds2.04/include/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/clone/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/command/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/data/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/etc/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/include/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/inherit/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/inherit/master/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/log/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/single/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/single/tests/compiler/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/single/tests/efuns/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/single/tests/operators/
skylib_fluffos_v3/fluffos-2.9-ds2.04/testsuite/u/
skylib_fluffos_v3/fluffos-2.9-ds2.04/tmp/
skylib_fluffos_v3/fluffos-2.9-ds2.04/windows/
skylib_fluffos_v3/mudlib/
skylib_fluffos_v3/mudlib/cmds/
skylib_fluffos_v3/mudlib/cmds/admin/
skylib_fluffos_v3/mudlib/cmds/guild-race/
skylib_fluffos_v3/mudlib/cmds/living/broken/
skylib_fluffos_v3/mudlib/cmds/player/group_cmds/
skylib_fluffos_v3/mudlib/cmds/playtester/
skylib_fluffos_v3/mudlib/d/admin/
skylib_fluffos_v3/mudlib/d/admin/room/
skylib_fluffos_v3/mudlib/d/admin/room/we_care/
skylib_fluffos_v3/mudlib/d/admin/save/
skylib_fluffos_v3/mudlib/d/admin/text/
skylib_fluffos_v3/mudlib/d/learning/TinyTown/buildings/
skylib_fluffos_v3/mudlib/d/learning/TinyTown/map/
skylib_fluffos_v3/mudlib/d/learning/TinyTown/roads/
skylib_fluffos_v3/mudlib/d/learning/chars/
skylib_fluffos_v3/mudlib/d/learning/functions/
skylib_fluffos_v3/mudlib/d/learning/handlers/
skylib_fluffos_v3/mudlib/d/learning/help_topics/
skylib_fluffos_v3/mudlib/d/learning/help_topics/npcs/
skylib_fluffos_v3/mudlib/d/learning/help_topics/objects/
skylib_fluffos_v3/mudlib/d/learning/help_topics/rcs_demo/
skylib_fluffos_v3/mudlib/d/learning/help_topics/rcs_demo/RCS/
skylib_fluffos_v3/mudlib/d/learning/help_topics/rooms/
skylib_fluffos_v3/mudlib/d/learning/help_topics/rooms/crowd/
skylib_fluffos_v3/mudlib/d/learning/help_topics/rooms/situations/
skylib_fluffos_v3/mudlib/d/learning/save/
skylib_fluffos_v3/mudlib/d/learning/school/
skylib_fluffos_v3/mudlib/d/learning/school/add_sc/
skylib_fluffos_v3/mudlib/d/learning/school/characters/
skylib_fluffos_v3/mudlib/d/learning/school/general/
skylib_fluffos_v3/mudlib/d/learning/school/getting-started/
skylib_fluffos_v3/mudlib/d/learning/school/getting-started/basic_commands/
skylib_fluffos_v3/mudlib/d/learning/school/getting-started/edtutor/
skylib_fluffos_v3/mudlib/d/learning/school/getting-started/unix_tutor/
skylib_fluffos_v3/mudlib/d/learning/school/items/
skylib_fluffos_v3/mudlib/d/learning/school/npc_school/
skylib_fluffos_v3/mudlib/d/learning/school/room_school/
skylib_fluffos_v3/mudlib/d/learning/school/room_school/room_basic/
skylib_fluffos_v3/mudlib/d/learning/school/room_school/situations/
skylib_fluffos_v3/mudlib/d/learning/school/room_school/terrain_tutor/
skylib_fluffos_v3/mudlib/d/learning/text/
skylib_fluffos_v3/mudlib/d/liaison/
skylib_fluffos_v3/mudlib/d/mudlib/
skylib_fluffos_v3/mudlib/d/mudlib/changes/
skylib_fluffos_v3/mudlib/d/playtesters/
skylib_fluffos_v3/mudlib/d/playtesters/effects/
skylib_fluffos_v3/mudlib/d/playtesters/handlers/
skylib_fluffos_v3/mudlib/d/playtesters/items/
skylib_fluffos_v3/mudlib/d/sage/
skylib_fluffos_v3/mudlib/doc/
skylib_fluffos_v3/mudlib/doc/creator/
skylib_fluffos_v3/mudlib/doc/driver/
skylib_fluffos_v3/mudlib/doc/driver/efuns/arrays/
skylib_fluffos_v3/mudlib/doc/driver/efuns/buffers/
skylib_fluffos_v3/mudlib/doc/driver/efuns/calls/
skylib_fluffos_v3/mudlib/doc/driver/efuns/compile/
skylib_fluffos_v3/mudlib/doc/driver/efuns/filesystem/
skylib_fluffos_v3/mudlib/doc/driver/efuns/floats/
skylib_fluffos_v3/mudlib/doc/driver/efuns/functions/
skylib_fluffos_v3/mudlib/doc/driver/efuns/general/
skylib_fluffos_v3/mudlib/doc/driver/efuns/mappings/
skylib_fluffos_v3/mudlib/doc/driver/efuns/mixed/
skylib_fluffos_v3/mudlib/doc/driver/efuns/mudlib/
skylib_fluffos_v3/mudlib/doc/driver/efuns/numbers/
skylib_fluffos_v3/mudlib/doc/driver/efuns/parsing/
skylib_fluffos_v3/mudlib/doc/login/
skylib_fluffos_v3/mudlib/doc/lpc/basic_manual/
skylib_fluffos_v3/mudlib/doc/lpc/intermediate/
skylib_fluffos_v3/mudlib/doc/new/add_command/
skylib_fluffos_v3/mudlib/doc/new/events/
skylib_fluffos_v3/mudlib/doc/new/handlers/
skylib_fluffos_v3/mudlib/doc/new/living/race/
skylib_fluffos_v3/mudlib/doc/new/living/spells/
skylib_fluffos_v3/mudlib/doc/new/object/
skylib_fluffos_v3/mudlib/doc/new/player/
skylib_fluffos_v3/mudlib/doc/new/room/guild/
skylib_fluffos_v3/mudlib/doc/new/room/outside/
skylib_fluffos_v3/mudlib/doc/new/room/storeroom/
skylib_fluffos_v3/mudlib/doc/object/
skylib_fluffos_v3/mudlib/doc/playtesters/
skylib_fluffos_v3/mudlib/doc/policy/
skylib_fluffos_v3/mudlib/doc/weapons/
skylib_fluffos_v3/mudlib/global/
skylib_fluffos_v3/mudlib/global/creator/
skylib_fluffos_v3/mudlib/handlers/
skylib_fluffos_v3/mudlib/include/casino/
skylib_fluffos_v3/mudlib/include/cmds/
skylib_fluffos_v3/mudlib/include/effects/
skylib_fluffos_v3/mudlib/include/npc/
skylib_fluffos_v3/mudlib/include/room/
skylib_fluffos_v3/mudlib/include/shops/
skylib_fluffos_v3/mudlib/net/daemon/
skylib_fluffos_v3/mudlib/net/daemon/chars/
skylib_fluffos_v3/mudlib/net/inherit/
skylib_fluffos_v3/mudlib/net/obj/
skylib_fluffos_v3/mudlib/net/obj/BACKUPS/
skylib_fluffos_v3/mudlib/obj/amulets/
skylib_fluffos_v3/mudlib/obj/armours/plate/
skylib_fluffos_v3/mudlib/obj/b_day/
skylib_fluffos_v3/mudlib/obj/clothes/transport/horse/
skylib_fluffos_v3/mudlib/obj/faith/symbols/
skylib_fluffos_v3/mudlib/obj/fungi/
skylib_fluffos_v3/mudlib/obj/gatherables/
skylib_fluffos_v3/mudlib/obj/instruments/
skylib_fluffos_v3/mudlib/obj/media/
skylib_fluffos_v3/mudlib/obj/misc/player_shop/
skylib_fluffos_v3/mudlib/obj/monster/godmother/
skylib_fluffos_v3/mudlib/obj/monster/transport/
skylib_fluffos_v3/mudlib/obj/rings/
skylib_fluffos_v3/mudlib/obj/scabbards/
skylib_fluffos_v3/mudlib/obj/spells/
skylib_fluffos_v3/mudlib/obj/stationery/
skylib_fluffos_v3/mudlib/obj/stationery/envelopes/
skylib_fluffos_v3/mudlib/obj/toys/
skylib_fluffos_v3/mudlib/obj/vessels/
skylib_fluffos_v3/mudlib/obj/weapons/axes/
skylib_fluffos_v3/mudlib/obj/weapons/chains/
skylib_fluffos_v3/mudlib/obj/weapons/maces/BACKUPS/
skylib_fluffos_v3/mudlib/save/autodoc/
skylib_fluffos_v3/mudlib/save/book_handler/
skylib_fluffos_v3/mudlib/save/books/history/calarien/
skylib_fluffos_v3/mudlib/save/mail/
skylib_fluffos_v3/mudlib/save/new_soul/data/
skylib_fluffos_v3/mudlib/save/parcels/
skylib_fluffos_v3/mudlib/save/playerinfo/
skylib_fluffos_v3/mudlib/save/players/d/
skylib_fluffos_v3/mudlib/save/players/s/
skylib_fluffos_v3/mudlib/save/random_names/
skylib_fluffos_v3/mudlib/save/random_names/data/
skylib_fluffos_v3/mudlib/save/terrains/
skylib_fluffos_v3/mudlib/save/terrains/tutorial_desert/
skylib_fluffos_v3/mudlib/save/terrains/tutorial_grassy_field/
skylib_fluffos_v3/mudlib/save/terrains/tutorial_mountain/
skylib_fluffos_v3/mudlib/save/todo_lists/
skylib_fluffos_v3/mudlib/secure/
skylib_fluffos_v3/mudlib/secure/cmds/admin/
skylib_fluffos_v3/mudlib/secure/cmds/lord/
skylib_fluffos_v3/mudlib/secure/config/
skylib_fluffos_v3/mudlib/secure/handlers/autodoc/
skylib_fluffos_v3/mudlib/secure/handlers/intermud/
skylib_fluffos_v3/mudlib/secure/include/global/
skylib_fluffos_v3/mudlib/secure/save/
skylib_fluffos_v3/mudlib/secure/save/handlers/
skylib_fluffos_v3/mudlib/secure/std/
skylib_fluffos_v3/mudlib/secure/std/classes/
skylib_fluffos_v3/mudlib/secure/std/modules/
skylib_fluffos_v3/mudlib/std/creator/
skylib_fluffos_v3/mudlib/std/dom/
skylib_fluffos_v3/mudlib/std/effects/
skylib_fluffos_v3/mudlib/std/effects/external/
skylib_fluffos_v3/mudlib/std/effects/fighting/
skylib_fluffos_v3/mudlib/std/effects/magic/
skylib_fluffos_v3/mudlib/std/effects/magic/BACKUPS/
skylib_fluffos_v3/mudlib/std/effects/other/BACKUPS/
skylib_fluffos_v3/mudlib/std/effects/priest/
skylib_fluffos_v3/mudlib/std/effects/room/
skylib_fluffos_v3/mudlib/std/environ/
skylib_fluffos_v3/mudlib/std/guilds/
skylib_fluffos_v3/mudlib/std/guilds/old/
skylib_fluffos_v3/mudlib/std/languages/
skylib_fluffos_v3/mudlib/std/liquids/
skylib_fluffos_v3/mudlib/std/npc/
skylib_fluffos_v3/mudlib/std/npc/goals/
skylib_fluffos_v3/mudlib/std/npc/goals/basic/
skylib_fluffos_v3/mudlib/std/npc/goals/misc/
skylib_fluffos_v3/mudlib/std/npc/plans/
skylib_fluffos_v3/mudlib/std/npc/plans/basic/
skylib_fluffos_v3/mudlib/std/npc/types/
skylib_fluffos_v3/mudlib/std/npc/types/helper/
skylib_fluffos_v3/mudlib/std/npcs/
skylib_fluffos_v3/mudlib/std/outsides/
skylib_fluffos_v3/mudlib/std/races/shadows/
skylib_fluffos_v3/mudlib/std/room/basic/BACKUPS/
skylib_fluffos_v3/mudlib/std/room/basic/topography/
skylib_fluffos_v3/mudlib/std/room/controller/
skylib_fluffos_v3/mudlib/std/room/inherit/topography/
skylib_fluffos_v3/mudlib/std/room/topography/area/
skylib_fluffos_v3/mudlib/std/room/topography/iroom/
skylib_fluffos_v3/mudlib/std/room/topography/milestone/
skylib_fluffos_v3/mudlib/std/shadows/curses/
skylib_fluffos_v3/mudlib/std/shadows/disease/
skylib_fluffos_v3/mudlib/std/shadows/fighting/
skylib_fluffos_v3/mudlib/std/shadows/healing/
skylib_fluffos_v3/mudlib/std/shadows/magic/
skylib_fluffos_v3/mudlib/std/shadows/poison/
skylib_fluffos_v3/mudlib/std/shadows/room/
skylib_fluffos_v3/mudlib/std/shops/controllers/
skylib_fluffos_v3/mudlib/std/shops/objs/
skylib_fluffos_v3/mudlib/std/shops/player_shop/
skylib_fluffos_v3/mudlib/std/socket/
skylib_fluffos_v3/mudlib/std/soul/d/
skylib_fluffos_v3/mudlib/std/soul/e/
skylib_fluffos_v3/mudlib/std/soul/i/
skylib_fluffos_v3/mudlib/std/soul/j/
skylib_fluffos_v3/mudlib/std/soul/k/
skylib_fluffos_v3/mudlib/std/soul/l/
skylib_fluffos_v3/mudlib/std/soul/n/
skylib_fluffos_v3/mudlib/std/soul/o/
skylib_fluffos_v3/mudlib/std/soul/q/
skylib_fluffos_v3/mudlib/std/soul/r/
skylib_fluffos_v3/mudlib/std/soul/u/
skylib_fluffos_v3/mudlib/std/soul/v/
skylib_fluffos_v3/mudlib/std/soul/y/
skylib_fluffos_v3/mudlib/std/soul/z/
skylib_fluffos_v3/mudlib/std/stationery/
skylib_fluffos_v3/mudlib/w/
skylib_fluffos_v3/mudlib/w/default/
skylib_fluffos_v3/mudlib/w/default/armour/
skylib_fluffos_v3/mudlib/w/default/clothes/
skylib_fluffos_v3/mudlib/w/default/item/
skylib_fluffos_v3/mudlib/w/default/npc/
skylib_fluffos_v3/mudlib/w/default/room/
skylib_fluffos_v3/mudlib/w/default/weapon/
skylib_fluffos_v3/mudlib/www/
skylib_fluffos_v3/mudlib/www/java/
skylib_fluffos_v3/mudlib/www/secure/
skylib_fluffos_v3/mudlib/www/secure/lpc/advanced/
skylib_fluffos_v3/mudlib/www/secure/lpc/intermediate/
skylib_fluffos_v3/win32/
/**
 * Keeps track of all the bulletin board notes and related information.
 * @index bulletin
 * @author Pinkfish
 */

#include <board.h>

#define T_MIN                0
#define T_MAX                1
#define T_TIMEOUT            2
#define DEFAULT_MIN          10
#define DEFAULT_MAX          80
#define DEFAULT_TIMEOUT      14

#define WWW_POST_INTERVAL    86400
#define WWW_POST_MAX         5

#define ARCHIVE_DIR          "/save/oldboards/"
#define MAX_ARCHIVE_SIZE     1048576

#define BACKUPS_DIR          "/save/board_backups/"
#define NEWSRC_SAVE_DIR      "/save/newsrc/"

#define BACKUP_TIME_OUT      (7*24*60*60)
#define BOARD_HANDLE_VERSION 1

#define CACHE_SIZE           100
#define CACHE_TIMEOUT        1800
#define NEWSRC_SAVE_DELAY    300

class newsrc {
    int cached;
    int changed;
    mapping newsrc;
    string *kill;
    string *board_order;
}

class cached_mess {
    int last;
    string mess;
}

class posted_information {
    int time_first_posted;
    int count;
}

private int num;
private int board_version;
private mapping archives;
private mapping boards;
private mapping close;
private mapping priv;
private mapping security;
private mapping timeouts;

private mapping posted_stuff;

private nosave string *_allowed_objects;
private nosave mapping message_cache;
private nosave mapping _newsrc_cache;
private nosave int total_reads, cache_hits;
private nosave int _newsrc_reads, _newsrc_cache_hits;
private nosave mapping _idiots;

private void expire_boards();

protected void create() {
    int number, last;
    string line, *lines;

    num = 1;

    message_cache = ([ ]);
    _newsrc_cache = ([ ]);

    // These are the objects allowed to post...
    _allowed_objects = ({
        BOARD_HAND,
        BOARD_OBJ,
        "/obj/misc/board_mas",
        BOARD_WEB,
        "/www/secure/errors",
        "/handlers/club_handler",
        "/handlers/deity_handler",
        "/handlers/folder_handler",
        "/handlers/playtesters",
        "/handlers/error_tracker",
        "/handlers/twiki",
        "/cmds/player/news",
        "/cmds/player/apply",
        "/cmds/player/bug",
        "/cmds/player/typo",
        "/cmds/playtester/report",
        "/cmds/player/idea",
    });

    _idiots = (["no-one" : 1 ]);

    unguarded( (: cp, BOARD_FILE+".o", BACKUPS_DIR+"boards."+time() :) );

    lines = unguarded( (: get_dir, BACKUPS_DIR+"boards.*" :) );

    if( sizeof(lines) > 6 ) {
        last = time() - BACKUP_TIME_OUT;
        foreach( line in lines ) {
            sscanf( line, "boards.%d", number );
            if( number < last )
                unguarded( (: rm, BACKUPS_DIR+line :) );
        }
    }

    unguarded( (: restore_object, BOARD_FILE :) );

    if( !archives )
        archives = ([ ]);
    if( !boards )
        boards = ([ ]);
    if( !close )
        close = ([ ]);
    if( !priv )
        priv = ([ ]);
    if( !security )
        security = ([ ]);
    if( !timeouts )
        timeouts = ([ ]);
    if( !posted_stuff )
        posted_stuff = ([ ]);

    call_out( (: expire_boards :), 5 );

} /* create() */

/**
 * This method returns the posted_stuff mapping.
 * @return the posted stuff mapping
 */
mapping query_posted_stuff() { return posted_stuff; }

/**
 * This method returns the maximum amount of posts one can
 * make from the www boards.
 * @return the max amount of posts one can do from www boards
 */
int query_www_post_max() { return WWW_POST_MAX; }

/*
 * This is called when something is posted from the www boards,
 * it will increment the count and fiddle with stuff.
 */
void www_post_note( string name ) {
    class posted_information bing;

    if( file_name(PO) != BOARD_WEB )
        return;

    if( creatorp( name ) ) {
        if( !undefinedp(posted_stuff[name]) )
            map_delete( posted_stuff, name );
        return;
    }

    if( !undefinedp(posted_stuff[name]) )
        bing = (class posted_information)posted_stuff[name];
    else
        bing = new( class posted_information );

    if( time() > bing->time_first_posted + WWW_POST_INTERVAL ) {
        bing->time_first_posted = time();
        bing->count = 0;
    }

    bing->count++;
    posted_stuff[name] = bing;

} /* www_post_note() */

/*
 * This is called when a note is eaten from the www boards,
 * it will decrement the allowed number of posts.
 */
void www_delete_note( string name ) {
    class posted_information bing;

    if( file_name(PO) != BOARD_WEB )
        return;

    if( !undefinedp(posted_stuff[name]) ) {
        bing = (class posted_information)posted_stuff[name];
        if( time() > bing->time_first_posted + WWW_POST_INTERVAL ) {
            map_delete( posted_stuff, name );
        } else {
            bing->count--;
            posted_stuff[name] = bing;
        }
    }

} /* www_delete_note() */

/**
 * This method checks whether or not a player can post on the www boards.
 * @param name the player to test
 * @return 1 if they can post, 0 if they have used up their posts
 * for the day or similar
 */
int can_post_www_note( string name ) {
    class posted_information bing;

    if( file_name(PO) != BOARD_WEB )
        return 0;

    // No additional checks for cres.
    if( creatorp(name) )
        return 1;

    // Check to see if they are less than 6 hours old.
    if( PLAYER_H->test_age(name) > - 6 * 60 * 60 )
        return 0;

    // Inactive players can't post.
    if( !PLAYER_H->test_active(name) )
        return 0;

    if( undefinedp(posted_stuff[name]) )
        return 1;

    bing = (class posted_information)posted_stuff[name];

    if( time() > bing->time_first_posted + WWW_POST_INTERVAL ) {
        map_delete( posted_stuff, name );
        return 1;
    }

    // Up to N posts - number of days since last login.
    // test_last returns a -ve number.
    if( bing->count < ( WWW_POST_MAX - ( time() -
        PLAYER_H->test_last(name) ) / 86400 ) )
        return 1;

    return 0;

} /* can_post_www_note() */

/**
 * This method returns the file name of the message to use in retreiving
 * the message.
 * @param board the board to get the message from
 * @param num the number of the message to download
 */
private string get_filename( string board, int num ) {
    string fixed_board;

    fixed_board = replace( board, ({" ", "_", "'", ""}) );

    // If a file exists in the old folder then return its name.
    if( file_size( BOARD_DIR + num ) != -1 )
        return BOARD_DIR + num;

    // If a sub-directory doesn't exist for this board then make one.
    if( file_size( BOARD_DIR + fixed_board ) != -2 )
        unguarded( (: mkdir, BOARD_DIR + fixed_board :) );

    // Return the filename in the appropriate subdirectory.
    return BOARD_DIR + fixed_board + "/" + num;

} /* get_filename() */

/**
 * Returns the names of all the boards.
 * @return the names of all the boards
 */
string *query_boards() { return asort( keys(boards) ); }

/**
 * This method tests to see if the board exists.
 * @param board the name of the board to check
 * @return 1 if the board exists, 0 if it does not
 */
int is_board( string board ) {
    if( boards[board] )
        return 1;
    return 0;
} /* is_board() */

/**
 * This method sets what board 'closed' posts move to.
 * @return 1 if the successful, 0 if it not
 */
int set_close( string board_from, string board_to ) {
    if( member_array( board_from, keys(boards) ) == 1 )
        return 0;

    if( member_array( board_to, keys(boards) ) == 1 )
        return 0;

    close[board_from] = board_to;
    return 1;

} /* set_close() */

/**
 * Returns the board closed posts move to.
 * @return the board closed posts move to
 */
string query_close( string board_from ) {
    if( member_array( board_from, keys(boards) ) == -1 )
        return 0;
    return close[board_from];
} /* query_close() */

/**
 * Returns a list of board that have closed set to.
 * @return a list of board that can be closed
 */
mapping query_close_boards() { return copy(close); }

/**
 * Check to see if read access is allowed.
 * @param board the board to check
 * @param previous the previous object
 * @param name the name of the person doing stuff
 * @return 1 if it is allowed, 0 if not
 */
int test_can_read( string board, object previous, string name ) {
    int bit;

    // For the moment we will just log all bad access...
    if( member_array( base_name(previous), _allowed_objects ) == -1 &&
        board != "announcements") {
#ifdef DEBUG
        log_file("BAD_BOARD", ctime(time())+" (read): ["+board+"] "+
            base_name(previous) + sprintf(" (%O)\n", TP->query_name() ) );
#endif
        return 0;
    }

    bit = priv[board] & B_PRIV_TYPE_MASK;

    if( bit == B_PRIV_ACCESS_RESTRICTED )
        return lordp(name) || member_array( name, security[board] ) != -1;

    if( bit == B_PRIV_ACCESS_ADMIN )
        return adminp(name) || member_array( name, security[board] ) != -1;

    if( bit == B_PRIV_ACCESS_RESTRICTED_METHOD ) {
        if( lordp(name) )
            return 1;
        // This will now need to check the method and function to call.
        if( sizeof(security[board]) == 2 )
            return call_other( security[board][1], security[board][0],
                   B_ACCESS_READ, board, previous, name );
        return 0;
    }

    return 1;

} /* test_can_read() */

/**
 * Check to see if write access is allowed.
 * @param board the board to check
 * @param previous the previous object
 * @param name the name of the person doing stuff
 * @return 1 if it is allowed, 0 if not
 */
int test_can_write( string board, object previous, string name ) {
    int bit;

    if( _idiots[name] )
        return 0;

    if( board == "applications")
        return 1;

    if( member_array( base_name(previous), _allowed_objects) == -1 ) {
#ifdef DEBUG
        log_file("BAD_BOARD", ctime(time())+" (write): "+base_name(previous)+
            sprintf(" (%O)\n", TP ) );
#endif
        return 0;
    }

    bit = priv[board] & B_PRIV_TYPE_MASK;

    if( bit == B_PRIV_ACCESS_RESTRICTED_FILE )
        return member_array( base_name(previous), security[board] ) != -1;

    if( bit == B_PRIV_ACCESS_ADMIN )
        return adminp(name) || member_array( name, security[board] ) != -1;

    if( bit == B_PRIV_ACCESS_RESTRICTED_METHOD ) {
        if( lordp(name) )
            return 1;
        // This will now need to check the method and function to call.
        if( sizeof(security[board]) == 2 )
            return call_other( security[board][1], security[board][0],
                   B_ACCESS_WRITE, board, previous, name );
        return 0;
    }

    if( bit == B_PRIV_ACCESS_RESTRICTED || bit == B_PRIV_READ_ONLY )
        return lordp(name) || member_array( name, security[board] ) != -1;

    return 1;

} /* test_can_write() */

/**
 * Check to see if delete is allowed.
 * @param board the board to check
 * @param previous the previous object
 * @param name the name of the person doing stuff
 * @return 1 if it is allowed, 0 if not
 */
int test_can_delete( string board, object previous, string name ) {
    int bit;

    if( member_array( base_name(previous), _allowed_objects ) == -1 ) {
#ifdef DEBUG
        log_file("BAD_BOARD", ctime(time())+" (write): "+base_name(previous)+
            sprintf(" (%O)\n", TP ) );
#endif
        return 0;
    }

    bit = priv[board] & B_PRIV_TYPE_MASK;

    if( bit == B_PRIV_ACCESS_RESTRICTED_FILE )
        return lordp(name) ||
               member_array( base_name(previous), security[board] ) != -1;

    if( bit == B_PRIV_ACCESS_ADMIN )
        return adminp(name) || member_array( name, security[board] ) != -1;

    if( bit == B_PRIV_ACCESS_RESTRICTED || bit == B_PRIV_READ_ONLY )
        return lordp(name) || member_array(name, security[board]) != -1;

    if( bit == B_PRIV_ACCESS_RESTRICTED_METHOD ) {
        if( lordp(name) )
            return 1;
        // This will now need to check the method and function to call.
        if( sizeof(security[board]) == 2 ) {
            return call_other( security[board][1], security[board][0],
                   B_ACCESS_DELETE, board, previous, name );
        }
    }

    return lordp(name);

} /* test_can_delete() */

/**
 * Get the subjects for the specifed board.  The subjects are
 * returns in a special array format.  See the include file for the
 * defines to get at the members of the array.
 * @see /include/board.h
 * @param name the board name to lookup
 * @return the subject array
 */
mixed get_subjects( string name, string person ) {
    if( member_array( file_name(PO), ({ BOARD_WEB,
        "/d/playtesters/handlers/login_calls" }) ) == -1 ) {
        if( TP )
            person = (string)TP->query_name();
        else
            person = "unknown";
    }

    if( file_name(PO) == "/d/playtesters/handlers/login_calls" ) {
        return boards[name];
    }

    if( !test_can_read( name, PO, person ) || !boards[name] )
        return ({ });

    return boards[name];

} /* get_subjects() */

/**
 * Get the text of a specific message.  This will look up the
 * text on a board with the given number and return that to the
 * caller.  The number is the offset into the subject array in
 * which to get the message from.
 * @param board the board name to get the message from
 * @param num the message number to use
 * @param name the person doing the reading
 * @return the message or 0 if it failed
 */
string get_message( string board, int num, string name ) {
    if( file_name(PO) != BOARD_WEB ) {
        if( file_name(PO)[0..14] != BOARD_OBJ )
            name = "unknown";
        else
            name = (string)TP->query_name();
    }

    if( !test_can_read( board, PO, name ) )
        return 0;

    if( num < 0 || num >= sizeof(boards[board]) )
        return 0;

    if( !message_cache )
        message_cache = ([ ]);

    total_reads++;

    // If it's not in the cache, put it in there.
    if( !message_cache[boards[board][num][B_NUM]] ) {
        name = get_filename( board, boards[board][num][B_NUM] );

        if( file_size(name) <= 0 )
            return 0;

        if( find_call_out("clean_cache") == -1 &&
            sizeof(message_cache) > CACHE_SIZE )
            call_out("clean_cache", 30 );

        message_cache[boards[board][num][B_NUM]] = new( class cached_mess,
            last : time(), mess : unguarded( (: read_file, name :) ) );

    } else {
        message_cache[boards[board][num][B_NUM]]->last = time();
        cache_hits++;
    }

    return message_cache[boards[board][num][B_NUM]]->mess;

} /* get_message() */

private int sort_by_last( int one, int two ) {
    if( message_cache[one]->last < message_cache[two]->last )
        return 1;
    if( message_cache[one]->last > message_cache[two]->last )
        return -1;
    return 0;
} /* sort_by_last() */

/**
 * @ignore yes
 * Prevent the cache from getting too big.
 */
void clean_cache() {
    string name;
    int i, *list;

    list = sort_array( keys(message_cache),
        (: sort_by_last( $1, $2 ) :) )[CACHE_SIZE..];

    foreach( i in list )
        map_delete( message_cache, i );

    foreach( name in keys(_newsrc_cache) )
        if( !_newsrc_cache[name]->changed &&
            _newsrc_cache[name]->cached < time() - CACHE_TIMEOUT )
            map_delete( _newsrc_cache, name );

} /* clean_cache() */

private void save_me() {
    unguarded( (: save_object, BOARD_FILE :) );
} /* save_me() */

/**
 * Return the archive file location for the board.
 * @param board the board to get the archive location for
 * @return the archive file location, 0 on failure
 */
string query_archive( string board ) {
    if( !boards[board] )
        return 0;

    if( undefinedp(archives[board]) )
        return ARCHIVE_DIR+board;

    return archives[board];

} /* query_archive() */

/** @ignore yes */
int zap_message( string board, int off ) {
    int num;
    string nam, archive;

    if( off < 0 || off >= sizeof(boards[board]) )
        return 0;

    num = boards[board][off][B_NUM];
    nam = get_filename( board, num );
    archive = query_archive(board);

    if( archive ) {
        mixed stuff;

        stuff = boards[board][off];

        // When we archive it, strip the colours out.
        unguarded( (: write_file, archive, sprintf("\n----\n"
            "Note #%d by %s posted at %s\nTitle: '%s'\n\n", off,
            stuff[B_NAME], ctime(stuff[B_TIME]), stuff[B_SUBJECT]) +
            strip_colours( unguarded( (: read_file, nam :) ) ) :) );

        if( unguarded( (: file_size, archive :) ) > MAX_ARCHIVE_SIZE )
            unguarded( (: rename, archive, archive+"."+time() :) );
    }

    boards[board] = delete( boards[board], off, 1 );
    unguarded( (: rm, nam :) );

    save_me();
    return 1;

} /* zap_message() */

/**
 * Adds a new message onto the board.  This call can only be done from
 * verified source, like the bulletin oard objects themselves.  The
 * number used as a reply to should be the message number itself, not
 * the logical index.  If the reply_to is 0 then it is not
 * replying to anything at all.
 * @param board the board to add the message to
 * @param cap_name the name ofthe person posting
 * @param subj the subject of the message
 * @param body the main section of the text
 * @param reply_to the note the message is replying to
 * @return the note number, or 0 on failure
 */
int add_message( string board, string cap_name, string subj,
                 string body, int reply_to, class reply_type bing ) {
    int irp, index;
    string fname, name, from_mess, mail_to;
    class reply_type reply;

    name = lower_case(cap_name);

    // Check to see what should happen to the reply.
    if( reply_to ) {
        for( index = 0; index < sizeof(boards[board]); index++ ) {
            if( boards[board][index][B_NUM] == reply_to &&
                boards[board][index][B_REPLY_TYPE] ) {
                // Do something special.
                reply = (class reply_type)boards[board][index][B_REPLY_TYPE];

                if( reply->type == B_REPLY_POSTER )
                    mail_to = boards[board][index][B_NAME];
                else if( reply->type == B_REPLY_NAMED )
                    mail_to = reply->data;

                // Send mail instead.
                if( mail_to ) {
                    MAIL_H->do_mail_message( mail_to, name, subj, "", body );
                    return 1;
                }
            }
        }
    }

    if( !test_can_write( board, PO, name ) )
        return 0;

    if( !boards[board] )
        return 0;
    else
        boards[board] += ({ ({ subj, cap_name, num++, time(), bing, reply_to }) });

    if( file_name(PO)[1..4] == "www/")
        from_mess = " [Web post]";
    else
        from_mess = "";

    fname = get_filename( board, num-1 );
    unguarded( (: rm, fname :) );
    fname = get_filename( board, num-1 );

    unguarded( (: write_file, fname, body :) );
    save_me();

    // Add this new message to the cache.
    message_cache[num-1] = new( class cached_mess, last : time(), mess : body );

    if( timeouts[board] && timeouts[board][T_MAX] &&
        sizeof(boards[board]) > timeouts[board][T_MAX] ) {
        // Should be at most one message we eat...
        while( sizeof(boards[board]) > timeouts[board][T_MAX] ) {
            zap_message( board, 0 );
            irp++;
        }

        if( ( priv[board] & B_PRIV_TYPE_MASK ) == B_PRIV_ACCESS_RESTRICTED_METHOD ) {
            // This will now need to check the method and function to call.
            if( sizeof(security[board]) == 2 )
                call_other( security[board][1], security[board][0],
                    B_ACCESS_INFORM, board, 0, cap_name, irp );
        }

        if( !( priv[board] & B_PRIV_NO_INFORM ) ) {
            event( filter( users(), (: test_can_read( $2, PO,
                $1->query_name() ) :), board ), "inform", sprintf(
                "%s posts a message to %s and %d message%s in sympathy%s",
                cap_name, board, irp, ( irp > 1 ? "s explode" : " explodes"),
                from_mess ), "message");
        }
    } else {
        if( ( priv[board] & B_PRIV_TYPE_MASK ) == B_PRIV_ACCESS_RESTRICTED_METHOD ) {
            // This will now need to check the method and function to call.
            if( sizeof(security[board]) == 2 )
                call_other( security[board][1], security[board][0],
                    B_ACCESS_INFORM, board, 0, cap_name, 0 );
        }
        if( !( priv[board] & B_PRIV_NO_INFORM ) ) {
            event( filter( users(), (: test_can_read( $2, PO,
                $1->query_name() ) :), board ),"inform", sprintf(
                "%s posts a message to %s%s", cap_name, board, from_mess ),
                "message");
        }
    }

    return num-1;

} /* add_message() */

/**
 * This method creates a new board.
 * @param board the name of the board to create
 * @param privileges is this board only allowed priviliged access?
 * @param person the person to add into the security array initially
 * @return 0 on a failure, 1 on success
 */
int create_board( string board, int privileges, string person ) {

    if( file_name(PO) != "/secure/cmds/admin/boardtool" )
        return 0;

    if( boards[board] )
        return 0;

    if( !person )
        person = TP->query_name();

    boards[board] = ({ });
    security[board] = ({ person });

    if( privileges )
        priv[board] = privileges;

    save_me();
    return 1;

} /* create_board() */

/**
 * Adds a member into the security array for a board.  This allows
 * certain people to read boards they may not normaly have
 * access to.
 * @param board the board to change the access on
 * @param name the name of the person to add to the array
 * @return 0 on failure, 1 on success
 */
int add_allowed( string board, string name ) {
    string nam;
    int board_type;

    board_type = priv[board] & B_PRIV_TYPE_MASK;

    if( sscanf( file_name(PO), "/obj/misc/board%s", nam ) != 1 )
        return 0;

    nam = (string)TP->query_name();

    if( !test_can_write( board, PO, nam ) ||
        ( board_type != B_PRIV_ACCESS_RESTRICTED &&
        board_type != B_PRIV_READ_ONLY &&
        board_type != B_PRIV_ACCESS_RESTRICTED_FILE ) )
        return 0;

    if( !PLAYER_H->test_user(name) )
        return 0;

    security[board] += ({ name });
    save_me();

    printf("Added %s to the security array for %s.\n", name, board );
    return 1;

} /* add_allowed() */

/**
 * Remove someone from the allowed array of the board.
 * @param board the board to remove the person from
 * @param name the name of the person to remove
 * @return 0 on nfailure and 1 on success
 */
int remove_allowed( string board, string name ) {
    string nam;
    int i, board_type;

    if( sscanf( file_name(PO), "/obj/misc/board%s", nam ) != 1 )
        return 0;

    nam = geteuid(PO);
    board_type = priv[board] & B_PRIV_TYPE_MASK;

    if( !test_can_write( board, PO, nam ) ||
        ( board_type != B_PRIV_ACCESS_RESTRICTED &&
        board_type != B_PRIV_READ_ONLY &&
        board_type != B_PRIV_ACCESS_RESTRICTED_FILE ) )
        return 0;

    security[board] = delete( security[board], i, 1 );
    save_me();

    printf("Removed %s from the security array for %s.\n", name, board );
    return 1;

} /* add_allowed() */

/**
 * This method sets the method to call to check for allowed postings
 * to a board setup as an method controlled post board.
 * @param board the name of the board to setup the method for
 * @param method the method to call on the object
 * @param name the object to call the method on
 * @return 0 if the method failed, 1 if it was successful
 */
int set_method_access_call( string board, string method, string name ) {
    string pl;

    if( !boards[board] ||
        ( priv[board] & B_PRIV_TYPE_MASK ) != B_PRIV_ACCESS_RESTRICTED_METHOD )
        return 0;

    if( TP )
        pl = (string)TP->query_name();
    else
        pl = "unknown";

    if( lordp(pl) || file_name(PO) == CLUB_HANDLER ) {
        security[board] = ({ method, name });
        save_me();
        return 1;
    }

    return 0;

} /* set_method_access_call() */

/**
 * This method changes the type of the board to be a method access call
 * access restriction, instead of whatever it had before.
 * @param board the name of the board to control the access for
 */
int force_board_method_access_restricted( string board ) {
    string pl;

    if( !boards[board] )
        return 0;

    if( TP )
        pl = (string)TP->query_name();
    else
        pl = "unknown";

    if( lordp(pl) || file_name(PO) == CLUB_HANDLER ) {
        priv[board] = ( priv[board] & ~B_PRIV_TYPE_MASK ) |
                        B_PRIV_ACCESS_RESTRICTED_METHOD;
        save_me();
        return 1;
    }

    return 0;

} /* force_board_method_access_restricted() */

/**
 * Check to see if the named person can delete the message.
 * @param pname the player name
 * @param board the board name
 * @param off the offset to delete
 * @see delete_message()
 */
int can_delete_message( string board, int off, string pname ) {
    if( !boards[board] )
        return 0;

    if( off >= sizeof(boards[board]) )
        return 0;

    if( !test_can_delete( board, PO, pname ) &&
        ( lower_case(boards[board][off][B_NAME]) != lower_case(pname) ) )
        return 0;

    return 1;

} /* can_delete_message() */

/**
 * Remove a message from a board.  The offset is the offset into the
 * subjects array.
 * @param board the board to remove the message from
 * @param off the offset to delete
 * @param override_name used by the web boards
 * @return 0 on failure and 1 on success
 */
int delete_message( string board, int off, string override_name ) {
    string nam;

    nam = ( file_name(PO) == BOARD_WEB ?
            override_name : TP->query_name() );

    if( !can_delete_message( board, off, nam ) )
        return 0;

    return zap_message( board, off );

} /* delete_message() */

/**
 * Returns the security array for the given board.
 * @param board the board to get the security array for
 * @return the security array
 */
string *query_security( string board ) {
    string *str;

    str = security[board];

    if( !str )
        return str;

    return copy(str);

} /* query_security() */

/**
 * Complete erase a board.
 * @param board the board to delete
 * @return 0 on failure and 1 on success
 */
int delete_board( string board ) {
    string nam;

    if( file_name(PO) != "/secure/cmds/admin/boardtool")
        return 0;

    if( !boards[board] )
        return 0;

    nam = geteuid(PO);

    if( !lordp(nam) || file_name(PO) == CLUB_HANDLER )
        return 0;

    while( sizeof(boards[board]) ) {
        if( !zap_message( board, 0 ) )
            return 0;
    }

    map_delete( boards, board );
    map_delete( security, board );
    map_delete( priv, board );
    map_delete( archives, board );
    map_delete( timeouts, board );

    board = BOARD_DIR + replace( board, ({" ", "_", "'", ""}) ) + "/";

    // Delete its directory as well if its empty.
    if( dir_exists(board) && !sizeof( get_dir(board) ) )
        unguarded( (: rmdir, board :) );

    save_me();
    return 1;

} /* delete_board() */

/**
 * This method returns the list of www boards for the
 * specified player name.
 * @param name the name of the player to get the board list for
 * @return the list of www boards for the player
 */
string *list_of_www_boards( string name ) {
    string *www_boards, board;

    www_boards = ({ });

    foreach( board in keys(boards) ) {
        if( !boards[board] )
            continue;

        if( priv[board] ) {
            int bit;

            bit = priv[board] & B_PRIV_TYPE_MASK;

            if( bit == B_PRIV_ACCESS_RESTRICTED && !lordp(name) &&
                member_array( name, security[board] ) == -1 )
                continue;

            if( bit == B_PRIV_ACCESS_ADMIN && !adminp(name) &&
                member_array( name, security[board] ) == -1 )
                continue;

            if( bit == B_PRIV_ACCESS_RESTRICTED_METHOD && !lordp(name) &&
                !( sizeof(security[board]) == 2 && call_other(
                security[board][1], security[board][0], B_ACCESS_READ,
                board, PO, name ) ) )
                continue;
        }

        www_boards += ({ board });

    }

    return www_boards;

} /* list_of_www_boards() */

/**
 * Change the time before a message automatic gets deleted off a
 * board.
 * @param board the name of the board to set the timeout for
 * @param timeout the timeout (in seconds)
 * @return 0 on failure and 1 on success
 */
int set_timeout( string board, int timeout ) {
    string nam;

    if( !boards[board] )
        return 0;

    if( board == "bugs")
        return 0;

    nam = geteuid(PO);

    if( !test_can_write( board, PO, nam ) )
        return 0;

    if( !timeouts[board] ) {
        timeouts[board] = ({ DEFAULT_MIN, DEFAULT_MAX, timeout });
        return 1;
    }

    timeouts[board][T_TIMEOUT] = timeout;
    save_me();

    printf("Set the automagic timeout to %d days for %s.\n", timeout, board );
    return 1;

} /* set_timeout() */

/**
 * Sets the minimum number of message to keep on a board.
 * If there is less than this number then they will not be auto deleted.
 * @param board the board to set the minimum for
 * @param min the number of message to keep
 * @return 0 on failure and 1 on success
 */
int set_minimum( string board, int min ) {
    string nam;

    if( !boards[board] )
        return 0;

    nam = geteuid(PO);

    if( !test_can_write( board, PO, nam ) )
        return 0;

    if( !timeouts[board] ) {
        timeouts[board] = ({ min, DEFAULT_MAX, DEFAULT_TIMEOUT });
        return 1;
    }

    timeouts[board][T_MIN] = min;
    save_me();

    printf("Set the minimum number of messages to %d for %s.\n", min, board );
    return 1;

} /* set_minimum() */

/**
 * Set the maximum number of message before they start being auto deleted
 * when someone posts to the board.
 * @param board the board to set the maximum for
 * @param max the maximum number of messages
 * @return 0 if it failed and 1 on success
 */
int set_maximum( string board, int max ) {
    string nam;

    if( !boards[board] )
        return 0;

    nam = geteuid(PO);

    if( !test_can_write( board, PO, nam ) )
        return 0;

    if( !timeouts[board] ) {
        timeouts[board] = ({ DEFAULT_MIN, max, DEFAULT_TIMEOUT });
        return 1;
    }

    timeouts[board][T_MAX] = max;
    save_me();

    printf("Set the maximum number of messages to %d for %s.\n", max, board );
    return 1;

} /* set_maximum() */

/**
 * Set the archive file location.  This is where all deleted messages
 * wil be stored.
 * @param board the board to set the archive for
 * @param file the file name to set it to
 * @return 0 on failure and 1 on success
 */
int set_archive( string board, string file ) {
    string nam;

    if( !boards[board] )
        return 0;

    nam = geteuid(PO);

    if( !test_can_write( board, PO, nam ) )
        return 0;

    if( !file || file == "" )
        map_delete( archives, board );
    else
        archives[board] = file;
    save_me();

    printf("Set the archive file to %s for %s.\n", file, board );
    return 1;

} /* set_archive() */

/**
 * Return the timeout time of the board.
 * @param board the board to get the timeout for
 * @return the timeout in seconds
 * @see set_timeout()
 */
int query_timeout( string board ) {
    if( !timeouts[board] )
        return 0;
    return timeouts[board][T_TIMEOUT];
} /* query_timeout() */

/**
 * Return the minimum number of message allowed on the board.
 * @param board the board to get the minimum numbr of message for
 * @return 0 on failure, the minimum number of messages on success
 */
int query_minimum( string board ) {
    if( !timeouts[board] )
        return 0;
    return timeouts[board][T_MIN];
} /* query_minimum() */

/**
 * Return the maximum number of message allowed on the board.
 * @param board the board to get the maximum numbr of message for
 * @return 0 on failure, the maximum number of messages on success
 */
int query_maximum( string board ) {
    if( !timeouts[board] )
        return 0;
    return timeouts[board][T_MAX];
} /* query_maximum() */

/**
 * This method checks to see if the board is in restricted access mode.
 * @param board the name of the board to check
 * @return 1 if it is, 0 if it is not
 */
int query_restricted_access( string board ) {
    return (priv[board] & B_PRIV_TYPE_MASK) == B_PRIV_ACCESS_RESTRICTED;
} /* query_restricted_access() */

/**
 * This method checks to see if the board is in restricted access file
 * mode.
 * @param board the name of the board to check
 * @return 1 if it is, 0 if it is not
 */
int query_restricted_access_file( string board ) {
    return (priv[board] & B_PRIV_TYPE_MASK) == B_PRIV_ACCESS_RESTRICTED_FILE;
} /* query_restricted_access_file() */

/**
 * This method checks to see if the board is in a read only mode.
 * @param board the name of the board to check
 * @return 1 if it is read only, 0 if not
 */
int query_read_only( string board ) {
    return (priv[board] & B_PRIV_TYPE_MASK) == B_PRIV_READ_ONLY;
} /* query_read_only() */

/**
 * This method checks to see if the board is in a no inform mode.
 * @param board the name of the board to check
 * @return 1 if it is no inform, 0 if not
 */
int query_no_inform( string board ) {
    return priv[board] & B_PRIV_NO_INFORM;
} /* query_no_inform() */

/**
 * This method returns the current privilege level of the board in
 * question.  This should be used for testing only.
 * @return the current privilege level
 */
int query_privilage( string board ) { return priv[board]; }

/**
 * This method runs through all the messages and expires any which are
 * too old.
 */
private void expire_boards() {
    string nam;
    int tim, num, *val;

    foreach( nam, val in timeouts ) {
        if( nam == "bugs")
            continue;
        num = 0;
        if( ( tim = val[T_TIMEOUT] ) &&
            sizeof(boards[nam]) > val[T_MIN] &&
            boards[nam][0][B_TIME]+(tim*(24*60*60)) < time() ) {
            // Got to delete some...
            while( sizeof(boards[nam]) > val[T_MIN] &&
                boards[nam][0][B_TIME]+(tim*24*60*60) < time() ) {
                zap_message( nam, 0 );
                num++;
            }
            event( filter( users(), (: test_can_read( $2, PO,
                $1->query_name() ) :), nam ), "inform",
                sprintf("Board handler removes %d message"+
                ( num > 1 ? "s" : "")+" from %s", num, nam ), "message");
        }
    }

    call_out( (: expire_boards :), 60*60 );

} /* expire_boards() */

/**
 * The current max board number.
 * @return the current max board number
 */
int query_num() { return num; }

/** @ignore yes */
int load_newsrc( string player ) {
    string fname, board;

    if( !PLAYER_H->test_user( player ) )
        return 0;

    _newsrc_reads++;

    if( _newsrc_cache[player] ) {
        _newsrc_cache_hits++;
        return 1;
    }

    fname = NEWSRC_SAVE_DIR+player[0..0]+"/"+player+".o";

    if( unguarded( (: file_size( $(fname) ) :) ) > 0 ) {
        _newsrc_cache[player] =
            unguarded( (: restore_variable( read_file($(fname)) ) :) );
        if( arrayp(_newsrc_cache[player]->newsrc) ) {
            _newsrc_cache[player]->newsrc = ([ ]);
            if( find_call_out("flush_newsrc") == -1 )
                call_out("flush_newsrc", NEWSRC_SAVE_DELAY );
        }
    } else {
        _newsrc_cache[player] = new( class newsrc, cached : time(),
            kill : ({ }), newsrc : ([ ]), board_order : ({ }) );
        if( PLAYER_H->test_property( player, NEWS_RC ) )
            _newsrc_cache[player]->newsrc =
                PLAYER_H->test_property( player, NEWS_RC );
        if( PLAYER_H->test_property( player, BOARD_ORDER ) )
            _newsrc_cache[player]->board_order =
                PLAYER_H->test_property( player, BOARD_ORDER );

        foreach( board in keys(boards) )
            if( PLAYER_H->test_property( player, "news_kill_"+board ) )
                _newsrc_cache[player]->kill += ({ board });

        _newsrc_cache[player]->changed = 1;

        if( find_call_out("flush_newsrc") == -1 )
            call_out("flush_newsrc", NEWSRC_SAVE_DELAY );
    }

    if( sizeof( _newsrc_cache[player]->board_order ) ) {
        string *a;

        a = filter( _newsrc_cache[player]->board_order, (: !is_board($1) :) );

        if( sizeof(a) ) {
            _newsrc_cache[player]->board_order -= a;
            _newsrc_cache[player]->changed = 1;
        }
    }

    return 1;

} /* load_newsrc() */

/** @ignore yes */
void flush_newsrc() {
    string fname, player, board;
    object ob;

    foreach( player in keys(_newsrc_cache) ) {
        if( !_newsrc_cache[player]->changed )
            continue;

        fname = NEWSRC_SAVE_DIR+player[0..0]+"/"+player+".o";

        // Save their file.
        if( unguarded( (: file_size($(fname)) :) ) > 0 ) {
            unguarded( (: write_file($(fname),
                save_variable(_newsrc_cache[$(player)]), 1 ) :) );
        } else if( ob = find_player(player) ) {
            // If they're around and don't have a file then write a new one
            // and clear their properties.
            unguarded( (: write_file($(fname),
                  save_variable(_newsrc_cache[$(player)]), 1 ) :) );
            ob->remove_property(NEWS_RC);
            ob->remove_property(BOARD_ORDER);
            foreach( board in keys(boards) )
                ob->remove_property("news_kill_"+lower_case(board) );
        } else {
            // They're not around and they don't have a file so just
            // update their properties.
            // I don't like this.
            // - Sandoz.
            LOGIN_OBJ->special_add_property( player, NEWS_RC,
                _newsrc_cache[player]->newsrc );
        }
        _newsrc_cache[player]->changed = 0;
    }
} /* flush_newsrc() */

/**
 * This method returns a player's newsrc.
 * @param string the name of the player
 * @return their newsrc mapping
 */
mapping query_newsrc( string player ) {
    if( !load_newsrc(player) )
        return ([ ]);

    _newsrc_cache[player]->cached = time();

    return _newsrc_cache[player]->newsrc;

} /* query_newsrc() */

/**
 * This method sets a player's newsrc.
 * @param string the name of the player
 * @param newsrc their new newsrc
 * @return 1 for success, 0 for failure
 */
int set_newsrc( string player, mapping newsrc ) {
    if( !load_newsrc(player) )
        return 0;

    _newsrc_cache[player]->newsrc = (mapping)newsrc;
    _newsrc_cache[player]->cached = time();
    _newsrc_cache[player]->changed = 1;

    if( find_call_out("flush_newsrc") == -1 )
        call_out("flush_newsrc", NEWSRC_SAVE_DELAY );

    return 1;

} /* set_newsrc() */

/**
 * Find out if a given board is in a player's killfile.
 * @param string the name of the player
 * @param string the name of the board (in lowercase)
 * @return 1 if it is, 0 if it isn't
 */
int query_killfile( string player, string board ) {
    if( !load_newsrc( player ) )
        return 0;

    _newsrc_cache[player]->cached = time();

    return member_array( board, _newsrc_cache[player]->kill ) != -1;

} /* query_killfile() */

/**
 * Add a board to someone's killfile.
 * @param string the name of the player
 * @param string the name of the board (in lowercase)
 * @return 1 for success, 0 for failure
 */
int set_killfile( string player, string board ) {
    if( !load_newsrc(player) )
        return 0;

    if( member_array( board, _newsrc_cache[player]->kill ) != -1 )
        return 1;

    _newsrc_cache[player]->kill += ({ board });
    _newsrc_cache[player]->changed = 1;
    _newsrc_cache[player]->cached = time();

    if( find_call_out("flush_newsrc") == -1 )
        call_out("flush_newsrc", NEWSRC_SAVE_DELAY );

    return 1;

} /* set_killfile() */

/**
 * This method removed a player's killfile.
 * @param player the player whose killfile to remove
 * @param the board to remove the killfile for
 * @return 1 upon success, 0 upon failure
 */
int remove_killfile( string player, string board ) {
    if( !load_newsrc(player) )
        return 0;

    if( member_array( board, _newsrc_cache[player]->kill ) == -1 )
        return 1;

    _newsrc_cache[player]->kill -= ({ board });
    _newsrc_cache[player]->changed = 1;
    _newsrc_cache[player]->cached = time();

    if( find_call_out("flush_newsrc") == -1 )
        call_out("flush_newsrc", NEWSRC_SAVE_DELAY );

    return 1;

} /* remove_killfile() */

/**
 * This method returns a player's killfile list.
 * @param string the players name
 * @return the array of boards in their killfile
 */
string *list_killfile( string player ) {
    if( !load_newsrc(player) )
        return ({ });

    return _newsrc_cache[player]->kill;

} /* list_killfile() */

/**
 * This method returns a player's board order.
 * @param string the players name
 * @return the list of boards, in order
 */
string *query_board_order( string player ) {
    if( !load_newsrc(player) )
        return ({ });

    _newsrc_cache[player]->cached = time();

    if( !_newsrc_cache[player]->board_order )
        return ({ });

    return _newsrc_cache[player]->board_order;

} /* query_board_order() */

/**
 * This method returns the board order for a given player.
 * @param string a players name
 * @param new_order the list of boards
 * @return 1 for success, 0 for failure
 */
int set_board_order( string player, string *new_order ) {
    if( !load_newsrc(player) )
        return 0;

    _newsrc_cache[player]->board_order = new_order;
    _newsrc_cache[player]->changed = 1;
    _newsrc_cache[player]->cached = time();

    if( find_call_out("flush_newsrc") == -1 )
        call_out("flush_newsrc", NEWSRC_SAVE_DELAY );

    return 1;

} /* set_board_order() */

/**
 * @ignore yes
 * Prevents us from being shadowed.
 */
int query_prevent_shadow() { return 1; }

/** @ignore yes */
void query_cache() {
    printf("The messages being cached are (sorted by last hit):\n%-*#s\n",
        (int)TP->query_cols(), implode( map( sort_array( keys(message_cache),
        (: sort_by_last( $1, $2 ) :) ),
        (: sprintf("%i ("+$2[$1]->last+")", $1 ) :), message_cache ), "\n") );
} /* query_cache() */

/**
 * This method is called when a player is deleted by bulk_delete,
 * and will delete their post history and newsrc.
 * @param name the name of the player being deleted
 */
void delete_player( string name ) {
    if( file_name(PO) != BULK_DELETE_H )
        return;

    if( !undefinedp( posted_stuff[name] ) ) {
        map_delete( posted_stuff, name );
        save_me();
    }

    if( _newsrc_cache[name] )
        map_delete( _newsrc_cache, name );

    unguarded( (: rm, NEWSRC_SAVE_DIR+name[0..0]+"/"+name+".o" :) );

} /* delete_player() */

/** @ignore yes */
mixed stats() {
    if( !total_reads )
        total_reads = 1;
    if( !cache_hits )
        cache_hits = 1;
    if( !_newsrc_reads )
        _newsrc_reads = 1;

    return ({
      ({"messages read", total_reads }),
      ({"cache hit percent", ( cache_hits * 100 ) / total_reads }),
      ({"messages in cache", sizeof(message_cache) }),
      ({"newsrc reads", _newsrc_reads }),
      ({"newsrc hit percent", ( _newsrc_cache_hits * 100 ) / _newsrc_reads }),
      ({"newsrcs in cache", sizeof(_newsrc_cache) }),
      ({"current post number", num }),
    });
} /* stats() */