/** * 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() */