/*
 * Created on Mar 19, 2008
 * Created by Paul Gardner
 * 
 * Copyright 2008 Vuze, Inc.  All rights reserved.
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License only.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */


package com.aelitis.azureus.plugins.net.buddy;

import java.io.*;
import java.net.*;
import java.util.*;


import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.download.DownloadManagerState;
import org.gudy.azureus2.core3.util.*;
import org.gudy.azureus2.core3.util.protocol.azplug.AZPluginConnection;
import org.gudy.azureus2.core3.xml.util.XUXmlWriter;
import org.gudy.azureus2.plugins.*;
import org.gudy.azureus2.plugins.ddb.*;
import org.gudy.azureus2.plugins.disk.DiskManagerFileInfo;
import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.DownloadException;
import org.gudy.azureus2.plugins.download.DownloadScrapeResult;
import org.gudy.azureus2.plugins.ipc.IPCException;
import org.gudy.azureus2.plugins.logging.LoggerChannel;
import org.gudy.azureus2.plugins.messaging.MessageException;
import org.gudy.azureus2.plugins.messaging.MessageManager;
import org.gudy.azureus2.plugins.messaging.generic.GenericMessageConnection;
import org.gudy.azureus2.plugins.messaging.generic.GenericMessageHandler;
import org.gudy.azureus2.plugins.messaging.generic.GenericMessageRegistration;
import org.gudy.azureus2.plugins.network.ConnectionManager;
import org.gudy.azureus2.plugins.network.RateLimiter;
import org.gudy.azureus2.plugins.torrent.Torrent;
import org.gudy.azureus2.plugins.torrent.TorrentAttribute;
import org.gudy.azureus2.plugins.ui.UIInstance;
import org.gudy.azureus2.plugins.ui.UIManagerListener;
import org.gudy.azureus2.plugins.ui.config.*;
import org.gudy.azureus2.plugins.ui.menus.MenuItem;
import org.gudy.azureus2.plugins.ui.menus.MenuItemFillListener;
import org.gudy.azureus2.plugins.ui.menus.MenuItemListener;
import org.gudy.azureus2.plugins.ui.model.BasicPluginConfigModel;
import org.gudy.azureus2.plugins.ui.tables.TableContextMenuItem;
import org.gudy.azureus2.plugins.ui.tables.TableManager;
import org.gudy.azureus2.plugins.ui.tables.TableRow;
import org.gudy.azureus2.plugins.utils.*;
import org.gudy.azureus2.plugins.utils.security.SEPublicKey;
import org.gudy.azureus2.plugins.utils.security.SEPublicKeyLocator;
import org.gudy.azureus2.plugins.utils.security.SESecurityManager;
import org.gudy.azureus2.pluginsimpl.local.PluginCoreUtils;
import org.gudy.azureus2.ui.swt.plugins.UISWTInstance;

import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.security.*;
import com.aelitis.azureus.core.util.AZ3Functions;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.util.bloom.BloomFilter;
import com.aelitis.azureus.core.util.bloom.BloomFilterFactory;
import com.aelitis.azureus.plugins.magnet.MagnetPlugin;
import com.aelitis.azureus.plugins.magnet.MagnetPluginProgressListener;
import com.aelitis.azureus.plugins.net.buddy.swt.BuddyPluginView;
import com.aelitis.azureus.plugins.net.buddy.tracker.BuddyPluginTracker;

public class 
BuddyPlugin 
	implements Plugin
{
	public static final boolean SUPPORT_ONLINE_STATUS		= true;
	
	public static final int VERSION_INITIAL	= 1;
	public static final int VERSION_CHAT	= 2;
	public static final int VERSION_CURRENT	= VERSION_CHAT;
	
	
	public static final int MT_V3_CHAT		= 1;
	
	private static final int FEED_UPDATE_MIN_MILLIS	= 6*60*60*1000;
	
	public static final int MAX_MESSAGE_SIZE	= 4*1024*1024;
	
	public static final int	SUBSYSTEM_INTERNAL	= 0;
	public static final int	SUBSYSTEM_AZ2		= 1;
	public static final int	SUBSYSTEM_AZ3		= 2;
	
	protected static final int	SUBSYSTEM_MSG_TYPE_BASE	= 1024;
	
	public static final int STATUS_ONLINE			= 0;
	public static final int STATUS_AWAY				= 1;
	public static final int STATUS_NOT_AVAILABLE	= 2;
	public static final int STATUS_BUSY				= 3;
	public static final int STATUS_APPEAR_OFFLINE	= 4;
	
	public static final String[] STATUS_VALUES 	= { "0", "1", "2", "3", "4" };
	
	public static final String[] STATUS_KEYS = {
		"os_online", "os_away", "os_not_avail", "os_busy", "os_offline"
	};

	public static final String[] STATUS_STRINGS = new String[ STATUS_KEYS.length ];
	
	protected static final int RT_INTERNAL_REQUEST_PING		= 1;
	protected static final int RT_INTERNAL_REPLY_PING		= 2;
	protected static final int RT_INTERNAL_REQUEST_CLOSE	= 3;
	protected static final int RT_INTERNAL_REPLY_CLOSE		= 4;
	protected static final int RT_INTERNAL_FRAGMENT			= 5;
	
	protected static final boolean TRACE = false; 

	private static final String VIEW_ID = "azbuddy";

	private static final int	INIT_UNKNOWN		= 0;
	private static final int	INIT_OK				= 1;
	private static final int	INIT_BAD			= 2;
	
	private static final int	MAX_UNAUTH_BUDDIES	= 16;
	
	public static final int	TIMER_PERIOD	= 10*1000;
	
	private static final int	BUDDY_STATUS_CHECK_PERIOD_MIN	= 3*60*1000;
	private static final int	BUDDY_STATUS_CHECK_PERIOD_INC	= 1*60*1000;
	
	protected static final int	STATUS_REPUBLISH_PERIOD		= 10*60*1000;
	private static final int	STATUS_REPUBLISH_TICKS		= STATUS_REPUBLISH_PERIOD/TIMER_PERIOD;

	private static final int	CHECK_YGM_PERIOD			= 5*60*1000;
	private static final int	CHECK_YGM_TICKS				= CHECK_YGM_PERIOD/TIMER_PERIOD;
	private static final int	YGM_BLOOM_LIFE_PERIOD		= 60*60*1000;
	private static final int	YGM_BLOOM_LIFE_TICKS		= YGM_BLOOM_LIFE_PERIOD/TIMER_PERIOD;

	private static final int	SAVE_CONFIG_PERIOD			= 60*1000;
	private static final int	SAVE_CONFIG_TICKS			= SAVE_CONFIG_PERIOD/TIMER_PERIOD;

	public static final int		PERSISTENT_MSG_RETRY_PERIOD		= 5*60*1000;
	private static final int	PERSISTENT_MSG_CHECK_PERIOD		= 60*1000;
	private static final int	PERSISTENT_MSG_CHECK_TICKS		= PERSISTENT_MSG_CHECK_PERIOD/TIMER_PERIOD;

	private static final int	UNAUTH_BLOOM_RECREATE		= 120*1000;
	private static final int	UNAUTH_BLOOM_CHUNK			= 1000;
	private static BloomFilter	unauth_bloom;
	private static long			unauth_bloom_create_time;

	private static final int	BLOOM_CHECK_PERIOD			= UNAUTH_BLOOM_RECREATE/2;
	private static final int	BLOOM_CHECK_TICKS			= BLOOM_CHECK_PERIOD/TIMER_PERIOD;

	private static BloomFilter	ygm_unauth_bloom;

	public static final int STREAM_CRYPTO 	= MessageManager.STREAM_ENCRYPTION_RC4_REQUIRED;
	public static final int BLOCK_CRYPTO	= SESecurityManager.BLOCK_ENCRYPTION_AES;
	
	//public static final int STREAM_CRYPTO 	= MessageManager.STREAM_ENCRYPTION_NONE;
	//public static final int BLOCK_CRYPTO	= SESecurityManager.BLOCK_ENCRYPTION_NONE;
	
	private volatile int	 initialisation_state = INIT_UNKNOWN;
	
	private PluginInterface	plugin_interface;
	
	private LoggerChannel	logger;
	
	private BooleanParameter 		enabled_param; 
	private StringParameter 		nick_name_param;
	private StringListParameter 	online_status_param;
	private BooleanParameter 		enable_chat_notifications; 
	private StringParameter 		cat_pub;

	private boolean			ready_to_publish;
	private publishDetails	current_publish		= new publishDetails();
	private publishDetails	latest_publish		= current_publish;
	private long			last_publish_start;
	private TimerEvent		republish_delay_event;
	
	private AsyncDispatcher	publish_dispatcher = new AsyncDispatcher();
	
	private	DistributedDatabase 	ddb;
	
	private CryptoHandler ecc_handler = CryptoManagerFactory.getSingleton().getECCHandler();

	private List<BuddyPluginBuddy>	buddies 	= new ArrayList<BuddyPluginBuddy>();
	
	private List<BuddyPluginBuddy>	connected_at_close;
	
	private Map<String,BuddyPluginBuddy>		buddies_map	= new HashMap<String,BuddyPluginBuddy>();
	
	private CopyOnWriteList<BuddyPluginListener>				listeners 			= new CopyOnWriteList<BuddyPluginListener>();
	private CopyOnWriteList<BuddyPluginBuddyRequestListener>	request_listeners	= new CopyOnWriteList<BuddyPluginBuddyRequestListener>(); 
		
	private SESecurityManager	sec_man;

	private GenericMessageRegistration	msg_registration;
			
	private RateLimiter	inbound_limiter; 
		
	private RateLimiter	outbound_limiter; 
	
	private boolean		config_dirty;
	
	private Random	random = RandomUtils.SECURE_RANDOM;
	
	private BuddyPluginAZ2		az2_handler;
	
	private List<DistributedDatabaseContact>	publish_write_contacts = new ArrayList<DistributedDatabaseContact>();
	
	private int		status_seq;
	
	{
		while( status_seq == 0 ){
			
			status_seq = random.nextInt();
		}
	}
		
	private Set<BuddyPluginBuddy>			pd_preinit		= new HashSet<BuddyPluginBuddy>();
	
	private List<BuddyPluginBuddy>			pd_queue 		= new ArrayList<BuddyPluginBuddy>();
	private AESemaphore						pd_queue_sem	= new AESemaphore( "BuddyPlugin:persistDispatch");
	private AEThread2						pd_thread;
	
	private boolean		bogus_ygm_written;
	
	private BuddyPluginTracker	buddy_tracker;
	
	private TorrentAttribute	ta_category;

	private Set<String>	public_categories = new HashSet<String>();
	
	public static void
	load(
		PluginInterface		plugin_interface )
	{
		String name = 
			plugin_interface.getUtilities().getLocaleUtilities().getLocalisedMessageText( "Views.plugins." + VIEW_ID + ".title" );
		
		plugin_interface.getPluginProperties().setProperty( "plugin.version", 	"1.0" );
		plugin_interface.getPluginProperties().setProperty( "plugin.name", 		name );
	}

	public void
	initialize(
		final PluginInterface		_plugin_interface )
	{		
		plugin_interface	= _plugin_interface;
		
			
		ta_category		= plugin_interface.getTorrentManager().getAttribute( TorrentAttribute.TA_CATEGORY );

		az2_handler = new BuddyPluginAZ2( this );
				
		sec_man = plugin_interface.getUtilities().getSecurityManager();

		logger = plugin_interface.getLogger().getChannel( "Friends" );
		
		logger.setDiagnostic();
				
		final LocaleUtilities lu = plugin_interface.getUtilities().getLocaleUtilities();
		
		lu.addListener(
			new LocaleListener()
			{
				public void 
				localeChanged(
					Locale		l )
				{
					updateLocale(lu);
				}
			});
		
		updateLocale(lu);
		
		BasicPluginConfigModel config = plugin_interface.getUIManager().createBasicPluginConfigModel( "Views.plugins." + VIEW_ID + ".title" );
			
			// enabled

		enabled_param = config.addBooleanParameter2( "azbuddy.enabled", "azbuddy.enabled", false );
				
			// nickname

		nick_name_param = config.addStringParameter2( "azbuddy.nickname", "azbuddy.nickname", "" );

		nick_name_param.setGenerateIntermediateEvents( false );
		
		nick_name_param.addListener(
				new ParameterListener()
				{
					public void
					parameterChanged(
						Parameter	param )
					{
						updateNickName( nick_name_param.getValue());
					}
				});
		
			// online status

		String[]	os_values 	= STATUS_VALUES;
		String[]	os_labels	= STATUS_STRINGS;
		
		online_status_param = config.addStringListParameter2(
				"azbuddy.online_status", "azbuddy.online_status",
				os_values,
				os_labels,
				os_values[0] );
				
		online_status_param.addListener(
				new ParameterListener()
				{
					public void
					parameterChanged(
						Parameter	param )
					{
						 int status = Integer.parseInt( online_status_param.getValue());
						 
						 updateOnlineStatus( status );
					}
				});
		
		online_status_param.setVisible( SUPPORT_ONLINE_STATUS  ); // If we add this then use proper message texts in the STATUS_STRINGS
		
			// protocol speed
		
		final IntParameter	protocol_speed = config.addIntParameter2( "azbuddy.protocolspeed", "azbuddy.protocolspeed", 32 );
		
		protocol_speed.setMinimumRequiredUserMode( Parameter.MODE_ADVANCED );
		
		ConnectionManager cman = plugin_interface.getConnectionManager();
		
		int inbound_limit = protocol_speed.getValue()*1024;

		inbound_limiter 	= cman.createRateLimiter( "buddy_up", inbound_limit );	
		outbound_limiter 	= cman.createRateLimiter( "buddy_down", 0 );

		protocol_speed.addListener(
				new ParameterListener()
				{
					public void
					parameterChanged(
						Parameter	param )
					{
						inbound_limiter.setRateLimitBytesPerSecond( protocol_speed.getValue()*1024 );
					}
				});
		
			// chat notifications
		
		enable_chat_notifications = config.addBooleanParameter2( "azbuddy.enable_chat_notif", "azbuddy.enable_chat_notif", true );
		
			// default published cats
		
		cat_pub = config.addStringParameter2( "azbuddy.enable_cat_pub", "azbuddy.enable_cat_pub", "" );
		
		cat_pub.setGenerateIntermediateEvents( false );
		
		setPublicCats( cat_pub.getValue(), false );
		
		cat_pub.addListener(
			new ParameterListener()
			{
				public void 
				parameterChanged(
					Parameter 	param ) 
				{
					setPublicCats( cat_pub.getValue(), false);
				}
			});
		
			// config end
				
		
		final TableContextMenuItem menu_item_itorrents = 
			plugin_interface.getUIManager().getTableManager().addContextMenuItem(TableManager.TABLE_MYTORRENTS_INCOMPLETE, "azbuddy.contextmenu");
		final TableContextMenuItem menu_item_ctorrents 	= 
			plugin_interface.getUIManager().getTableManager().addContextMenuItem(TableManager.TABLE_MYTORRENTS_COMPLETE, "azbuddy.contextmenu");
		
		menu_item_itorrents.setStyle(TableContextMenuItem.STYLE_MENU);
		menu_item_ctorrents.setStyle(TableContextMenuItem.STYLE_MENU);
		
		MenuItemFillListener	menu_fill_listener = 
			new MenuItemFillListener()
			{
				public void
				menuWillBeShown(
					MenuItem	menu,
					Object		_target )
				{
					menu.removeAllChildItems();

					if ( !( isEnabled() && isAvailable())){
						
						menu.setEnabled( false );
						
						return;
					}
					
					final List<Torrent>	torrents = new ArrayList<Torrent>();
								
					if ( _target instanceof TableRow ){
						
						addDownload( torrents, (TableRow)_target );
	
					}else{
						
						TableRow[] rows = (TableRow[])_target;
					     
						for ( TableRow row: rows ){
							
							addDownload( torrents, row );
						}
					}
					
					if ( torrents.size() == 0 ){
						
						menu.setEnabled( false );

					}else{
					
						List<BuddyPluginBuddy> buddies = getBuddies();
						
						boolean	incomplete = ((TableContextMenuItem)menu).getTableID() == TableManager.TABLE_MYTORRENTS_INCOMPLETE;
						
						TableContextMenuItem parent = incomplete?menu_item_itorrents:menu_item_ctorrents;
												
						for (int i=0;i<buddies.size();i++){
							
							final BuddyPluginBuddy	buddy = (BuddyPluginBuddy)buddies.get(i);
							
							if ( buddy.isOnline( true )){
								
								TableContextMenuItem item =
									plugin_interface.getUIManager().getTableManager().addContextMenuItem(
										parent,
										"!" + buddy.getName() + "!");
								
								item.addMultiListener(
									new MenuItemListener()
									{
										public void 
										selected(
											MenuItem 	menu,
											Object 		target ) 
										{
											for ( Torrent torrent: torrents ){
											
												az2_handler.sendAZ2Torrent( torrent, buddy );
											}
										}
									});
							}
						}
					
						menu.setEnabled( true );
					}
				}
				
				protected void
				addDownload(
					List<Torrent>		torrents,
					TableRow			row )
				{
					Object obj = row.getDataSource();
					
					Download	download;
					
					if ( obj instanceof Download ){
						
						download = (Download)obj;
						
					}else{
						
						DiskManagerFileInfo file = (DiskManagerFileInfo)obj;
						
						try{
							download	= file.getDownload();
							
						}catch( DownloadException e ){	
							
							Debug.printStackTrace(e);
							
							return;
						}
					}
					
					Torrent torrent = download.getTorrent();
					
					if ( torrent != null && !TorrentUtils.isReallyPrivate( PluginCoreUtils.unwrap( torrent ))){
						
						torrents.add( torrent );
					}
				}
			};
			
		menu_item_itorrents.addFillListener( menu_fill_listener );
		menu_item_ctorrents.addFillListener( menu_fill_listener );
		
		buddy_tracker = new BuddyPluginTracker( this, config );
		
		plugin_interface.getUIManager().addUIListener(
			new UIManagerListener()
			{
				public void
				UIAttached(
					final UIInstance		instance )
				{
					if ( instance instanceof UISWTInstance ){
						
						UISWTInstance swt_ui = (UISWTInstance)instance;
						
						BuddyPluginView view = new BuddyPluginView( BuddyPlugin.this, swt_ui );

						swt_ui.addView(	UISWTInstance.VIEW_MAIN, VIEW_ID, view );
						
						//swt_ui.openMainView( VIEW_ID, view, null );
					}
					
					setupDisablePrompt(instance);
				}

				public void
				UIDetached(
					UIInstance		instance )
				{
				}
			});
		
		ParameterListener enabled_listener = 
			new ParameterListener()
			{
				public void
				parameterChanged(
					Parameter	param )
				{
					boolean enabled = enabled_param.getValue();

					nick_name_param.setEnabled( enabled );
					
						// only toggle overall state on a real change
					
					if ( param != null ){
					
						setEnabledInternal( enabled );
						fireEnabledStateChanged();
					}
				}
			};
		
		enabled_listener.parameterChanged( null );
			
		enabled_param.addListener( enabled_listener );
		
		loadConfig();
		
		registerMessageHandler();
		
		plugin_interface.addListener(
			new PluginListener()
			{
				public void
				initializationComplete()
				{
					final DelayedTask dt = plugin_interface.getUtilities().createDelayedTask(new Runnable()
						{
							public void 
							run() 
							{
								new AEThread2( "BuddyPlugin:init", true )
								{
									public void
									run()
									{
										startup();
									}
								}.start();
							}
						});
					
					dt.queue();
				}
				
				public void
				closedownInitiated()
				{	
					saveConfig( true );
					
					closedown();
				}
				
				public void
				closedownComplete()
				{				
				}
			});
	}
		
	protected void
	updateLocale(
		LocaleUtilities	lu )
	{
		for ( int i=0;i<STATUS_STRINGS.length;i++){
			
			STATUS_STRINGS[i] = lu.getLocalisedMessageText( "azbuddy." + STATUS_KEYS[i] );
		}
		
		if ( online_status_param != null ){
			
			online_status_param.setLabels( STATUS_STRINGS );
		}
	}
	
	/**
	 * 
	 *
	 * @since 3.0.5.3
	 */
	protected void 
	setupDisablePrompt(
			final UIInstance ui) 
	{
		if (plugin_interface == null) {
			return;
		}

		String enabledConfigID = "PluginInfo." + plugin_interface.getPluginID()
				+ ".enabled";
		COConfigurationManager.addParameterListener(enabledConfigID,
				new org.gudy.azureus2.core3.config.ParameterListener() {
					public void parameterChanged(
							String parameterName) 
					{
						fireEnabledStateChanged();
					}
				});
	}
	
	public void
	showConfig()
	{
		plugin_interface.getUIManager().showConfigSection("Views.plugins." + VIEW_ID + ".title");
	}
	
	protected void
	startup()
	{
		try{
			ddb = plugin_interface.getDistributedDatabase();
		
			if ( !ddb.isAvailable()){
				
				throw( new Exception( "DDB Unavailable" ));
			}
				// pick up initial values before enabling

			ddb.addListener(
				new DistributedDatabaseListener()
				{
					public void 
					event(
						DistributedDatabaseEvent event )
					{
						if ( event.getType() == DistributedDatabaseEvent.ET_LOCAL_CONTACT_CHANGED ){
							
							updateIP();
						}
					}
				});
					
			updateIP();
			
			updateNickName( nick_name_param.getValue());
						 
			updateOnlineStatus( Integer.parseInt( online_status_param.getValue()));
			 
			COConfigurationManager.addAndFireParameterListeners(
					new String[]{
						"TCP.Listen.Port",
						"TCP.Listen.Port.Enable",
						"UDP.Listen.Port",
						"UDP.Listen.Port.Enable" },
					new org.gudy.azureus2.core3.config.ParameterListener()
					{
						public void 
						parameterChanged(
							String parameterName )
						{
							updateListenPorts();
						}
					});
			
			CryptoManagerFactory.getSingleton().addKeyListener(
				new CryptoManagerKeyListener()
				{
					public void 
					keyChanged(
						CryptoHandler handler ) 
					{
						updateKey();
					}
					
					public void
					keyLockStatusChanged(
						CryptoHandler		handler )
					{	
						boolean unlocked = handler.isUnlocked();
						
						if ( unlocked ){
							
							if ( latest_publish.isEnabled()){
								
								updatePublish( latest_publish );
							}
						}else{
							
							new AEThread2( "BuddyPlugin:disc", true )
							{
								public void
								run()
								{
									List buddies = getAllBuddies();
									
									for (int i=0;i<buddies.size();i++){
										
										((BuddyPluginBuddy)buddies.get(i)).disconnect();
									}
								}
							}.start();
						}
					}
				});
			
			ready_to_publish	= true;
			
			setEnabledInternal( enabled_param.getValue());
			
			checkBuddiesAndRepublish();
			
			fireInitialised( true );
			
				// try to re-establish connection to previously connectd buddies
			
			List<BuddyPluginBuddy> buddies = getBuddies();
			
			for ( BuddyPluginBuddy buddy: buddies ){
				
				if ( buddy.getIP() != null && !buddy.isConnected()){
				
					log( "Attempting reconnect to " + buddy.getString());
					
					buddy.sendKeepAlive();
				}
			}
			
		}catch( Throwable e ){
		
			log( "Initialisation failed", e );
			
			fireInitialised( false );
		}
	}
	
	public boolean
	isEnabled()
	{
		if (enabled_param == null) {return false;}
		return( enabled_param.getValue());
	}
	
	public void
	setEnabled(
		boolean		enabled )
	{
		if (enabled_param == null) {return;}
		enabled_param.setValue( enabled );
	}
	
	protected void
	setEnabledInternal(
		boolean		_enabled )
	{
		synchronized( this ){
						
			if ( latest_publish.isEnabled() != _enabled ){
				
				publishDetails new_publish = latest_publish.getCopy();
				
				new_publish.setEnabled( _enabled );
				
				updatePublish( new_publish );
			}
		}
	}
	
	public BuddyPluginTracker
	getTracker()
	{
		return( buddy_tracker );
	}
	
	public String
	getNickname()
	{
		return(  nick_name_param.getValue());
	}
	
	public void
	setNickname(
		String	str )
	{
		nick_name_param.setValue( str );
	}
	
	public void
	setOnlineStatus(
		int		status )
	{
		online_status_param.setValue( "" + status );
	}
	
	public int
	getOnlineStatus()
	{
		return( latest_publish.getOnlineStatus());
	}
	
	public BooleanParameter
	getEnableChatNotificationsParameter()
	{
		return( enable_chat_notifications );
	}
	
	protected String
	normaliseCat(
		String		str )
	{
		if ( str == null ){
			
			return( null );
			
		}else if ( str.toLowerCase().equals( "all" )){
			
			return( "All" );
			
		}else{
			
			return( str );
		}
	}
	
	protected void
	normaliseCats(
		Set<String>	cats )
	{
		if ( cats != null ){
			
			boolean	all_found = false;
			
			Iterator<String> it = cats.iterator();
			
			while( it.hasNext()){
				
				if ( it.next().toLowerCase().equals( "all" )){
					
					it.remove();
					
					all_found = true;
				}
			}
			
			if ( all_found ){
				
				cats.add( "All" );
			}
		}
	}
	
	public boolean
	isPublicCategory(
		String	cat )
	{
		cat = normaliseCat( cat );
		
		return( public_categories.contains( cat ));
	}
	
	public void
	addPublicCategory(
		String	cat )
	{
		cat = normaliseCat( cat );
		
		Set<String> new_cats = new HashSet( public_categories );
		
		if ( new_cats.add( cat )){
		
			setPublicCats( new_cats, true );
		}
	}
	
	public void
	removePublicCategory(
		String	cat )
	{
		cat = normaliseCat( cat );
		
		Set<String> new_cats = new HashSet( public_categories );
		
		if ( new_cats.remove( cat )){
		
			setPublicCats( new_cats, true );
		}
	}
	
	protected void
	setPublicCats(
		String	str,
		boolean	persist )
	{
		Set<String>	new_pub_cats = new HashSet<String>();
		
		String[]	bits = str.split(",");
		
		for (String s: bits ){
			
			s = s.trim();
			
			if ( bits.length > 0 ){
				
				new_pub_cats.add( normaliseCat( s ));
			}
		}
		
		setPublicCats( new_pub_cats, persist );
	}
	
	protected void
	setPublicCats(
		Set<String>	new_pub_cats,
		boolean		persist )
	{
		if ( !public_categories.equals( new_pub_cats )){
			
			Set<String> removed = new HashSet<String>( public_categories );
			
			removed.removeAll( new_pub_cats );
			
			public_categories = new_pub_cats;
			
			if ( persist ){
				
				String cat_str = "";
				
				for ( String s: public_categories ){
					
					cat_str += (cat_str.length()==0?"":",") + s;
				}
				
				cat_pub.setValue( cat_str );
			}
			
			List<BuddyPluginBuddy> buds = getBuddies();
			
			for ( BuddyPluginBuddy b: buds ){
				
				Set<String> local = b.getLocalAuthorisedRSSCategories();
				
				if ( local != null || new_pub_cats.size() > 0 ){
					
					if ( local == null ){
						
						local = new HashSet<String>();
						
					}else{
						
							// gotta clone else we're messing with stuff that ain't ours
						
						local = new HashSet<String>( local );
					}
					
					local.addAll( new_pub_cats );
				
					local.removeAll( removed );
				
					b.setLocalAuthorisedRSSCategories( local );
				}
			}
		}
	}
	
	protected void
	registerMessageHandler()
	{
		try{
			addRequestListener(
				new BuddyPluginBuddyRequestListener()
				{
					public Map
					requestReceived(
						BuddyPluginBuddy	from_buddy,
						int					subsystem,
						Map					request )
					
						throws BuddyPluginException
					{
						if ( subsystem == SUBSYSTEM_INTERNAL ){
						
							if ( !from_buddy.isAuthorised()){
							
								throw( new BuddyPluginException( "Unauthorised" ));
							}
							
							return( processInternalRequest( from_buddy, request ));							
						}

						return( null );
					}
					
					public void
					pendingMessages(
						BuddyPluginBuddy[]	from_buddies )
					{
					}
				});
			
			msg_registration = 
				plugin_interface.getMessageManager().registerGenericMessageType(
					"AZBUDDY", "Buddy message handler", 
					STREAM_CRYPTO,
					new GenericMessageHandler()
					{
						public boolean
						accept(
							GenericMessageConnection	connection )
						
							throws MessageException
						{
							if ( !isEnabled()){
								
								return( false );
							}
							
							final String originator = connection.getEndpoint().getNotionalAddress().getAddress().getHostAddress();
							
							if ( TRACE ){
								System.out.println( "accept " + originator );
							}
							
							try{
								String reason = "Friend: Incoming connection establishment (" + originator + ")";
									
								addRateLimiters( connection );

								connection = 
									sec_man.getSTSConnection(
											connection, 
											sec_man.getPublicKey( SEPublicKey.KEY_TYPE_ECC_192, reason ),
											new SEPublicKeyLocator()
											{
												public boolean
												accept(
													Object		context,
													SEPublicKey	other_key )
												{
													String	other_key_str = Base32.encode( other_key.encodeRawPublicKey());

													if ( TRACE ){
														System.out.println( "Incoming: acceptKey - " + other_key_str );
													}
													
													try{
														synchronized( BuddyPlugin.this ){
																
															int	unauth_count = 0;
															
															for (int i=0;i<buddies.size();i++){
															
																BuddyPluginBuddy	buddy = (BuddyPluginBuddy)buddies.get(i);
	
																if ( buddy.getPublicKey().equals( other_key_str )){
																	
																		// don't accept a second or subsequent connection for unauth buddies
																		// as they have a single chance to be processed
																	
																	if ( !buddy.isAuthorised()){
																		
																		log( "Incoming connection from " + originator + " failed as for unauthorised buddy" );
																		
																		return( false );
																	}
																	
																	buddy.incomingConnection((GenericMessageConnection)context );	
																	
																	return( true );
																}
																
																if ( !buddy.isAuthorised()){
																	
																	unauth_count++;
																}
															}
															
																// no existing authorised buddy
															
															if ( unauth_count < MAX_UNAUTH_BUDDIES ){
																		
																if ( tooManyUnauthConnections( originator )){
																	
																	log( "Too many recent unauthorised connections from " + originator );
																	
																	return( false );
																}
																
																BuddyPluginBuddy buddy = addBuddy( other_key_str, SUBSYSTEM_AZ2, false );
																
																buddy.incomingConnection((GenericMessageConnection)context );	
																	
																return( true );
	
															}
														}
														
														log( "Incoming connection from " + originator + " failed due to pk mismatch" );
	
														return( false );
														
													}catch( Throwable e ){
														
														log( "Incomming connection from " + originator + " failed", e );
														
														return( false );
													}
												}
											},
											reason,
											BLOCK_CRYPTO );
							
							}catch( Throwable e ){
								
								connection.close();
								
								log( "Incoming connection from " + originator + " failed", e );
							}
							
							return( true );
						}
					});
					
		}catch( Throwable e ){
			
			log( "Failed to register message listener", e );
		}
	}
	
	protected void
	addRateLimiters(
		GenericMessageConnection	connection )
	{
		connection.addInboundRateLimiter( inbound_limiter );
		connection.addOutboundRateLimiter( outbound_limiter );
	}
	
	protected boolean
	tooManyUnauthConnections(
		String	originator )
	{
		synchronized( this ){
	
			if ( unauth_bloom == null ){
				
				unauth_bloom = BloomFilterFactory.createAddRemove4Bit( UNAUTH_BLOOM_CHUNK );
				
				unauth_bloom_create_time	= SystemTime.getCurrentTime();
			}
			
			int	hit_count = unauth_bloom.add( originator.getBytes());
			
			if ( hit_count >= 8 ){
			    		
				Debug.out( "Too many recent unauthorised connection attempts from " + originator );
     		
				return( true );
			}
			
			return( false );
		}
	}
	
	protected void
	checkUnauthBloom()
	{
		synchronized( this ){
		
			if ( unauth_bloom != null ){
				
				long	now = SystemTime.getCurrentTime();
				
				if ( now < unauth_bloom_create_time ){
					
					unauth_bloom_create_time = now;
					
				}else if ( now - unauth_bloom_create_time > UNAUTH_BLOOM_RECREATE ){
					
					unauth_bloom = null;
				}
			}
		}
	}
	
	protected void
	checkMaxMessageSize(
		int		size )
	
		throws BuddyPluginException
	{
		if ( size > MAX_MESSAGE_SIZE ){
			
			throw( new BuddyPluginException( "Message is too large to send, limit is " + DisplayFormatters.formatByteCountToKiBEtc( MAX_MESSAGE_SIZE )));
		}
	}
	
	protected void
	checkPersistentDispatch()
	{
		List	buddies_copy;
		
		synchronized( this ){
		
			buddies_copy = new ArrayList( buddies );
		}
				
		for (int i=0;i<buddies_copy.size();i++){
			
			BuddyPluginBuddy	buddy = (BuddyPluginBuddy)buddies_copy.get(i);

			buddy.checkPersistentDispatch();
		}
	}
	
	protected void
	persistentDispatchInit()
	{
		Iterator it = pd_preinit.iterator();
		
		while( it.hasNext()){
		
			persistentDispatchPending((BuddyPluginBuddy)it.next());
		}
		
		pd_preinit = null;
	}
	
	protected void
	persistentDispatchPending(
		BuddyPluginBuddy	buddy )
	{
		synchronized( pd_queue ){
			
			if ( initialisation_state == INIT_UNKNOWN ){
				
				pd_preinit.add( buddy );
				
				return;
			}
			
			if ( !pd_queue.contains( buddy )){
				
				pd_queue.add( buddy );
				
				pd_queue_sem.release();
				
				if ( pd_thread == null ){
					
					pd_thread = 
						new AEThread2( "BuddyPlugin:persistDispatch", true )
						{
							public void
							run()
							{
								while( true ){
									
									if ( !pd_queue_sem.reserve( 30*1000 )){
										
										synchronized( pd_queue ){
											
											if ( pd_queue.isEmpty()){
												
												pd_thread	= null;
												
												break;
											}
										}
									}else{
										
										BuddyPluginBuddy	buddy;
										
										synchronized( pd_queue ){
											
											buddy = (BuddyPluginBuddy)pd_queue.remove(0);
										}
										
										buddy.persistentDispatch();
									}
								}
							}
						};
						
					pd_thread.start();
				}
			}
		}
	}
	
	protected Map
	processInternalRequest(
		BuddyPluginBuddy	from_buddy,
		Map					request )		
		
		throws BuddyPluginException
	{
		int	type = ((Long)request.get("type")).intValue();
		
		if ( type == RT_INTERNAL_REQUEST_PING ){
		
			Map	reply = new HashMap();
		
			reply.put( "type", new Long( RT_INTERNAL_REPLY_PING ));
		
			return( reply );
			
		}else if ( type == RT_INTERNAL_REQUEST_CLOSE ){
		
			from_buddy.receivedCloseRequest( request );
			
			Map	reply = new HashMap();
		
			reply.put( "type", new Long( RT_INTERNAL_REPLY_CLOSE ));
		
			return( reply );
			
		}else{
			
			throw( new BuddyPluginException( "Unrecognised request type " + type ));
		}
	}

	protected void
	updateListenPorts()
	{
		synchronized( this ){

			int	tcp_port = COConfigurationManager.getIntParameter( "TCP.Listen.Port" );
			boolean	tcp_enabled = COConfigurationManager.getBooleanParameter( "TCP.Listen.Port.Enable" );
			int	udp_port = COConfigurationManager.getIntParameter("UDP.Listen.Port" );
			boolean	udp_enabled = COConfigurationManager.getBooleanParameter( "UDP.Listen.Port.Enable" );
				
			if ( !tcp_enabled ){
				
				tcp_port = 0;
			}
			
			if ( !udp_enabled ){
				
				udp_port = 0;
			}
			
			if ( 	latest_publish.getTCPPort() != tcp_port ||
					latest_publish.getUDPPort() != udp_port ){
				
				publishDetails new_publish = latest_publish.getCopy();
				
				new_publish.setTCPPort( tcp_port );
				new_publish.setUDPPort( udp_port );
				
				updatePublish( new_publish );
			}
		}
	}
	
	protected void
	updateIP()
	{
		if ( ddb == null || !ddb.isAvailable()){
			
			return;
		}
				
		synchronized( this ){

			InetAddress public_ip = ddb.getLocalContact().getAddress().getAddress();
				
			if ( 	latest_publish.getIP() == null ||
					!latest_publish.getIP().equals( public_ip )){
					
				publishDetails new_publish = latest_publish.getCopy();
				
				new_publish.setIP( public_ip );
				
				updatePublish( new_publish );
			}
		}
	}
	
	protected void
	updateNickName(
		String		new_nick )
	{
		new_nick = new_nick.trim();
		
		if ( new_nick.length() == 0 ){
			
			new_nick = null;
		}
		
		synchronized( this ){

			String	old_nick = latest_publish.getNickName();
			
			if ( !stringsEqual( new_nick, old_nick )){
			
				publishDetails new_publish = latest_publish.getCopy();
					
				new_publish.setNickName( new_nick );
					
				updatePublish( new_publish );
			}
		}
	}
	
	protected void
	updateOnlineStatus(
		int		new_status )
	{
		boolean	changed;
		
		synchronized( this ){

			int	old_status = latest_publish.getOnlineStatus();
			
			changed = old_status != new_status;
			
			if ( changed ){
			
				publishDetails new_publish = latest_publish.getCopy();
					
				new_publish.setOnlineStatus( new_status );
					
				updatePublish( new_publish );
			}
		}
		
		if ( changed ){
			
			List	buddies = getAllBuddies();
						
			for (int i=0;i<buddies.size();i++){
				
				BuddyPluginBuddy	buddy = (BuddyPluginBuddy)buddies.get(i);
				
				if ( buddy.isConnected()){
				
					buddy.sendKeepAlive();
				}
			}
		}
	}
	
	public String
	getOnlineStatus(
		int		status )
	{
		if ( status >= STATUS_STRINGS.length || status < 0 ){
		
			status = 0;
		}
		
		return( STATUS_STRINGS[status] );
	}
	
	protected boolean
	stringsEqual(
		String	s1, 
		String	s2 )
	{
		if ( s1 == null && s2 == null ){
			
			return( true );
		}
		
		if ( s1 == null || s2 == null ){
			
			return( false );
		}
		
		return( s1.equals( s2 ));
	}
	
	protected void
	updateKey()
	{
		synchronized( this ){

			publishDetails new_publish = latest_publish.getCopy();
				
			new_publish.setPublicKey( null );
				
			updatePublish( new_publish );
		}
	}
	
	protected void
	updatePublish(
		final publishDetails	details )
	{
		latest_publish = details;
		
		if ( ddb == null || !ready_to_publish ){
			
			return;
		}
		
		publish_dispatcher.dispatch(
			new AERunnable()
			{
				public void
				runSupport()
				{
						// only execute the most recent publish
					
					if ( publish_dispatcher.getQueueSize() > 0 ){
						
						return;
					}
					
					updatePublishSupport( details );
				}
			});
	}
	
	protected void
	updatePublishSupport(
		publishDetails	details )
	{
		byte[]	key_to_remove = null;
		
		publishDetails	existing_details;
		
		boolean	log_this;
		
		synchronized( this ){

			log_this = !current_publish.getString().equals( details.getString());
			
			existing_details = current_publish;
			
			if ( !details.isEnabled()){
				
				if ( current_publish.isPublished()){
					
					key_to_remove	= current_publish.getPublicKey();
				}
			}else{
								
				if ( details.getPublicKey() == null ){
					
					try{
						details.setPublicKey( ecc_handler.getPublicKey( "Creating online status key" ));
						
					}catch( Throwable e ){
						
						log( "Failed to publish details", e );
						
						return;
					}			
				}
				
				if ( current_publish.isPublished()){
					
					byte[]	existing_key = current_publish.getPublicKey();
				
					if ( !Arrays.equals( existing_key, details.getPublicKey())){
						
						key_to_remove = existing_key;
					}
				}
			}
			
			current_publish = details;
		}
		
		if ( key_to_remove != null ){
			
			log( "Removing old status publish: " + existing_details.getString());
			
			try{
				ddb.delete(
					new DistributedDatabaseListener()
					{
						public void
						event(
							DistributedDatabaseEvent		event )
						{
						}
					},
					getStatusKey( key_to_remove, "Friend status de-registration for old key" ));
				
			}catch( Throwable e ){	
			
				log( "Failed to remove existing publish", e );
			}
		}
		
		if ( details.isEnabled()){
			
				// ensure we have a sensible ip
			
			InetAddress ip = details.getIP();
			
			if ( ip.isLoopbackAddress() || ip.isLinkLocalAddress() || ip.isSiteLocalAddress()){
				
				log( "Can't publish as ip address is invalid: " + details.getString());
				
				return;
			}
			
			details.setPublished( true );
			
			Map	payload = new HashMap();
			
			if ( details.getTCPPort() > 0 ){
			
				payload.put( "t", new Long(  details.getTCPPort() ));
			}
			
			if (  details.getUDPPort() > 0 ){
				
				payload.put( "u", new Long( details.getUDPPort() ));
			}
						
			payload.put( "i", ip.getAddress());
			
			String	nick = details.getNickName();
			
			if ( nick != null ){
				
				if ( nick.length() > 32 ){
					
					nick = nick.substring( 0, 32 );
				}
				
				payload.put( "n", nick );
			}
			
			payload.put( "o", new Long( details.getOnlineStatus()));
			
			int	next_seq = ++status_seq;
			
			if ( next_seq == 0 ){
				
				next_seq = ++status_seq;
			}
			
			details.setSequence( next_seq );
			
			payload.put( "s", new Long( next_seq ));
			
			payload.put( "v", new Long( VERSION_CURRENT ));
			
			boolean	failed_to_get_key = true;
			
			try{
				byte[] data = BEncoder.encode( payload );
										
				DistributedDatabaseKey	key = getStatusKey( details.getPublicKey(), "My buddy status registration " + payload );
	
				byte[] signature = ecc_handler.sign( data, "Friend online status" );
			
				failed_to_get_key = false;
				
				byte[]	signed_payload = new byte[ 1 + signature.length + data.length ];
				
				signed_payload[0] = (byte)signature.length;
				
				System.arraycopy( signature, 0, signed_payload, 1, signature.length );
				System.arraycopy( data, 0, signed_payload, 1 + signature.length, data.length );		
				
				DistributedDatabaseValue	value = ddb.createValue( signed_payload );
				
				final AESemaphore	sem = new AESemaphore( "BuddyPlugin:reg" );
				
				if ( log_this ){
					
					logMessage( "Publishing status starts: " + details.getString());
				}
				
				last_publish_start = SystemTime.getMonotonousTime();
				
				ddb.write(
					new DistributedDatabaseListener()
					{
						private List<DistributedDatabaseContact>	write_contacts = new ArrayList<DistributedDatabaseContact>();
						
						public void
						event(
							DistributedDatabaseEvent		event )
						{
							int	type = event.getType();
						
							if ( type == DistributedDatabaseEvent.ET_VALUE_WRITTEN ){
								
								write_contacts.add( event.getContact());
								
							}else if ( 	type == DistributedDatabaseEvent.ET_OPERATION_TIMEOUT ||
										type == DistributedDatabaseEvent.ET_OPERATION_COMPLETE ){

								synchronized( publish_write_contacts ){
									
									publish_write_contacts.clear();
									
									publish_write_contacts.addAll( write_contacts );
								}
								
								sem.release();
							}
						}
					},
					key,
					value );
				
				sem.reserve();
				
				if ( log_this ){
				
					logMessage( "My status publish complete" );
				}
			}catch( Throwable e ){
				
				logMessage( "Failed to publish online status", e );
				
				if ( failed_to_get_key ){
					
					synchronized( this ){
						
						if ( republish_delay_event != null ){
							
							return;
						}
						
						if ( 	last_publish_start == 0 ||
								SystemTime.getMonotonousTime() - last_publish_start > STATUS_REPUBLISH_PERIOD ){
						
							log( "Rescheduling publish as failed to get key" );
						
							republish_delay_event = SimpleTimer.addEvent(
								"BuddyPlugin:republish",
								SystemTime.getCurrentTime() + 60*1000,
								new TimerEventPerformer()
								{
									public void 
									perform(
										TimerEvent event) 
									{
										synchronized( BuddyPlugin.this ){
											
											republish_delay_event = null;
										}
										
										if ( 	last_publish_start == 0 ||
												SystemTime.getMonotonousTime() - last_publish_start > STATUS_REPUBLISH_PERIOD ){
										
											if ( latest_publish.isEnabled()){
												
												updatePublish( latest_publish );
											}
										}
									}
								});
								
						}	
					}
				}
			}
		}
	}
	
	protected int
	getCurrentStatusSeq()
	{
		return( current_publish.getSequence());
	}
	
	protected void
	closedown()
	{
		logMessage( "Closing down" );

		List<BuddyPluginBuddy>	buddies = getAllBuddies();
		
		synchronized( this ){
			
			connected_at_close = new ArrayList<BuddyPluginBuddy>();
			
			for ( BuddyPluginBuddy buddy: buddies ){
				
				if ( buddy.isConnected()){
					
					connected_at_close.add( buddy );
				}
			}
		}
		
		if ( ddb != null ){
			
			boolean	restarting = AzureusCoreFactory.isCoreAvailable() ? AzureusCoreFactory.getSingleton().isRestarting() : false;
			
			logMessage( "   closing buddy connections" );
			
			for (int i=0;i<buddies.size();i++){
				
				((BuddyPluginBuddy)buddies.get(i)).sendCloseRequest( restarting );
			}
			
			if ( !restarting ){
				
				logMessage( "   updating online status" );
				
				List	contacts = new ArrayList();
				
				synchronized( publish_write_contacts ){
					
					contacts.addAll( publish_write_contacts );
				}
				
				byte[] key_to_remove;
				
				synchronized( this ){
	
					key_to_remove	= current_publish.getPublicKey();
				}
				
				if ( contacts.size() == 0 || key_to_remove == null ){
					
					return;
				}
				
				DistributedDatabaseContact[] contact_a = new DistributedDatabaseContact[contacts.size()];
				
				contacts.toArray( contact_a );
				
				try{
					ddb.delete(
						new DistributedDatabaseListener()
						{
							public void
							event(
								DistributedDatabaseEvent		event )
							{
								if ( event.getType() == DistributedDatabaseEvent.ET_VALUE_DELETED ){
	
									// System.out.println( "Deleted status from " + event.getContact().getName());
								}
							}
						},
						getStatusKey( key_to_remove, "Friend status de-registration for closedown" ),
						contact_a );
					
				}catch( Throwable e ){	
				
					log( "Failed to remove existing publish", e );
				}
			}
		}
	}
	
	protected DistributedDatabaseKey
	getStatusKey(
		byte[]	public_key,
		String	reason )
	
		throws Exception
	{
		byte[]	key_prefix = "azbuddy:status".getBytes();
		
		byte[]	key_bytes = new byte[ key_prefix.length + public_key.length ];
		
		System.arraycopy( key_prefix, 0, key_bytes, 0, key_prefix.length );
		System.arraycopy( public_key, 0, key_bytes, key_prefix.length, public_key.length );
		
		DistributedDatabaseKey key = ddb.createKey( key_bytes, reason );
		
		return( key );
	}

	protected DistributedDatabaseKey
	getYGMKey(
		byte[]	public_key,
		String	reason )
	
		throws Exception
	{
		byte[]	key_prefix = "azbuddy:ygm".getBytes();
		
		byte[]	key_bytes = new byte[ key_prefix.length + public_key.length ];
		
		System.arraycopy( key_prefix, 0, key_bytes, 0, key_prefix.length );
		System.arraycopy( public_key, 0, key_bytes, key_prefix.length, public_key.length );
		
		DistributedDatabaseKey key = ddb.createKey( key_bytes, reason );
		
		return( key );
	}
	
	protected void
	setConfigDirty()
	{
		synchronized( this ){
			
			config_dirty = true;
		}
	}
		
	protected void
	loadConfig()
	{
		long	now = SystemTime.getCurrentTime();
		
		synchronized( this ){
			
			Map map = readConfig(); 
					
			List	buddies_config = (List)map.get( "friends" );
				
			if ( buddies_config != null ){
					
				if ( buddies_config.size() == 0 ){
					
					deleteConfig();
					
				}else{
					for (int i=0;i<buddies_config.size();i++){
						
						Object o = buddies_config.get(i);
			
						if ( o instanceof Map ){
							
							Map	details = (Map)o;
							
							Long	l_ct = (Long)details.get( "ct" );
							
							long	created_time = l_ct==null?now:l_ct.longValue();
							
							if ( created_time > now ){
								
								created_time = now;
							}
							
							String	key = new String((byte[])details.get( "pk" ));
							
							List	recent_ygm = (List)details.get( "ygm" );
												
							String	nick = decodeString((byte[])details.get( "n" ));
							
							Long	l_seq = (Long)details.get( "ls" );
							
							int	last_seq = l_seq==null?0:l_seq.intValue();
							
							Long	l_lo = (Long)details.get( "lo" );
							
							long	last_time_online = l_lo==null?0:l_lo.longValue();
						
							if ( last_time_online > now ){
								
								last_time_online = now;
							}
							
							Long l_subsystem = (Long)details.get( "ss" );
							
							int	subsystem = l_subsystem==null?SUBSYSTEM_AZ2:l_subsystem.intValue();
							
							if (subsystem == SUBSYSTEM_AZ3) {
								continue;
							}
							
							Long l_ver = (Long)details.get("v");
							
							int	ver = l_ver==null?VERSION_INITIAL:l_ver.intValue();
														
							String	loc_cat = decodeString((byte[])details.get( "lc" ));
							String	rem_cat = decodeString((byte[])details.get( "rc" ));
							
							BuddyPluginBuddy buddy = new BuddyPluginBuddy( this, created_time, subsystem, true, key, nick, ver, loc_cat, rem_cat, last_seq, last_time_online, recent_ygm );
							
							byte[]	ip_bytes = (byte[])details.get( "ip" );
							
							if ( ip_bytes != null ){
								
								try{
									InetAddress ip = InetAddress.getByAddress( ip_bytes );
									
									int	tcp_port = ((Long)details.get( "tcp" )).intValue();
									int	udp_port = ((Long)details.get( "udp" )).intValue();
									
									buddy.setCachedStatus( ip, tcp_port, udp_port );
									
								}catch( Throwable e ){
								}
							}
							
							logMessage( "Loaded buddy " + buddy.getString());
							
							buddies.add( buddy );
							
							buddies_map.put( key, buddy );
						}
					}
				}
			}
			
			int	num_buddies = buddies.size();
			
			for ( BuddyPluginBuddy b: buddies ){
				
				b.setInitialStatus( now, num_buddies );
			}
		}
	}
	
	protected String
	decodeString(
		byte[]		bytes )
	{
		if (  bytes == null ){
			
			return( null );
		}
		
		try{
			return( new String( bytes, "UTF8" ));
			
		}catch( Throwable e ){
			
			return( null );
		}
	}
	
	protected void
	saveConfig()
	{
		saveConfig( false );
	}
	
	protected void
	saveConfig(
		boolean	force )
	{
		synchronized( this ){

			if ( config_dirty || force ){
				
				List buddies_config = new ArrayList();
		
				for (int i=0;i<buddies.size();i++){
					
					BuddyPluginBuddy buddy = (BuddyPluginBuddy)buddies.get(i);
		
					if ( !buddy.isAuthorised()){
						
						continue;
					}
					
					Map	map = new HashMap();
				
					map.put( "ct", new Long( buddy.getCreatedTime()));
					
					map.put( "pk", buddy.getPublicKey());
				
					List	ygm = buddy.getYGMMarkers();
					
					if ( ygm != null ){
						
						map.put( "ygm", ygm );
					}
					
					String	nick = buddy.getNickName();
					
					if ( nick != null ){
						
						map.put( "n", nick );
					}
					
					map.put( "ls", new Long( buddy.getLastStatusSeq()));
					
					map.put( "lo", new Long( buddy.getLastTimeOnline()));
					
					map.put( "ss", new Long( buddy.getSubsystem()));
					
					map.put( "v", new Long( buddy.getVersion()));
					
					if ( buddy.getLocalAuthorisedRSSCategoriesAsString() != null ){
						map.put( "lc", buddy.getLocalAuthorisedRSSCategoriesAsString());
					}
					
					if ( buddy.getRemoteAuthorisedRSSCategoriesAsString() != null ){
						map.put( "rc", buddy.getRemoteAuthorisedRSSCategoriesAsString());
					}

					boolean connected = 
						buddy.isConnected() ||
						( connected_at_close != null && connected_at_close.contains( buddy ));
					
					if ( connected ){
						
						InetAddress	ip 			= buddy.getIP();
						int			tcp_port	= buddy.getTCPPort();
						int			udp_port	= buddy.getUDPPort();
						
						if ( ip != null ){
							
							map.put( "ip", ip.getAddress());
							map.put( "tcp", new Long( tcp_port ));
							map.put( "udp", new Long( udp_port ));
						}
					}
					
					buddies_config.add( map );
				}
				
				Map	map = new HashMap();
				
				if ( buddies_config.size() > 0 ){
				
					map.put( "friends", buddies_config );
				
					writeConfig( map );
					
				}else{
					
					deleteConfig();
				}
				
				config_dirty = false;
			}
		}
	}
	
	public BuddyPluginBuddy
	addBuddy(
		String		key,
		int			subsystem )
	
	{
		return( addBuddy( key, subsystem, true ));
	}
	
	protected BuddyPluginBuddy
	addBuddy(
		String		key,
		int			subsystem,
		boolean		authorised )
	{
		if ( key.length() == 0 || !verifyPublicKey( key )){
			
			return( null );
		}
				
		BuddyPluginBuddy	buddy_to_return = null;
		
			// buddy may be already present as unauthorised in which case we pick it up
			// and authorise it and send the added event (we don't fire added events for
			// unauthorised buddies)
		
		synchronized( this ){
						
			for (int i=0;i<buddies.size();i++){
				
				BuddyPluginBuddy buddy = (BuddyPluginBuddy)buddies.get(i);
				
				if ( buddy.getPublicKey().equals( key )){
					
					if ( buddy.getSubsystem() != subsystem ){
						
						log( "Buddy " + buddy.getString() + ": subsystem changed from " + buddy.getSubsystem() + " to " + subsystem );
						
						buddy.setSubsystem( subsystem );
						
						saveConfig( true );
					}
					
					if ( authorised && !buddy.isAuthorised()){
						
						log( "Buddy " + buddy.getString() + ": no authorised" );
						
						buddy.setAuthorised( true );
						
						buddy_to_return	= buddy;
						
					}else{
					
						return( buddy );
					}
				}
			}
			
			if ( buddy_to_return == null ){
				
				buddy_to_return = 
					new BuddyPluginBuddy( this, SystemTime.getCurrentTime(), subsystem, authorised, key, null, VERSION_CURRENT, null, null, 0, 0, null );
				
				buddies.add( buddy_to_return );
				
				buddies_map.put( key, buddy_to_return );
				
				if ( !authorised ){
					
					log( "Added unauthorised buddy: " + buddy_to_return.getString());
				}
			}
			
			if ( buddy_to_return.isAuthorised()){
				
				logMessage( "Added buddy " + buddy_to_return.getString());
	
				saveConfig( true );
			}
		}
				
		fireAdded( buddy_to_return );
		
		return( buddy_to_return );
	}
	
	protected void
	removeBuddy(
		BuddyPluginBuddy 	buddy )
	{
		synchronized( this ){

			if ( !buddies.remove( buddy )){
				
				return;
			}
		
			buddies_map.remove( buddy.getPublicKey());
						
			logMessage( "Removed friend " + buddy.getString());

			saveConfig( true );
		}
		
		buddy.destroy();
				
		fireRemoved( buddy );
	}
	
	protected Map
	readConfig()
	{
		File	config_file = new File( plugin_interface.getUtilities().getAzureusUserDir(), "friends.config" );
		
		return( readConfigFile( config_file ));
	}
	
	protected void
	writeConfig(
		Map		map )
	{
		File	config_file = new File( plugin_interface.getUtilities().getAzureusUserDir(), "friends.config" );
		
		writeConfigFile( config_file, map );
	}
	
	protected void
	deleteConfig()
	{
		File	config_file = new File( plugin_interface.getUtilities().getAzureusUserDir(), "friends.config" );
		
		Utilities utils = plugin_interface.getUtilities();
			
		plugin_interface.getUtilities().deleteResilientBEncodedFile(
				config_file.getParentFile(), config_file.getName(), true );

	}
	
	protected Map
	readConfigFile(
		File		name )
	{
		Utilities utils = plugin_interface.getUtilities();
		
		Map map = utils.readResilientBEncodedFile(
						name.getParentFile(), name.getName(), true );
		
		if ( map == null ){
			
			map = new HashMap();
		}
		
		return( map );
	}
	
	protected boolean
	writeConfigFile(
		File		name,
		Map			data )
	{
		Utilities utils = plugin_interface.getUtilities();
			
		plugin_interface.getUtilities().writeResilientBEncodedFile(
			name.getParentFile(), name.getName(), data, true );
		
		return( name.exists());
	}
	
	protected File
	getBuddyConfigDir()
	{
		return( new File( plugin_interface.getUtilities().getAzureusUserDir(), "friends" ));
	}
	
	public BuddyPluginAZ2
	getAZ2Handler()
	{
		return( az2_handler );
	}
	
	public String
	getPublicKey()
	{
		try{
			return( Base32.encode(ecc_handler.getPublicKey( "Friend get key" )));
			
		}catch( Throwable e ){
			
			logMessage( "Failed to access public key", e );
			
			return( null );
		}
	}
	
	public boolean
	verifyPublicKey(
		String		key )
	{
		return( ecc_handler.verifyPublicKey( Base32.decode( key )));
	}
	
	protected void
	checkBuddiesAndRepublish()
	{
		updateBuddys();
		
		plugin_interface.getUtilities().createTimer( "Buddy checker" ).addPeriodicEvent(
			TIMER_PERIOD,
			new UTTimerEventPerformer()
			{
				int	tick_count;
				
				public void 
				perform(
					UTTimerEvent event ) 
				{
					tick_count++;
					
					if ( !isEnabled()){
						
						return;
					}
												
					updateBuddys();
					
					if ( tick_count % STATUS_REPUBLISH_TICKS == 0 ){
							
						if ( latest_publish.isEnabled()){
								
							updatePublish( latest_publish );
						}
					}
					
					if ( tick_count % CHECK_YGM_TICKS == 0 ){

						checkMessagePending( tick_count );
					}
					
					if ( tick_count % BLOOM_CHECK_TICKS == 0 ){
						
						checkUnauthBloom();
					}
					
					if ( tick_count % SAVE_CONFIG_TICKS == 0 ){

						saveConfig();
					}
					
					if ( tick_count % PERSISTENT_MSG_CHECK_TICKS == 0 ){
						
						checkPersistentDispatch();
					}
					
					if ( buddy_tracker != null ){
						
						buddy_tracker.tick( tick_count );
					}
				}
			});
	}
	
	protected void
	updateBuddys()
	{
		List	buddies_copy;
		
		synchronized( this ){
		
			buddies_copy = new ArrayList( buddies );
		}
		
		long	now = SystemTime.getCurrentTime();
		
		Random random = new Random();
		
		for (int i=0;i<buddies_copy.size();i++){
			
			BuddyPluginBuddy	buddy = (BuddyPluginBuddy)buddies_copy.get(i);
			
			long	last_check = buddy.getLastStatusCheckTime();
			
			buddy.checkTimeouts();
			
			int	period = BUDDY_STATUS_CHECK_PERIOD_MIN + BUDDY_STATUS_CHECK_PERIOD_INC*buddies_copy.size()/5;
			
				// randomise a bit
			
			period += random.nextInt( 2*60*1000 );
			
				// last check may be in the future as we defer checks for seemingly inactive buddies
			
			if ( now - last_check > period ){
				
				if ( !buddy.statusCheckActive()){
			
					if ( buddy.isAuthorised()){
					
						updateBuddyStatus( buddy );
					}
				}
			}
		}
		
			// trim any non-authorised buddies that have gone idle

		synchronized( this ){
			
			for (int i=0;i<buddies_copy.size();i++){
			
				BuddyPluginBuddy	buddy = (BuddyPluginBuddy)buddies_copy.get(i);
				
				if ( buddy.isIdle() && !buddy.isAuthorised()){
					
					removeBuddy( buddy );
				}
			}
		}
	}
	
	protected void
	updateBuddyStatus(
		final BuddyPluginBuddy	buddy )
	{	
		if ( !buddy.statusCheckStarts()){
			
			return;
		}
		
		log( "Updating buddy status: " + buddy.getString());

		try{							
			final byte[]	public_key = buddy.getRawPublicKey();

			DistributedDatabaseKey	key = 
				getStatusKey( public_key, "Friend status check for " + buddy.getName());
			
			ddb.read(
				new DistributedDatabaseListener()
				{
					private long	latest_time;
					private Map		status;
					
					public void
					event(
						DistributedDatabaseEvent		event )
					{
						int	type = event.getType();
						
						if ( type == DistributedDatabaseEvent.ET_VALUE_READ ){
							
							try{
								DistributedDatabaseValue value = event.getValue();
								
								long time = value.getCreationTime();
								
								if ( time > latest_time ){
								
									byte[] signed_stuff = (byte[])value.getValue( byte[].class );
								
									Map	new_status = verifyAndExtract( signed_stuff, public_key );
									
									if ( new_status != null ){
	
										status = new_status;
																																							
										latest_time = time;
									}
								}
							}catch( Throwable e ){
								
								log( "Read failed", e );
							}
						}else if ( 	type == DistributedDatabaseEvent.ET_OPERATION_TIMEOUT ||
									type == DistributedDatabaseEvent.ET_OPERATION_COMPLETE ){
							
							if ( status == null ){
																
								buddy.statusCheckFailed();
								
							}else{
								
								try{
									Long	l_tcp_port = (Long)status.get( "t" );
									Long	l_udp_port = (Long)status.get( "u" );
									
									int	tcp_port = l_tcp_port==null?0:l_tcp_port.intValue();
									int udp_port = l_udp_port==null?0:l_udp_port.intValue();
									
									InetAddress ip = InetAddress.getByAddress((byte[])status.get("i"));
									
									String	nick = decodeString((byte[])status.get( "n" ));
									
									Long	l_seq = (Long)status.get( "s" );
									
									int		seq = l_seq==null?0:l_seq.intValue();
									
									Long	l_os = (Long)status.get( "o" );
									
									int		os = l_os==null?BuddyPlugin.STATUS_ONLINE:l_os.intValue();
											
									Long	l_ver = (Long)status.get( "v" );
									
									int		ver = l_ver==null?VERSION_INITIAL:l_ver.intValue();
											
									buddy.statusCheckComplete( latest_time, ip, tcp_port, udp_port, nick, os, seq, ver );
									
								}catch( Throwable e ){
									
									buddy.statusCheckFailed();
									
									log( "Status decode failed", e );
								}
							}
						}
					}
				},
				key,
				120*1000 );
			
		}catch( Throwable e ){
			
			buddy.statusCheckFailed();
			
			log( "Friend status update failed: " + buddy.getString(), e );
		}
	}
	
	protected Map
	verifyAndExtract(
		byte[]		signed_stuff,
		byte[]		public_key )
	
		throws BuddyPluginException
	{
		int	signature_length = ((int)signed_stuff[0])&0xff;
		
		byte[]	signature 	= new byte[ signature_length ];
		byte[]	data		= new byte[ signed_stuff.length - 1 - signature_length];
		
		System.arraycopy( signed_stuff, 1, signature, 0, signature_length );
		System.arraycopy( signed_stuff, 1 + signature_length, data, 0, data.length );
			
		try{
			if ( ecc_handler.verify( public_key, data, signature )){													
	
				return( BDecoder.decode( data ));
																																	
			}else{
				
				logMessage( "Signature verification failed" );
				
				return( null );
			}
		}catch( Throwable e ){
			
			rethrow( "Verification failed", e );
			
			return( null );
		}
	}
	
	protected byte[]
	signAndInsert(
		Map		plain_stuff,
		String	reason )
	
		throws BuddyPluginException
	{
		try{
			byte[] data = BEncoder.encode( plain_stuff );
			
			byte[] signature = ecc_handler.sign( data, reason );
		
			byte[]	signed_payload = new byte[ 1 + signature.length + data.length ];
			
			signed_payload[0] = (byte)signature.length;
			
			System.arraycopy( signature, 0, signed_payload, 1, signature.length );
			System.arraycopy( data, 0, signed_payload, 1 + signature.length, data.length );		
	
			return( signed_payload );
			
		}catch( Throwable e ){
			
			rethrow( "Signing failed", e );
			
			return( null );
		}
	}
	
	public boolean
	verify(
		String				pk,
		byte[]				payload,
		byte[]				signature )
	
		throws BuddyPluginException
	{
		return( verify( Base32.decode( pk ), payload, signature ));
	}
	
	protected boolean
	verify(
		BuddyPluginBuddy	buddy,
		byte[]				payload,
		byte[]				signature )
	
		throws BuddyPluginException
	{
		return( verify( buddy.getRawPublicKey(), payload, signature ));
	}
	
	protected boolean
	verify(
		byte[]				pk,
		byte[]				payload,
		byte[]				signature )
	
		throws BuddyPluginException
	{
		try{
		
			return( ecc_handler.verify( pk, payload, signature ));
			
		}catch( Throwable e ){
			
			rethrow( "Verification failed", e );
			
			return( false );
		}
	}
	
	public byte[]
   	sign(
   		byte[]		payload )
	        	
	   	throws BuddyPluginException
	{ 
		try{
		
			return( ecc_handler.sign( payload, "Friend message signing" ));

		}catch( Throwable e ){
			
			rethrow( "Signing failed", e );
			
			return( null );
		}
	}

	protected cryptoResult
	encrypt(
		BuddyPluginBuddy	buddy,
		byte[]				payload )
	
		throws BuddyPluginException
	{
		return encrypt(buddy.getPublicKey(), payload, buddy.getName());
	}

	public cryptoResult
	encrypt(
		String				pk,
		byte[]				payload,
		String				forWho )
	
		throws BuddyPluginException
	{
		
		try{
			byte[]	hash = new byte[20];
			
			random.nextBytes( hash );
			
			Map	content = new HashMap();
			
			content.put( "h", hash );
			content.put( "p", payload );
			
			final byte[] encrypted = ecc_handler.encrypt( Base32.decode(pk), BEncoder.encode( content ), "Encrypting message for " + forWho);
			
			final byte[] sha1_hash = new SHA1Simple().calculateHash( hash );
			
			return( 
				new cryptoResult()
				{
					public byte[]
		    		getChallenge()
					{
						return( sha1_hash );
					}
		    		
		    		public byte[]
		    		getPayload()
		    		{
		    			return( encrypted );
		    		}
				});
			
		}catch( Throwable e ){
			
			rethrow( "Encryption failed", e );
			
			return( null );
		}
	}
	
	protected cryptoResult
	decrypt(
		BuddyPluginBuddy	buddy,
		byte[]				content,
		String forName)
	
		throws BuddyPluginException
	{
		
		try{
			final byte[] decrypted = ecc_handler.decrypt( buddy.getRawPublicKey(), content, "Decrypting message for " + buddy.getName());
			
			final Map	map = BDecoder.decode( decrypted );
			
			return( 
				new cryptoResult()
				{
					public byte[]
		    		getChallenge()
					{
						return((byte[])map.get("h"));
					}
		    		
		    		public byte[]
		    		getPayload()
		    		{
		    			return((byte[])map.get("p"));
		    		}
				});
			
		}catch( Throwable e ){
			
			rethrow( "Decryption failed", e );
			
			return( null );
		}
	}

	public cryptoResult
	decrypt(
		String				public_key,
		byte[]				content )
	
		throws BuddyPluginException
	{
		
		try{
			final byte[] decrypted = ecc_handler.decrypt( Base32.decode(public_key), content, "Decrypting message for " + public_key);
			
			final Map	map = BDecoder.decode( decrypted );
			
			return( 
				new cryptoResult()
				{
					public byte[]
		    		getChallenge()
					{
						return((byte[])map.get("h"));
					}
		    		
		    		public byte[]
		    		getPayload()
		    		{
		    			return((byte[])map.get("p"));
		    		}
				});
			
		}catch( Throwable e ){
			
			rethrow( "Decryption failed", e );
			
			return( null );
		}
	}

	protected void
	setMessagePending(
		BuddyPluginBuddy			buddy,
		final operationListener		listener )
	
		throws BuddyPluginException
	{		
		try{
			checkAvailable();

			final String	reason = "Friend YGM write for " + buddy.getName();
			
			Map	payload = new HashMap();
			
			payload.put( "r", new Long( random.nextLong()));
			
			byte[] signed_payload = signAndInsert( payload, reason);
			
			Map	envelope = new HashMap();
			
			envelope.put( "pk", ecc_handler.getPublicKey( reason ));
			envelope.put( "ss", signed_payload );
			
			DistributedDatabaseValue	value = ddb.createValue( BEncoder.encode( envelope ));
										
			logMessage( reason + " starts: " + payload );
			
			DistributedDatabaseKey	key = getYGMKey( buddy.getRawPublicKey(), reason );

			ddb.write(
				new DistributedDatabaseListener()
				{
					public void
					event(
						DistributedDatabaseEvent		event )
					{
						int	type = event.getType();
					
						if ( 	type == DistributedDatabaseEvent.ET_OPERATION_TIMEOUT ||
								type == DistributedDatabaseEvent.ET_OPERATION_COMPLETE ){
							
							logMessage( reason + " complete"  );

							listener.complete();
						}
					}
				},
				key,
				value );

		}catch( Throwable e ){
			
			try{				
				rethrow( "Failed to publish YGM", e );
								
			}finally{
				
				listener.complete();
			}
		}
	}
	
	public void
	checkMessagePending(
		int	tick_count )
	{
		log( "Checking YGM" );

		if ( tick_count % YGM_BLOOM_LIFE_TICKS == 0 ){
			
			synchronized( this ){
				
				ygm_unauth_bloom = null;
			}
		}

		try{	
			String	reason = "Friend YGM check";
			
			byte[] public_key = ecc_handler.getPublicKey( reason );

			DistributedDatabaseKey	key = getYGMKey( public_key, reason );
			
			ddb.read(
				new DistributedDatabaseListener()
				{	
					private List		new_ygm_buddies = new ArrayList();
					private boolean	 	unauth_permitted = false;;
					
					public void
					event(
						DistributedDatabaseEvent		event )
					{
						int	type = event.getType();
						
						if ( type == DistributedDatabaseEvent.ET_VALUE_READ ){
							
							try{
								DistributedDatabaseValue value = event.getValue();
																
								byte[]	envelope = (byte[])value.getValue( byte[].class );
								
								Map	map = BDecoder.decode( envelope );
								
								byte[]	pk = (byte[])map.get( "pk" );
								
								if ( pk == null ){
									
									return;
								}
								
								String	pk_str = Base32.encode( pk );
								
								BuddyPluginBuddy buddy = getBuddyFromPublicKey( pk_str );
																
								if ( buddy == null || !buddy.isAuthorised() ){
									
									if ( buddy == null ){
									
										log( "YGM entry from unknown friend '" + pk_str + "' - ignoring" );
										
									}else{								
									
										log( "YGM entry from unauthorised friend '" + pk_str + "' - ignoring" );
									}

									byte[] address = event.getContact().getAddress().getAddress().getAddress();
									
									synchronized( BuddyPlugin.this ){
									
										if ( ygm_unauth_bloom == null ){
										
											ygm_unauth_bloom = BloomFilterFactory.createAddOnly(512);
										}
										
										if ( !ygm_unauth_bloom.contains( address )){
											
											ygm_unauth_bloom.add( address );
											
											unauth_permitted = true;
										}
									}
								}else{
									
									byte[]	signed_stuff = (byte[])map.get( "ss" );
									
									Map	payload = verifyAndExtract( signed_stuff, pk );
									
									if ( payload != null ){
										
										long	rand = ((Long)payload.get("r")).longValue();
										
										if ( buddy.addYGMMarker( rand )){
											
											new_ygm_buddies.add( buddy );
										}
									}
								}
							}catch( Throwable e ){
								
								log( "Read failed", e );
							}
						}else if ( 	type == DistributedDatabaseEvent.ET_OPERATION_TIMEOUT ||
									type == DistributedDatabaseEvent.ET_OPERATION_COMPLETE ){
							
							if ( new_ygm_buddies.size() > 0 || unauth_permitted ){
								
								BuddyPluginBuddy[] b = new BuddyPluginBuddy[new_ygm_buddies.size()];
								
								new_ygm_buddies.toArray( b );
								
								fireYGM( b );
							}
						}
					}
				},
				key,
				120*1000,
				DistributedDatabase.OP_EXHAUSTIVE_READ );
			
			boolean	write_bogus_ygm = false;
			
			synchronized( this ){
			
				if ( !bogus_ygm_written ){
					
					bogus_ygm_written = write_bogus_ygm = true;
				}
			}
			
			if ( write_bogus_ygm ){
				
				final String	reason2 = "Friend YGM write for myself";
				
				Map	envelope = new HashMap();
								
				DistributedDatabaseValue	value = ddb.createValue( BEncoder.encode( envelope ));
											
				logMessage( reason2 + " starts" );
				
				ddb.write(
					new DistributedDatabaseListener()
					{
						public void
						event(
							DistributedDatabaseEvent		event )
						{
							int	type = event.getType();
						
							if ( type == DistributedDatabaseEvent.ET_OPERATION_COMPLETE ){
								
								logMessage( reason2 + " complete"  );
							}
						}
					},
					key,
					value );
			}
			
		}catch( Throwable e ){
						
			logMessage( "YGM check failed", e );
		}
	}
	
	public BuddyPluginBuddy
	getBuddyFromPublicKey(
		String		key )
	{
		synchronized( this ){
			
			return((BuddyPluginBuddy)buddies_map.get( key ));
		}
	}
	
	public PluginInterface
	getPluginInterface()
	{
		return( plugin_interface );
	}
	
	protected SESecurityManager
	getSecurityManager()
	{
		return( sec_man );
	}
	
	protected GenericMessageRegistration
	getMessageRegistration()
	{
		return( msg_registration );
	}
	
		/**
		 * Returns authorised buddies only
		 */
	
	public List<BuddyPluginBuddy>
	getBuddies()
	{
		synchronized( this ){
			
			List<BuddyPluginBuddy>	result = new ArrayList<BuddyPluginBuddy>();
			
			for (int i=0;i<buddies.size();i++){
				
				BuddyPluginBuddy	buddy = (BuddyPluginBuddy)buddies.get(i);
				
				if ( buddy.isAuthorised()){
					
					result.add( buddy );
				}
			}
			
			return( result );
		}
	}
	
	protected List<BuddyPluginBuddy>
	getAllBuddies()
	{
		synchronized( this ){
			
			return( new ArrayList<BuddyPluginBuddy>( buddies ));
		}
	}
	
	public boolean
	isAvailable()
	{
		try{
			checkAvailable();
			
			return( true );
			
		}catch( Throwable e ){
			
			return( false );
		}
	}
	
	protected void
	checkAvailable()
	
		throws BuddyPluginException
	{
		if ( initialisation_state == INIT_UNKNOWN ){
			
			throw( new BuddyPluginException( "Plugin not yet initialised" ));
			
		}else if ( initialisation_state == INIT_BAD ){
			
			throw( new BuddyPluginException( "Plugin unavailable" ));

		}
	}
	

	protected void
	fireInitialised(
		boolean		ok )
	{
		if ( ok ){
			
			initialisation_state = INIT_OK;
				
		}else{
			
			initialisation_state = INIT_BAD;
		}
		      
		persistentDispatchInit();
		
		if ( ok ){
			
			buddy_tracker.initialise();
		}
		
		List	 listeners_ref = listeners.getList();
		
		for (int i=0;i<listeners_ref.size();i++){

			try{
				((BuddyPluginListener)listeners_ref.get(i)).initialised( ok );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}	
	}
	
	public void
	addListener(
		BuddyPluginListener	listener )
	{
		if ( listeners.contains(listener) ){
			return;
		}

		listeners.add( listener );
		
		if ( initialisation_state != INIT_UNKNOWN ){
			
			listener.initialised( initialisation_state == INIT_OK );
		}
	}
	
	public void
	removeListener(
		BuddyPluginListener	listener )
	{
		listeners.remove( listener );
	}
	
	protected Map
	requestReceived(
		BuddyPluginBuddy		from_buddy,
		int						subsystem,
		Map						content )
	
		throws BuddyPluginException
	{
		List	 listeners_ref = request_listeners.getList();
		
		for (int i=0;i<listeners_ref.size();i++){
			
			try{
				Map reply = ((BuddyPluginBuddyRequestListener)listeners_ref.get(i)).requestReceived(from_buddy, subsystem, content);
				
				if ( reply != null ){
					
					return( reply );
				}
			}catch( BuddyPluginException e ){
				
				throw( e );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace( e );
				
				throw( new BuddyPluginException( "Request processing failed", e ));
			}
		}
		
		return( null );
	}
	
	protected void
   	fireAdded(
   		BuddyPluginBuddy		buddy )
   	{
		if ( buddy.isAuthorised()){

			buddy.setLocalAuthorisedRSSCategories( public_categories );
			
	   		List	 listeners_ref = listeners.getList();
	   		
	   		for (int i=0;i<listeners_ref.size();i++){
	   			
	   			try{
	   				((BuddyPluginListener)listeners_ref.get(i)).buddyAdded( buddy );
	 
	   			}catch( Throwable e ){
	   				
	   				Debug.printStackTrace( e );
	   			}
	   		}
		}
   	}
	
	protected void
   	fireRemoved(
   		BuddyPluginBuddy		buddy )
   	{
		if ( buddy.isAuthorised()){
			
	   		List	 listeners_ref = listeners.getList();
	   		
	   		for (int i=0;i<listeners_ref.size();i++){
	   			
	   			try{
	   				((BuddyPluginListener)listeners_ref.get(i)).buddyRemoved( buddy );
	 
	   			}catch( Throwable e ){
	   				
	   				Debug.printStackTrace( e );
	   			}
	   		}
		}
   	}
	
	protected void
   	fireDetailsChanged(
   		BuddyPluginBuddy		buddy )
   	{
		if ( buddy.isAuthorised()){
			
	   		List	 listeners_ref = listeners.getList();
	   		
	   		for (int i=0;i<listeners_ref.size();i++){
	   			
	   			try{
	   				((BuddyPluginListener)listeners_ref.get(i)).buddyChanged( buddy );
	 
	   			}catch( Throwable e ){
	   				
	   				Debug.printStackTrace( e );
	   			}
	   		}
		}
   	}
	
	protected void
   	fireYGM(
   		BuddyPluginBuddy[]		from_buddies )
   	{
   		List	 listeners_ref = request_listeners.getList();
   		
   		for (int i=0;i<listeners_ref.size();i++){
   			
   			try{
   				((BuddyPluginBuddyRequestListener)listeners_ref.get(i)).pendingMessages( from_buddies );
 
   			}catch( Throwable e ){
   				
   				Debug.printStackTrace( e );
   			}
   		}
   	}

	protected void
 	fireEnabledStateChanged()
 	{
		final boolean enabled = !plugin_interface.getPluginState().isDisabled() && isEnabled();

 		List	 listeners_ref = listeners.getList();
 		
 		for (int i=0;i<listeners_ref.size();i++){
 			
 			try{
 				((BuddyPluginListener)listeners_ref.get(i)).enabledStateChanged( enabled );

 			}catch( Throwable e ){
 				
 				Debug.printStackTrace( e );
 			}
 		}
 	}
	
	protected void
	rethrow(
		String		reason,
		Throwable	e )
	
		throws BuddyPluginException
	{
		logMessage( reason, e );

		if ( e instanceof CryptoManagerPasswordException ){
		
		
			throw( new BuddyPluginPasswordException(((CryptoManagerPasswordException)e).wasIncorrect(), reason, e ));
		
		}else{
		
			throw( new BuddyPluginException( reason, e ));
		}
	}
	

	public InputStream
	handleURLProtocol(
		 AZPluginConnection			connection,
		String						arg_str )
	
		throws IPCException
	{
		String[]	args = arg_str.split( "&" );
		
		String		pk 			= null;
		String		category	= "All";
		byte[]		hash		= null;
		
		for (String arg: args ){
			
			String[]	bits = arg.split( "=" );
			
			String	lhs = bits[0];
			String	rhs	= UrlUtils.decode( bits[1] );
			
			if ( lhs.equals( "pk" )){
				
				pk		= rhs;
				
			}else if ( lhs.equals( "cat" )){
				
				category = rhs;
				
			}else if ( lhs.equals( "hash" )){
				
				hash	= Base32.decode(rhs);
			}
		}
		
		if ( pk == null ){
			
			throw( new IPCException( "Public key missing from '" + arg_str + "'" ));
		}
		
		BuddyPluginBuddy	buddy	= getBuddyFromPublicKey( pk );

		if ( buddy == null ){
			
			throw( new IPCException( "Buddy with public key '" + pk + "' not found" ));
		}
		
		if ( hash == null ){
			
			return( handleUPRSS( connection, buddy, category ));
			
		}else{
			
			return( handleUPTorrent( connection, buddy, category, hash ));
		}
	}
	
	public InputStream
	handleUPRSS(
		final AZPluginConnection	connection,
		BuddyPluginBuddy			buddy,
		String						category )
	
		throws IPCException
	{
		if ( !buddy.isOnline( true )){
			
			throw( new IPCException( "Buddy isn't online" ));
		}
		
		Map<String,Object>	msg = new HashMap<String, Object>();

		final String if_mod 	= connection.getRequestProperty( "If-Modified-Since" );

		try{
			msg.put( "cat", category.getBytes( "UTF-8" ));
									
			if ( if_mod != null ){
				
				msg.put( "if_mod", if_mod );
			}
			
			// String etag		= connection.getRequestProperty( "If-None-Match" );
					
		}catch( Throwable e ){
			
			Debug.out( e );
		}
		
		final Object[] 		result 		= { null };
		final AESemaphore	result_sem 	= new AESemaphore( "BuddyPlugin:rss" );
				
		final String	etag = buddy.getPublicKey() + "-" + category;
		
		az2_handler.sendAZ2RSSMessage( 
			buddy,
			msg,
			new BuddyPluginAZ2TrackerListener()
			{
				public Map
				messageReceived(
					BuddyPluginBuddy	buddy,
					Map					message )
				{
					try{						
						byte[] bytes = (byte[])message.get( "rss" );
					
						ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
						
						result[0] = bais;
						
						connection.setHeaderField( "ETag", etag );
						
						byte[] b_last_mod = (byte[])message.get( "last_mod" );
						
						if ( b_last_mod != null ){
						
							String	last_mod = new String( b_last_mod, "UTF-8" );
							
							connection.setHeaderField( "Last-Modified", last_mod );
							
							if ( if_mod != null && if_mod.equals( last_mod ) && bytes.length == 0 ){
								
								connection.setResponse( HttpURLConnection.HTTP_NOT_MODIFIED, "Not Modified" );
							}
						}
						
						result_sem.release();
						
					}catch( Throwable e ){
					
						messageFailed( buddy, e );
					}
					
					return( null );
				}
				
				public void
				messageFailed(
					BuddyPluginBuddy	buddy,
					Throwable			cause )
				{					
					result[0] = new IPCException( "Read failed", cause );
					
					result_sem.release();
				}
			});
		
		result_sem.reserve( 60*1000 );
		
		if ( result[0] == null ){
			
			throw( new IPCException( "Timeout" ));
			
		}else if ( result[0] instanceof InputStream ){
			
			return((InputStream)result[0]);
			
		}else{
			
			throw((IPCException)result[0]);
		}
	}

	public InputStream
	handleUPTorrent(
		final AZPluginConnection	connection,
		final BuddyPluginBuddy		buddy,
		String						category,
		final byte[]				hash )
	
		throws IPCException
	{
		final long timeout = 120*1000;

		final Object[] 		result 		= { null };
		final AESemaphore	result_sem 	= new AESemaphore( "BuddyPlugin:upt" );
			
		log( "Attempting to download torrent for " + Base32.encode( hash ));
		
			// first try and get torrent direct from the buddy
		
		if ( buddy.isOnline( true )){

			try{
				
				Map<String,Object>	msg = new HashMap<String, Object>();
			
				try{
					msg.put( "cat", category.getBytes( "UTF-8" ));
									
					msg.put( "hash", hash );
							
				}catch( Throwable e ){
					
					Debug.out( e );
				}
							
				az2_handler.sendAZ2RSSMessage( 
					buddy,
					msg,
					new BuddyPluginAZ2TrackerListener()
					{
						private boolean	result_set;
					
						public Map
						messageReceived(
							BuddyPluginBuddy	buddy,
							Map					message )
						{
							try{						
								byte[] bytes = (byte[])message.get( "torrent" );
									
								log( "    torrent downloaded from buddy" );
								
								setResult( bytes );
																
							}catch( Throwable e ){
							
								messageFailed( buddy, e );
							}
							
							return( null );
						}
						
						public void
						messageFailed(
							BuddyPluginBuddy	buddy,
							Throwable			cause )
						{					
							setResult( new IPCException( "Read failed", cause ));
						}
						
						protected void
						setResult(
							Object		obj )
						{
							synchronized( result ){
								
								if ( result_set ){
									
									return;
								}
								
								result_set = true;
								
								if ( !( result[0] instanceof byte[] )){
								
									result[0] = obj;
								}
								
								result_sem.release();
							}
						}
					});
			}catch( Throwable e ){
				
				result[0] = new IPCException( "Buddy torrent get failed", e );
				
				result_sem.release();
			}
		}else{
			
			result[0] = new IPCException( "Buddy is offline" );

			result_sem.release();
		}
		
			// second try and get via magnet
		
		final MagnetPlugin	magnet_plugin = getMagnetPlugin();
		
		if ( magnet_plugin == null ){
		
			synchronized( result ){
				
				if ( result[0] == null ){
					
					result[0] = new IPCException( "Magnet plugin unavailable" );
				}
			}
				
			result_sem.release();
			
		}else{

			new AEThread2( "BuddyPlugin:mag", true )
			{
				private boolean result_set;
				
				public void
				run()
				{
					try{
						
						if ( buddy.isOnline( true )){
							
							Thread.sleep(10*1000);
						}
						
						synchronized( result ){

							if ( result[0] instanceof byte[] ){
								
								setResult( null );
								
								return;
							}
						}
						
						byte[] torrent_data = magnet_plugin.download(
							new MagnetPluginProgressListener()
							{
								public void
								reportSize(
									long	size )
								{
								}
								
								public void
								reportActivity(
									String	str )
								{
									log( "    MagnetDownload: " + str );
								}
								
								public void
								reportCompleteness(
									int		percent )
								{
								}
								
								public void
								reportContributor(
									InetSocketAddress	address )
								{
								}
								
								public boolean 
								verbose() 
								{
									return( false );
								}
								
								public boolean 
								cancelled() 
								{
									return( false );
								}
							},
							hash,
							"",
							new InetSocketAddress[0],
							timeout,
							MagnetPlugin.FL_NONE );
						
						if ( torrent_data == null ){
							
							setResult( new IPCException( "Magnet timeout" ));
													
						}else{					
							
							log( "    torrent downloaded from magnet" );

							setResult( torrent_data );
						}
					}catch( Throwable e ){
						
						setResult( new IPCException( "Magnet get failed", e ));
					}
				}
				
				protected void
				setResult(
					Object		obj )
				{
					synchronized( result ){
						
						if ( result_set ){
							
							return;
						}
						
						result_set = true;
						
						if ( obj != null ){
							
							if ( 	result[0] == null ||
									( obj instanceof byte[] && !( result[0] instanceof byte[] ))){
								
								result[0] = obj;
							}
						}
							
						result_sem.release();
					}
				}
			}.start();
		}
		
		long	start = SystemTime.getMonotonousTime();
				
		if ( result_sem.reserve(timeout )){
			
			if ( !( result[0] instanceof byte[] )){
		
				long	rem = timeout - ( SystemTime.getMonotonousTime() - start );
				
				if ( rem > 0 ){
				
					result_sem.reserve(rem );
				}
			}
		}
		
		if ( result[0] == null ){
			
			log( "    torrent download timeout" );

			throw( new IPCException( "Timeout" ));
			
		}else if ( result[0] instanceof byte[] ){
						
			return( new ByteArrayInputStream((byte[])result[0]));
			
		}else{
			
			IPCException error = (IPCException)result[0];
			
			log( "    torrent downloaded failed: " + Debug.getNestedExceptionMessage( error ));

			throw( error );
		}
	}

	protected MagnetPlugin
	getMagnetPlugin()
	{
		PluginInterface  pi  = AzureusCoreFactory.getSingleton().getPluginManager().getPluginInterfaceByClass( MagnetPlugin.class );
	
		if ( pi == null ){
			
			return( null );
		}
		
		return((MagnetPlugin)pi.getPlugin());
	}
	
	public feedDetails
	getRSS(
		BuddyPluginBuddy		buddy,
		String					category,
		String					if_mod )
	
		throws BuddyPluginException
	{
		if ( !buddy.isLocalRSSCategoryAuthorised( category )){
			
			throw( new BuddyPluginException( "Unauthorised category '" + category + "'" ));
		}
		
		buddy.localRSSCategoryRead( category );
		
		Download[] downloads = plugin_interface.getDownloadManager().getDownloads();
		
		List<Download>	selected_dls = new ArrayList<Download>();
		
		long	fingerprint	= 0;
		
		for (int i=0;i<downloads.length;i++){
			
			Download download = downloads[i];
			
			Torrent torrent = download.getTorrent();
			
			if ( torrent == null ){
				
				continue;
			}
			
			String dl_cat = download.getAttribute( ta_category );
		
			if ( 	category.equalsIgnoreCase( "all" ) ||
					( dl_cat != null && dl_cat.equals( category ))){
				
				if ( !TorrentUtils.isReallyPrivate( PluginCoreUtils.unwrap( torrent ))){
					
					selected_dls.add( download );
					
					byte[] hash = torrent.getHash();
					
					int	num = (hash[0]<<24)&0xff000000 | (hash[1] << 16)&0x00ff0000 | (hash[2] << 8)&0x0000ff00 | hash[3]&0x000000ff;

					fingerprint += num;
				}
			}
		}
		
		PluginConfig pc = plugin_interface.getPluginconfig();

		String	feed_finger_key = "feed_finger.category." + category;
		String	feed_date_key 	= "feed_date.category." + category;

		long	existing_fingerprint 	= pc.getPluginLongParameter( feed_finger_key, 0 );
		long	feed_date 				= pc.getPluginLongParameter( feed_date_key, 0 );

		long	now = SystemTime.getCurrentTime();

		if ( existing_fingerprint == fingerprint ){
			
				// update every now and then to pick up new peer/seed values
			
			if ( selected_dls.size() > 0 ){

				if ( 	now < feed_date ||
						now - feed_date > FEED_UPDATE_MIN_MILLIS ){
					
					feed_date = now;
				
					pc.setPluginParameter( feed_date_key, feed_date );
				}
			}
		}else{
			
			pc.setPluginParameter( feed_finger_key, fingerprint );
			
			
				// ensure feed date goes up
			
			if ( now <= feed_date ){
				
				feed_date++;
				
			}else{
				
				feed_date = now;
			}
			
			pc.setPluginParameter( feed_date_key, feed_date );
		}
		
		String last_modified = TimeFormatter.getHTTPDate( feed_date );

		if ( if_mod != null && if_mod.equals( last_modified )){
			
			return( new feedDetails( new byte[0], last_modified ));
		}
				
		ByteArrayOutputStream	os = new ByteArrayOutputStream();
			
		try{
			PrintWriter pw = new PrintWriter(new OutputStreamWriter( os, "UTF-8" ));
			
			pw.println( "<?xml version=\"1.0\" encoding=\"utf-8\"?>" );
			
			pw.println( "<rss version=\"2.0\" xmlns:vuze=\"http://www.vuze.com\">" );
			
			pw.println( "<channel>" );
			
			pw.println( "<title>" + escape( category ) + "</title>" );
			
			Collections.sort(
				selected_dls,
				new Comparator<Download>()
				{
					public int 
					compare(
						Download d1, 
						Download d2) 
					{
						long	added1 = getAddedTime( d1 )/1000;
						long	added2 = getAddedTime( d2 )/1000;
		
						return((int)(added2 - added1 ));
					}
				});
								
							
			pw.println(	"<pubDate>" + last_modified + "</pubDate>" );
		
			for (int i=0;i<selected_dls.size();i++){
				
				Download download = (Download)selected_dls.get( i );
				
				DownloadManager	core_download = PluginCoreUtils.unwrap( download );
				
				Torrent torrent = download.getTorrent();
				
				String	hash_str = Base32.encode( torrent.getHash());
				
				pw.println( "<item>" );
				
				pw.println( "<title>" + escape( download.getName()) + "</title>" );
				
				pw.println( "<guid>" + hash_str + "</guid>" );
				
				long added = core_download.getDownloadState().getLongParameter(DownloadManagerState.PARAM_DOWNLOAD_ADDED_TIME);
				
				pw.println(	"<pubDate>" + TimeFormatter.getHTTPDate( added ) + "</pubDate>" );
				
				pw.println(	"<vuze:size>" + torrent.getSize()+ "</vuze:size>" );
				pw.println(	"<vuze:assethash>" + hash_str + "</vuze:assethash>" );
				
				String url = "azplug:?id=azbuddy&name=Friends&arg=";
				
				String arg = "pk=" + getPublicKey() + "&cat=" + category + "&hash=" + Base32.encode(torrent.getHash());

				url += URLEncoder.encode( arg, "UTF-8" );
			
				pw.println( "<vuze:downloadurl>" + escape( url ) + "</vuze:downloadurl>" );
		
				DownloadScrapeResult scrape = download.getLastScrapeResult();
				
				if ( scrape != null && scrape.getResponseType() == DownloadScrapeResult.RT_SUCCESS ){
					
					pw.println(	"<vuze:seeds>" + scrape.getSeedCount() + "</vuze:seeds>" );
					pw.println(	"<vuze:peers>" + scrape.getNonSeedCount() + "</vuze:peers>" );
				}
				
				pw.println( "</item>" );
			}
			
			pw.println( "</channel>" );
			
			pw.println( "</rss>" );
		
			pw.flush();
			
			return( new feedDetails( os.toByteArray(), last_modified ));
			
		}catch( IOException e ){
			
			throw( new BuddyPluginException( "", e ));
		}
	}
	
	public byte[]
	getRSSTorrent(
		BuddyPluginBuddy		buddy,
		String					category,
		byte[]					hash )
	
		throws BuddyPluginException
	{
		if ( !buddy.isLocalRSSCategoryAuthorised( category )){
			
			throw( new BuddyPluginException( "Unauthorised category '" + category + "'" ));
		}
		
		try{
			Download download = plugin_interface.getDownloadManager().getDownload( hash );
			
			if ( download != null ){
				
				Torrent	torrent = download.getTorrent();
			
				if ( torrent != null ){
								
					String dl_cat = download.getAttribute( ta_category );
					
					if ( 	category.equalsIgnoreCase( "all" ) ||
							( dl_cat != null && dl_cat.equals( category ))){
						
						if ( !TorrentUtils.isReallyPrivate( PluginCoreUtils.unwrap( torrent ))){
							
							torrent = torrent.removeAdditionalProperties();
							
							return( torrent.writeToBEncodedData());
						}
					}
				}
			}
		}catch( Throwable e ){
			
			throw( new BuddyPluginException( "getTorrent failed", e ));
		}
		
		throw( new BuddyPluginException( "Not found" ));
	}		

	protected long
	getAddedTime(
		Download	download )
	{
		DownloadManager	core_download = PluginCoreUtils.unwrap( download );
		
		return( core_download.getDownloadState().getLongParameter(DownloadManagerState.PARAM_DOWNLOAD_ADDED_TIME));
	}
	
	protected String
	escape(
		String	str )
	{
		return( XUXmlWriter.escapeXML(str));
	}
	
	public void
	addRequestListener(
		BuddyPluginBuddyRequestListener	listener )
	{
		request_listeners.add( listener );
	}
	
	public void
	removeRequestListener(
		BuddyPluginBuddyRequestListener	listener )
	{
		request_listeners.remove( listener );
	}
	
	public void
	logMessage(
		String		str,
		Throwable	e )
	{
		logMessage( str + ": " + Debug.getNestedExceptionMessage(e), true );
	}
	
	public void
	logMessage(
		String		str )
	{
		logMessage( str, false );
	}
	
	public void
	logMessage(
		String		str,
		boolean		is_error )
	{
		log( str );
		
		Iterator it = listeners.iterator();
		
		while( it.hasNext()){
			
			try{
				((BuddyPluginListener)it.next()).messageLogged( str, is_error );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace( e );
			}
		}
	}
	
	public void
	log(
		String		str )
	{
		logger.log( str );
	}
	
	public void
	log(
		String		str,
		Throwable	e )
	{
		logger.log( str + ": " + Debug.getNestedExceptionMessageAndStack( e ));
	}

	private class
	publishDetails
		implements Cloneable
	{
		private byte[]			public_key;
		private InetAddress		ip;
		private int				tcp_port;
		private int				udp_port;
		private String			nick_name;
		private int				online_status		= STATUS_ONLINE;
		
		private boolean			enabled;
		private boolean			published;
		
		private int				sequence;
		
		protected publishDetails
		getCopy()
		{
			try{
				publishDetails copy = (publishDetails)clone();
				
				copy.published = false;
				
				return( copy );
				
			}catch( Throwable e ){
				
				return( null);
			}
		}
		
		protected boolean
		isPublished()
		{
			return( published );
		}
		
		protected void
		setPublished(
			boolean		b )
		{
			published	= b;
		}
		
		protected boolean
		isEnabled()
		{
			return( enabled );
		}
		
		protected void
		setEnabled(
			boolean	_enabled )
		{
			enabled	= _enabled;
		}
		
		protected void
		setSequence(
			int		seq )
		{
			sequence = seq;
		}
		
		protected int
		getSequence()
		{
			return( sequence );
		}
		
		protected byte[]
		getPublicKey()
		{
			return( public_key );
		}
		
		protected void
		setPublicKey(
			byte[]		k )
		{
			public_key	= k;
		}
		
		protected InetAddress
		getIP()
		{
			return( ip );
		}
		
		protected void
		setIP(
			InetAddress	_ip )
		{
			ip	= _ip;
		}
		
		protected int
		getTCPPort()
		{
			return( tcp_port );
		}
		
		protected void
		setTCPPort(
			int		_port )
		{
			tcp_port = _port;
		}
		
		protected int
		getUDPPort()
		{
			return( udp_port );
		}
		
		protected void
		setUDPPort(
			int		_port )
		{
			udp_port = _port;
		}
		
		protected String
		getNickName()
		{
			return( nick_name );
		}
		
		protected void
		setNickName(
			String		 n )
		{
			nick_name	= n;
		}
		
		protected int
		getOnlineStatus()
		{
			return( online_status );
		}
		
		protected void
		setOnlineStatus(
			int		_status )
		{
			online_status = _status;
		}
		
		protected String
		getString()
		{
			return( "enabled=" + enabled + ",ip=" + ip + ",tcp=" + tcp_port + ",udp=" + udp_port + ",stat=" + online_status + ",key=" + (public_key==null?"<none>":Base32.encode( public_key )));
		}
	}
	
	protected interface
	operationListener
	{
		public void
		complete();
	}
	
	public interface
	cryptoResult
	{
		public byte[]
		getChallenge();
		
		public byte[]
		getPayload();
	}
	
	protected class
	feedDetails
	{
		private byte[]		contents;
		private String		last_modified;
		
		protected
		feedDetails(
			byte[]		_contents,
			String		_last_modified )
		{
			contents		= _contents;
			last_modified	= _last_modified;
		}
		
		protected byte[]
		getContent()
		{
			return( contents );
		}
		
		protected String
		getLastModified()
		{
			return( last_modified );
		}
	}
}
