code:
import json, os, time, traceback, collections

from twisted.web.static import File as FileResource

from lib.Actions.Import.FileSeeds import FileImportJob

from lib.Hydrus.API.Local import api_request_dialog_open, APIPermissions
from lib.Hydrus.Data import ContentUpdate, ConvertTimestampToPrettyExpires
from lib.Hydrus.Exceptions import BadRequestException, BandwidthException, \
	DataMissing, FileMissingException, InsufficientCredentialsException, \
	MissingCredentialsException, NotFoundException
from lib.Hydrus.Paths import GetTempPath, MirrorFile, ReadFileLikeAsBlocks
from lib.Hydrus.Search import FileSearchContext, Predicate
from lib.Hydrus.Server.Resources import HydrusResource, ResponseContext
from lib.Hydrus.Tags import CleanTags, SortNumericTags
from lib.Hydrus.Tags.Local import ServiceKeysToTags

from lib.Networking import ParsedRequestArguments, ParseTwistedRequestGETArgs

### CONSTANTS ###

import lib.Hydrus.Globals as HG
import lib.Hydrus.Constants as HC
import lib.Hydrus.Constants.Local as CC

from lib.Hydrus.Data.co import CLIENT_API_PERMISSION["ADD_TAGS"], CLIENT_API_PERMISSION["ADD_FILES"], \
	CLIENT_API_PERMISSION["ADD_URLS"], CLIENT_API_PERMISSION["SEARCH_FILES"]

local_booru_css = FileResource( os.path.join( HC.STATIC_DIR, 'local_booru_style.css' ), defaultType = 'text/css' )

LOCAL_BOORU_INT_PARAMS = set()
LOCAL_BOORU_BYTE_PARAMS = { 'share_key', 'hash' }
LOCAL_BOORU_STRING_PARAMS = set()
LOCAL_BOORU_JSON_PARAMS = set()

CLIENT_API_INT_PARAMS = { 'file_id' }
CLIENT_API_BYTE_PARAMS = { 'hash' }
CLIENT_API_STRING_PARAMS = { 'name', 'url' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'hashes', 'only_return_identifiers' }

### FUNCTIONS ###

def ParseLocalBooruGETArgs( requests_args ):
	args = ParseTwistedRequestGETArgs( requests_args, LOCAL_BOORU_INT_PARAMS, LOCAL_BOORU_BYTE_PARAMS, LOCAL_BOORU_STRING_PARAMS, LOCAL_BOORU_JSON_PARAMS )

	return args

def ParseClientAPIGETArgs( requests_args ):
	args = ParseTwistedRequestGETArgs( requests_args, CLIENT_API_INT_PARAMS, CLIENT_API_BYTE_PARAMS, CLIENT_API_STRING_PARAMS, CLIENT_API_JSON_PARAMS )

	return args

def ParseClientAPIPOSTHashesArgs( args ):
	if not isinstance( args, dict ):
		raise BadRequestException( 'The given parameter did not seem to be a JSON Object!' )

	parsed_request_args = ParsedRequestArguments( args )

	if 'hash' in parsed_request_args:
		try:
			hash = bytes.fromhex( parsed_request_args[ 'hash' ] )

			if len( hash ) == 0:
				del parsed_request_args[ 'hash' ]

			else:
				parsed_request_args[ 'hash' ] = hash

		except Exception:
			raise BadRequestException( 'I was expecting to parse \'' + 'hash' + '\' as a hex-encoded bytestring, but it failed.' )

	if 'hashes' in parsed_request_args:
		try:
			hashes = [ bytes.fromhex( hash_hex ) for hash_hex in parsed_request_args[ 'hashes' ] ]

			hashes = [ hash for hash in hashes if len( hash ) > 0 ]

			if len( hashes ) == 0:
				del parsed_request_args[ 'hashes' ]

			else:
				parsed_request_args[ 'hashes' ] = hashes

		except Exception:
			raise BadRequestException( 'I was expecting to parse \'' + 'hashes' + '\' as a list of hex-encoded bytestrings, but it failed.' )

	return parsed_request_args

def ParseClientAPIPOSTArgs( request ):
	request.content.seek( 0 )

	if not request.requestHeaders.hasHeader( 'Content-Type' ):
		parsed_request_args = ParsedRequestArguments()

		total_bytes_read = 0

	else:
		content_types = request.requestHeaders.getRawHeaders( 'Content-Type' )

		content_type = content_types[0]

		try:
			mime = HC.mime_enum_lookup[ content_type ]

		except Exception:
			raise BadRequestException( 'Did not recognise Content-Type header!' )

		total_bytes_read = 0

		if mime == HC.APPLICATION_JSON:
			json_bytes = request.content.read()

			total_bytes_read += len( json_bytes )

			json_string = str( json_bytes, 'utf-8' )

			args = json.loads( json_string )

			parsed_request_args = ParseClientAPIPOSTHashesArgs( args )

		else:
			parsed_request_args = ParsedRequestArguments()

			( os_file_handle, temp_path ) = GetTempPath()

			request.temp_file_info = ( os_file_handle, temp_path )

			with open( temp_path, 'wb' ) as f:
				for block in ReadFileLikeAsBlocks( request.content ):
					f.write( block )

					total_bytes_read += len( block )

	return ( parsed_request_args, total_bytes_read )

def ParseClientAPISearchPredicates( request ):
	default_search_values = {}

	default_search_values[ 'tags' ] = []
	default_search_values[ 'system_inbox' ] = False
	default_search_values[ 'system_archive' ] = False

	for ( key, value ) in default_search_values.items():
		if key not in request.parsed_request_args:
			request.parsed_request_args[ key ] = value

	tags = request.parsed_request_args[ 'tags' ]
	system_inbox = request.parsed_request_args[ 'system_inbox' ]
	system_archive = request.parsed_request_args[ 'system_archive' ]

	negated_tags = [ tag for tag in tags if tag.startswith( '-' ) ]
	tags = [ tag for tag in tags if not tag.startswith( '-' ) ]

	negated_tags = CleanTags( negated_tags )
	tags = CleanTags( tags )

	request.client_api_permissions.CheckCanSearchTags( tags )

	predicates = []

	for tag in negated_tags:
		predicates.append( Predicate( predicate_type = HC.PREDICATE_TYPE_TAG, value = tag, inclusive = False ) )

	for tag in tags:
		predicates.append( Predicate( predicate_type = HC.PREDICATE_TYPE_TAG, value = tag ) )

	if system_inbox:
		predicates.append( Predicate( predicate_type = HC.PREDICATE_TYPE_SYSTEM_INBOX ) )

	elif system_archive:
		predicates.append( Predicate( predicate_type = HC.PREDICATE_TYPE_SYSTEM_ARCHIVE ) )

	return predicates

class HydrusResourceBooru( HydrusResource ):
	def _callbackParseGETArgs( self, request ):

		parsed_request_args = ParseLocalBooruGETArgs( request.args )

		request.parsed_request_args = parsed_request_args

		return request

	def _callbackParsePOSTArgs( self, request ):
		return request

	def _reportDataUsed( self, request, num_bytes ):
		self._service.ReportDataUsed( num_bytes )

	def _reportRequestUsed( self, request ):
		self._service.ReportRequestUsed()

	def _checkService( self, request ):
		HydrusResource._checkService( self, request )

		if not self._service.BandwidthOK():
			raise BandwidthException( 'This service has run out of bandwidth. Please try again later.' )

class HydrusResourceBooruFile( HydrusResourceBooru ):
	def _threadDoGETJob( self, request ):

		share_key = request.parsed_request_args[ 'share_key' ]
		hash = request.parsed_request_args[ 'hash' ]

		HG.client_controller.local_booru_manager.CheckFileAuthorised( share_key, hash )

		media_result = HG.client_controller.local_booru_manager.GetMediaResult( share_key, hash )

		mime = media_result.GetMime()

		client_files_manager = HG.client_controller.client_files_manager

		path = client_files_manager.GetFilePath( hash, mime )

		response_context = ResponseContext( 200, mime = mime, path = path )

		return response_context

class HydrusResourceBooruGallery( HydrusResourceBooru ):
	def _threadDoGETJob( self, request ):

		# in future, make this a standard frame with a search key that'll load xml or yaml AJAX stuff
		# with file info included, so the page can sort and whatever

		share_key = request.parsed_request_args[ 'share_key' ]

		local_booru_manager = HG.client_controller.local_booru_manager

		local_booru_manager.CheckShareAuthorised( share_key )

		( name, text, timeout, media_results ) = local_booru_manager.GetGalleryInfo( share_key )

		body = '''
	'''

		if name == '':
			body += '''
		<title>hydrus network local booru share</title>'''
		else:
			body += '''
		<title>''' + name + '''</title>'''

		body += '''

		<link href="hydrus.ico" rel="shortcut icon" />
		<link href="style.css" rel="stylesheet" type="text/css" />'''

		( thumbnail_width, thumbnail_height ) = HC.options[ 'thumbnail_dimensions' ]

		body += '''
		'''

		body += '''

	'''

		body += '''

This share ''' + ConvertTimestampToPrettyExpires( timeout ) + '''.
'''

		if name != '':
			body += '''

''' + name + '''
'''

		if text != '':
			newline = '''

'''

			body += '''

''' + text.replace( os.linesep, newline ).replace( '\n', newline ) + '''
'''

		body+= '''

'''

		for media_result in media_results:
			hash = media_result.GetHash()
			# mime = media_result.GetMime()

			# if mime in flash or pdf or whatever, get other thumbnail

			body += '''

			'''

		body += '''

hydrus network

'''

		response_context = ResponseContext( 200, mime = HC.TEXT_HTML, body = body )

		return response_context

class HydrusResourceBooruPage( HydrusResourceBooru ):
	def _threadDoGETJob( self, request ):

		share_key = request.parsed_request_args[ 'share_key' ]
		hash = request.parsed_request_args[ 'hash' ]

		local_booru_manager = HG.client_controller.local_booru_manager

		local_booru_manager.CheckFileAuthorised( share_key, hash )

		( name, text, timeout, media_result ) = local_booru_manager.GetPageInfo( share_key, hash )

		body = '''
	'''

		if name == '':
			body += '''
		<title>hydrus network local booru share</title>'''
		else:
			body += '''
		<title>''' + name + '''</title>'''

		body += '''

		<link href="hydrus.ico" rel="shortcut icon" />
		<link href="style.css" rel="stylesheet" type="text/css" />'''

		body += '''

	'''

		body += '''

This share ''' + ConvertTimestampToPrettyExpires( timeout ) + '''.
'''

		if name != '':
			body += '''

''' + name + '''
'''

		if text != '':
			newline = '''

'''

			body += '''

''' + text.replace( os.linesep, newline ).replace( '\n', newline ) + '''
'''

		body+= '''

'''

		mime = media_result.GetMime()

		if mime in HC.IMAGES:
			( width, height ) = media_result.GetResolution()

			body += '''
			'''

		elif mime in HC.VIDEO:
			( width, height ) = media_result.GetResolution()

			body += '''
			<video width="''' + str( width ) + '''" height="''' + str( height ) + '''" controls="" loop="" autoplay="" src="file?share_key=''' + share_key.hex() + '''&hash=''' + hash.hex() + '''" />

link to ''' + HC.mime_string_lookup[ mime ] + ''' file
'''

		elif mime == HC.APPLICATION_FLASH:
			( width, height ) = media_result.GetResolution()

			body += '''
			<embed width="''' + str( width ) + '''" height="''' + str( height ) + '''" src="file?share_key=''' + share_key.hex() + '''&hash=''' + hash.hex() + '''" />

link to ''' + HC.mime_string_lookup[ mime ] + ''' file
'''

		else:
			body += '''

link to ''' + HC.mime_string_lookup[ mime ] + ''' file
'''

		body += '''

hydrus network

'''

		response_context = ResponseContext( 200, mime = HC.TEXT_HTML, body = body )

		return response_context

class HydrusResourceBooruThumbnail( HydrusResourceBooru ):
	def _threadDoGETJob( self, request ):

		share_key = request.parsed_request_args[ 'share_key' ]
		hash = request.parsed_request_args[ 'hash' ]

		local_booru_manager = HG.client_controller.local_booru_manager

		local_booru_manager.CheckFileAuthorised( share_key, hash )

		media_result = local_booru_manager.GetMediaResult( share_key, hash )

		mime = media_result.GetMime()

		response_context_mime = HC.IMAGE_PNG

		if mime in HC.MIMES_WITH_THUMBNAILS:
			client_files_manager = HG.client_controller.client_files_manager

			path = client_files_manager.GetThumbnailPath( media_result )

			response_context_mime = HC.APPLICATION_UNKNOWN

		elif mime in HC.AUDIO:
			path = os.path.join( HC.STATIC_DIR, 'audio.png' )

		elif mime == HC.APPLICATION_PDF:
			path = os.path.join( HC.STATIC_DIR, 'pdf.png' )

		elif mime == HC.APPLICATION_PSD:
			path = os.path.join( HC.STATIC_DIR, 'psd.png' )

		else:
			path = os.path.join( HC.STATIC_DIR, 'hydrus.png' )

		response_context = ResponseContext( 200, mime = response_context_mime, path = path )

		return response_context

class HydrusResourceClientAPI( HydrusResource ):
	def _callbackParseGETArgs( self, request ):

		parsed_request_args = ParseClientAPIGETArgs( request.args )

		request.parsed_request_args = parsed_request_args

		return request

	def _callbackParsePOSTArgs( self, request ):
		( parsed_request_args, total_bytes_read ) = ParseClientAPIPOSTArgs( request )

		self._reportDataUsed( request, total_bytes_read )

		request.parsed_request_args = parsed_request_args

		return request

	def _ParseClientAPIAccessKey( self, request ):
		if not request.requestHeaders.hasHeader( 'Hydrus-Client-API-Access-Key' ):
			raise MissingCredentialsException( 'No Hydrus-Client-API-Access-Key header!' )

		access_key_texts = request.requestHeaders.getRawHeaders( 'Hydrus-Client-API-Access-Key' )

		access_key_text = access_key_texts[0]

		try:
			access_key = bytes.fromhex( access_key_text )

		except Exception:
			raise Exception( 'Problem parsing api access key!' )

		return access_key

	def _reportDataUsed( self, request, num_bytes ):
		self._service.ReportDataUsed( num_bytes )

	def _reportRequestUsed( self, request ):
		self._service.ReportRequestUsed()

	def _checkService( self, request ):
		HydrusResource._checkService( self, request )

		if not self._service.BandwidthOK():
			raise BandwidthException( 'This service has run out of bandwidth. Please try again later.' )

class HydrusResourceClientAPIPermissionsRequest( HydrusResourceClientAPI ):
	def _threadDoGETJob( self, request ):

		if not api_request_dialog_open:
			raise InsufficientCredentialsException( 'The permission registration dialog is not open. Please open it under "review services" in the hydrus client.' )

		name = request.parsed_request_args[ 'name' ]
		basic_permissions = request.parsed_request_args[ 'basic_permissions' ]

		basic_permissions = [ int( value ) for value in basic_permissions ]

		api_permissions = APIPermissions( name = name, basic_permissions = basic_permissions )

		# last_api_permissions_request = api_permissions

		access_key = api_permissions.GetAccessKey()

		body_dict = {}

		body_dict[ 'access_key' ] = access_key.hex()

		body = json.dumps( body_dict )

		response_context = ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )

		return response_context

class HydrusResourceClientAPIVerify( HydrusResourceClientAPI ):
	def _threadDoGETJob( self, request ):

		access_key = self._ParseClientAPIAccessKey( request )

		client_api_manager = HG.client_controller.client_api_manager

		try:
			api_permissions = client_api_manager.GetPermissions( access_key )

			basic_permissions = api_permissions.GetBasicPermissions()
			human_description = api_permissions.ToHumanString()

			body_dict = {}

			body_dict[ 'basic_permissions' ] = list( basic_permissions ) # set->list for json
			body_dict[ 'human_description' ] = human_description

			body = json.dumps( body_dict )

			response_context = ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )

		except DataMissing:
			raise InsufficientCredentialsException( 'Could not find that access key!' )

		return response_context

class HydrusResourceClientAPIVersion( HydrusResourceClientAPI ):
	def _threadDoGETJob( self, request ):

		body_dict = {}

		body_dict[ 'version' ] = HC.CLIENT_API_VERSION

		body = json.dumps( body_dict )

		response_context = ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )

		return response_context

class HydrusResourceClientAPIRestricted( HydrusResourceClientAPI ):
	def _callbackCheckRestrictions( self, request ):

		HydrusResource._callbackCheckRestrictions( self, request )

		self._EstablishAPIPermissions( request )

		self._CheckAPIPermissions( request )

		return request

	def _CheckAPIPermissions( self, request ):
		raise NotImplementedError()

	def _EstablishAPIPermissions( self, request ):
		access_key = self._ParseClientAPIAccessKey( request )

		try:
			api_permissions = HG.client_controller.client_api_manager.GetPermissions( access_key )

		except DataMissing:
			raise InsufficientCredentialsException( 'Could not find that access key in the list of permissions!' )

		request.client_api_permissions = api_permissions

class HydrusResourceClientAPIRestrictedAddFile( HydrusResourceClientAPIRestricted ):
	def _CheckAPIPermissions( self, request ):

		request.client_api_permissions.CheckPermission( CLIENT_API_PERMISSION["ADD_FILES"] )

	def _threadDoPOSTJob( self, request ):
		if not hasattr( request, 'temp_file_info' ):
			path = request.parsed_request_args[ 'path' ]

			if not os.path.exists( path ):
				raise BadRequestException( 'Path "{}" does not exist!'.format( path ) )

			( os_file_handle, temp_path ) = GetTempPath()

			request.temp_file_info = ( os_file_handle, temp_path )

			MirrorFile( path, temp_path )

		( os_file_handle, temp_path ) = request.temp_file_info

		file_import_options = HG.client_controller.new_options.GetDefaultFileImportOptions( 'quiet' )

		file_import_job = FileImportJob( temp_path, file_import_options )

		try:
			( status, hash, note ) = HG.client_controller.client_files_manager.ImportFile( file_import_job )

		except Exception:
			status = CC.STATUS_ERROR
			hash = file_import_job.GetHash()
			note = traceback.format_exc()

		body_dict = {}

		body_dict[ 'status' ] = status
		body_dict[ 'hash' ] = hash.hex()
		body_dict[ 'note' ] = note

		body = json.dumps( body_dict )

		response_context = ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )

		return response_context

class HydrusResourceClientAPIRestrictedAddTags( HydrusResourceClientAPIRestricted ):
	def _CheckAPIPermissions( self, request ):

		request.client_api_permissions.CheckPermission( CLIENT_API_PERMISSION["ADD_TAGS"] )

class HydrusResourceClientAPIRestrictedAddTagsAddTags( HydrusResourceClientAPIRestrictedAddTags ):
	def _threadDoPOSTJob( self, request ):

		hashes = set()

		if 'hash' in request.parsed_request_args:
			hash = request.parsed_request_args[ 'hash' ]

			hashes.add( hash )

		if 'hashes' in request.parsed_request_args:
			more_hashes = request.parsed_request_args[ 'hashes' ]

			hashes.update( more_hashes )

		if len( hashes ) == 0:
			raise BadRequestException( 'There were no hashes given!' )

		#

		add_siblings_and_parents = True

		if 'add_siblings_and_parents' in request.parsed_request_args:
			add_siblings_and_parents = request.parsed_request_args[ 'add_siblings_and_parents' ]

		service_keys_to_content_updates = collections.defaultdict( list )

		if 'service_names_to_tags' in request.parsed_request_args:
			service_names_to_tags = request.parsed_request_args[ 'service_names_to_tags' ]

			for ( service_name, tags ) in service_names_to_tags.items():
				try:
					service_key = HG.client_controller.services_manager.GetServiceKeyFromName( HC.TAG_SERVICES, service_name )

				except Exception:
					raise BadRequestException( 'Could not find the service "{}"!'.format( service_name ) )

				tags = CleanTags( tags )

				if len( tags ) == 0:
					continue

				if service_key == CC.LOCAL_TAG_SERVICE_KEY:
					content_action = HC.CONTENT_UPDATE_ADD

				else:
					content_action = HC.CONTENT_UPDATE_PEND

				if add_siblings_and_parents:
					siblings_manager = HG.client_controller.tag_siblings_manager

					tags = siblings_manager.CollapseTags( service_key, tags )

					parents_manager = HG.client_controller.tag_parents_manager

					tags = parents_manager.ExpandTags( service_key, tags )

				content_updates = [ ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, content_action, ( tag, hashes ) ) for tag in tags ]

				service_keys_to_content_updates[ service_key ].extend( content_updates )

		if 'service_names_to_actions_to_tags' in request.parsed_request_args:
			service_names_to_actions_to_tags = request.parsed_request_args[ 'service_names_to_actions_to_tags' ]

			for ( service_name, actions_to_tags ) in service_names_to_actions_to_tags.items():
				try:
					service_key = HG.client_controller.services_manager.GetServiceKeyFromName( HC.TAG_SERVICES, service_name )

				except Exception:
					raise BadRequestException( 'Could not find the service "{}"!'.format( service_name ) )

				for ( content_action, tags ) in actions_to_tags.items():
					content_action = int( content_action )

					tags = CleanTags( tags )

					if len( tags ) == 0:
						continue

					if service_key == CC.LOCAL_TAG_SERVICE_KEY:
						if content_action not in ( HC.CONTENT_UPDATE_ADD, HC.CONTENT_UPDATE_DELETE ):
							continue

					else:
						if content_action in ( HC.CONTENT_UPDATE_ADD, HC.CONTENT_UPDATE_DELETE ):
							continue

					if content_action in ( HC.CONTENT_UPDATE_ADD, HC.CONTENT_UPDATE_PEND ) and add_siblings_and_parents:
						siblings_manager = HG.client_controller.tag_siblings_manager

						tags = siblings_manager.CollapseTags( service_key, tags )

						parents_manager = HG.client_controller.tag_parents_manager

						tags = parents_manager.ExpandTags( service_key, tags )

					if content_action == HC.CONTENT_UPDATE_PETITION:
						if isinstance( tags[0], str ):
							tags_and_reasons = [ ( tag, 'Petitioned from API' ) for tag in tags ]

						else:
							tags_and_reasons = tags

						content_updates = [ ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, content_action, ( tag, hashes ), reason = reason ) for ( tag, reason ) in tags_and_reasons ]

					else:
						content_updates = [ ContentUpdate( HC.CONTENT_TYPE_MAPPINGS, content_action, ( tag, hashes ) ) for tag in tags ]

					service_keys_to_content_updates[ service_key ].extend( content_updates )

		if len( service_keys_to_content_updates ) > 0:
			HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )

		response_context = ResponseContext( 200 )

		return response_context

class HydrusResourceClientAPIRestrictedAddTagsGetTagServices( HydrusResourceClientAPIRestrictedAddTags ):
	def _threadDoGETJob( self, request ):

		local_tags = HG.client_controller.services_manager.GetServices( ( HC.LOCAL_TAG, ) )
		tag_repos = HG.client_controller.services_manager.GetServices( ( HC.TAG_REPOSITORY, ) )

		body_dict = {}

		body_dict[ 'local_tags' ] = [ service.GetName() for service in local_tags ]
		body_dict[ 'tag_repositories' ] = [ service.GetName() for service in tag_repos ]

		body = json.dumps( body_dict )

		response_context = ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )

		return response_context

class HydrusResourceClientAPIRestrictedAddTagsCleanTags( HydrusResourceClientAPIRestrictedAddTags ):
	def _threadDoGETJob( self, request ):

		tags = request.parsed_request_args[ 'tags' ]

		tags = list( CleanTags( tags ) )

		tags = SortNumericTags( tags )

		body_dict = {}

		body_dict[ 'tags' ] = tags

		body = json.dumps( body_dict )

		response_context = ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )

		return response_context

class HydrusResourceClientAPIRestrictedAddURLs( HydrusResourceClientAPIRestricted ):
	def _CheckAPIPermissions( self, request ):

		request.client_api_permissions.CheckPermission( CLIENT_API_PERMISSION["ADD_URLS"] )

class HydrusResourceClientAPIRestrictedAddURLsAssociateURL( HydrusResourceClientAPIRestrictedAddURLs ):
	def _threadDoPOSTJob( self, request ):

		urls_to_add = []

		if 'url_to_add' in request.parsed_request_args:
			url = request.parsed_request_args[ 'url_to_add' ]

			if not isinstance( url, str ):
				raise BadRequestException( 'Did not understand the given URL "{}"!'.format( url ) )

			urls_to_add.append( url )

		if 'urls_to_add' in request.parsed_request_args:
			urls = request.parsed_request_args[ 'urls_to_add' ]

			if not isinstance( urls, list ):
				raise BadRequestException( 'Did not understand the given URLs!' )

			for url in urls:
				if not isinstance( url, str ):
					continue

				urls_to_add.append( url )

		urls_to_delete = []

		if 'url_to_delete' in request.parsed_request_args:
			url = request.parsed_request_args[ 'url_to_delete' ]

			if not isinstance( url, str ):
				raise BadRequestException( 'Did not understand the given URL "{}"!'.format( url ) )

			urls_to_delete.append( url )

		if 'urls_to_delete' in request.parsed_request_args:
			urls = request.parsed_request_args[ 'urls_to_delete' ]

			if not isinstance( urls, list ):
				raise BadRequestException( 'Did not understand the given URLs!' )

			for url in urls:
				if not isinstance( url, str ):
					continue

				urls_to_delete.append( url )

		applicable_hashes = []

		if 'hash' in request.parsed_request_args:
			applicable_hashes.append( request.parsed_request_args[ 'hash' ] )

		if 'hashes' in request.parsed_request_args:
			applicable_hashes.extend( request.parsed_request_args[ 'hashes' ] )

		if len( urls_to_add ) == 0 and len( urls_to_delete ) == 0:
			raise BadRequestException( 'Did not find any URLs to add!' )

		service_keys_to_content_updates = collections.defaultdict( list )

		if len( urls_to_add ) > 0:
			content_update = ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_ADD, ( urls_to_add, applicable_hashes ) )

			service_keys_to_content_updates[ CC.COMBINED_LOCAL_FILE_SERVICE_KEY ].append( content_update )

		if len( urls_to_delete ) > 0:
			content_update = ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_DELETE, ( urls_to_delete, applicable_hashes ) )

			service_keys_to_content_updates[ CC.COMBINED_LOCAL_FILE_SERVICE_KEY ].append( content_update )

		HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )

		response_context = ResponseContext( 200 )

		return response_context

class HydrusResourceClientAPIRestrictedAddURLsGetURLFiles( HydrusResourceClientAPIRestrictedAddURLs ):
	def _threadDoGETJob( self, request ):

		url = request.parsed_request_args[ 'url' ]

		normalised_url = HG.client_controller.network_engine.domain_manager.NormaliseURL( url )

		url_statuses = HG.client_controller.Read( 'url_statuses', normalised_url )

		json_happy_url_statuses = []

		for ( status, hash, note ) in url_statuses:
			d = {}

			d[ 'status' ] = status
			d[ 'hash' ] = hash.hex()
			d[ 'note' ] = note

			json_happy_url_statuses.append( d )

		body_dict = { 'normalised_url' : normalised_url, 'url_file_statuses' : json_happy_url_statuses }

		body = json.dumps( body_dict )

		response_context = ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )

		return response_context

class HydrusResourceClientAPIRestrictedAddURLsGetURLInfo( HydrusResourceClientAPIRestrictedAddURLs ):
	def _threadDoGETJob( self, request ):

		url = request.parsed_request_args[ 'url' ]

		normalised_url = HG.client_controller.network_engine.domain_manager.NormaliseURL( url )

		( url_type, match_name, can_parse ) = HG.client_controller.network_engine.domain_manager.GetURLParseCapability( normalised_url )

		body_dict = { 'normalised_url' : normalised_url, 'url_type' : url_type, 'url_type_string' : HC.url_type_string_lookup[ url_type ], 'match_name' : match_name, 'can_parse' : can_parse }

		body = json.dumps( body_dict )

		response_context = ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )

		return response_context

class HydrusResourceClientAPIRestrictedAddURLsImportURL( HydrusResourceClientAPIRestrictedAddURLs ):
	def _threadDoPOSTJob( self, request ):

		url = request.parsed_request_args[ 'url' ]

		service_keys_to_tags = None

		if 'service_names_to_tags' in request.parsed_request_args:
			service_keys_to_tags = ServiceKeysToTags()

			request.client_api_permissions.CheckPermission( CLIENT_API_PERMISSION["ADD_TAGS"] )

			service_names_to_tags = request.parsed_request_args[ 'service_names_to_tags' ]

			for ( service_name, tags ) in service_names_to_tags.items():
				try:
					service_key = HG.client_controller.services_manager.GetServiceKeyFromName( HC.TAG_SERVICES, service_name )

				except Exception:
					raise BadRequestException( 'Could not find the service "{}"!'.format( service_name ) )

				tags = CleanTags( tags )

				if len( tags ) == 0:
					continue

				service_keys_to_tags[ service_key ] = tags

		destination_page_name = None

		if 'destination_page_name' in request.parsed_request_args:
			destination_page_name = request.parsed_request_args[ 'destination_page_name' ]

			if not isinstance( destination_page_name, str ):
				raise BadRequestException( '"destination_page_name" did not seem to be a string!' )

		show_destination_page = False

		if 'show_destination_page' in request.parsed_request_args:
			show_destination_page = request.parsed_request_args[ 'show_destination_page' ]

			if not isinstance( show_destination_page, bool ):
				raise BadRequestException( '"show_destination_page" did not seem to be a boolean!' )

		gui = HG.client_controller.gui

		( normalised_url, result_text ) = HG.client_controller.CallBlockingToWX( gui, gui.ImportURLFromAPI, url, service_keys_to_tags, destination_page_name, show_destination_page )

		time.sleep( 0.05 ) # yield and give the ui time to catch up with new URL pubsubs in case this is being spammed

		body_dict = { 'human_result_text' : result_text, 'normalised_url' : normalised_url }

		body = json.dumps( body_dict )

		response_context = ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )

		return response_context

class HydrusResourceClientAPIRestrictedGetFiles( HydrusResourceClientAPIRestricted ):
	def _CheckAPIPermissions( self, request ):
		request.client_api_permissions.CheckPermission( CLIENT_API_PERMISSION["SEARCH_FILES"] )

class HydrusResourceClientAPIRestrictedGetFilesSearchFiles( HydrusResourceClientAPIRestrictedGetFiles ):
	def _threadDoGETJob( self, request ):
		predicates = ParseClientAPISearchPredicates( request )

		file_search_context = FileSearchContext( file_service_key = CC.LOCAL_FILE_SERVICE_KEY, tag_service_key = CC.COMBINED_TAG_SERVICE_KEY, predicates = predicates )

		hash_ids = HG.client_controller.Read( 'file_query_ids', file_search_context )

		request.client_api_permissions.SetLastSearchResults( hash_ids )

		body_dict = { 'file_ids' : list( hash_ids ) }

		body = json.dumps( body_dict )

		response_context = ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )

		return response_context

class HydrusResourceClientAPIRestrictedGetFilesGetFile( HydrusResourceClientAPIRestrictedGetFiles ):
	def _threadDoGETJob( self, request ):
		try:
			if 'file_id' in request.parsed_request_args:
				file_id = request.parsed_request_args[ 'file_id' ]

				request.client_api_permissions.CheckPermissionToSeeFiles( ( file_id, ) )

				( media_result, ) = HG.client_controller.Read( 'media_results_from_ids', ( file_id, ) )

			elif 'hash' in request.parsed_request_args:
				request.client_api_permissions.CheckCanSeeAllFiles()

				hash = request.parsed_request_args[ 'hash' ]

				( media_result, ) = HG.client_controller.Read( 'media_results', ( hash, ) )

			else:
				raise BadRequestException( 'Please include a file_id or hash parameter!' )

		except DataMissing:
			raise NotFoundException( 'One or more of those file identifiers was missing!' )

		try:
			hash = media_result.GetHash()
			mime = media_result.GetMime()

			path = HG.client_controller.client_files_manager.GetFilePath( hash, mime )

		except FileMissingException:
			raise NotFoundException( 'Could not find that file!' )

		response_context = ResponseContext( 200, mime = mime, path = path )

		return response_context

class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClientAPIRestrictedGetFiles ):
	def _threadDoGETJob( self, request ):
		only_return_identifiers = False

		if 'only_return_identifiers' in request.parsed_request_args:
			only_return_identifiers = request.parsed_request_args[ 'only_return_identifiers' ]

		try:
			if 'file_ids' in request.parsed_request_args:
				file_ids = request.parsed_request_args[ 'file_ids' ]

				request.client_api_permissions.CheckPermissionToSeeFiles( file_ids )

				if only_return_identifiers:
					file_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hash_ids = file_ids )

				else:
					media_results = HG.client_controller.Read( 'media_results_from_ids', file_ids )

			elif 'hashes' in request.parsed_request_args:
				request.client_api_permissions.CheckCanSeeAllFiles()

				hashes_hex = request.parsed_request_args[ 'hashes' ]

				hashes = [ bytes.fromhex( hash ) for hash in hashes_hex ]

				if only_return_identifiers:
					file_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hashes = hashes )

				else:
					media_results = HG.client_controller.Read( 'media_results', hashes )

			else:
				raise BadRequestException( 'Please include a file_ids or hashes parameter!' )

		except DataMissing:
			raise NotFoundException( 'One or more of those file identifiers was missing!' )

		body_dict = {}

		metadata = []

		if only_return_identifiers:
			for ( file_id, hash ) in file_ids_to_hashes.items():
				metadata_row = {}

				metadata_row[ 'file_id' ] = file_id
				metadata_row[ 'hash' ] = hash.hex()

				metadata.append( metadata_row )

		else:
			services_manager = HG.client_controller.services_manager

			service_keys_to_names = {}

			for media_result in media_results:
				metadata_row = {}

				file_info_manager = media_result.GetFileInfoManager()

				metadata_row[ 'file_id' ] = file_info_manager.hash_id
				metadata_row[ 'hash' ] = file_info_manager.hash.hex()
				metadata_row[ 'size' ] = file_info_manager.size
				metadata_row[ 'mime' ] = HC.mime_string_lookup[ file_info_manager.mime ]
				metadata_row[ 'width' ] = file_info_manager.width
				metadata_row[ 'height' ] = file_info_manager.height
				metadata_row[ 'duration' ] = file_info_manager.duration
				metadata_row[ 'num_frames' ] = file_info_manager.num_frames
				metadata_row[ 'num_words' ] = file_info_manager.num_words

				tags_manager = media_result.GetTagsManager()

				service_names_to_statuses_to_tags = {}

				service_keys_to_statuses_to_tags = tags_manager.GetServiceKeysToStatusesToTags()

				for ( service_key, statuses_to_tags ) in service_keys_to_statuses_to_tags.items():
					if service_key not in service_keys_to_names:
						service_keys_to_names[ service_key ] = services_manager.GetName( service_key )

					service_name = service_keys_to_names[ service_key ]

					service_names_to_statuses_to_tags[ service_name ] = { str( status ) : list( tags ) for ( status, tags ) in statuses_to_tags.items() }

				metadata_row[ 'service_names_to_statuses_to_tags' ] = service_names_to_statuses_to_tags

				metadata.append( metadata_row )

		body_dict[ 'metadata' ] = metadata

		mime = HC.APPLICATION_JSON
		body = json.dumps( body_dict )

		response_context = ResponseContext( 200, mime = mime, body = body )

		return response_context

class HydrusResourceClientAPIRestrictedGetFilesGetThumbnail( HydrusResourceClientAPIRestrictedGetFiles ):
	def _threadDoGETJob( self, request ):
		try:
			if 'file_id' in request.parsed_request_args:
				file_id = request.parsed_request_args[ 'file_id' ]

				request.client_api_permissions.CheckPermissionToSeeFiles( ( file_id, ) )

				( media_result, ) = HG.client_controller.Read( 'media_results_from_ids', ( file_id, ) )

			elif 'hash' in request.parsed_request_args:
				request.client_api_permissions.CheckCanSeeAllFiles()

				hash = request.parsed_request_args[ 'hash' ]

				( media_result, ) = HG.client_controller.Read( 'media_results', ( hash, ) )

			else:
				raise BadRequestException( 'Please include a file_id or hash parameter!' )

		except DataMissing:
			raise NotFoundException( 'One or more of those file identifiers was missing!' )

		try:
			path = HG.client_controller.client_files_manager.GetThumbnailPath( media_result )

		except FileMissingException:
			raise NotFoundException( 'Could not find that file!' )

		response_context = ResponseContext( 200, mime = HC.APPLICATION_OCTET_STREAM, path = path )

		return response_context


code:
import http.cookies

from twisted.web.resource import Resource

from lib.Hydrus.Data import GetNow
from lib.Hydrus.Paths import GetTempPath, ReadFileLikeAsBlocks
from lib.Hydrus.Exceptions import BandwidthException, MissingCredentialsException, \
	NotFoundException, BadRequestException
from lib.Hydrus.Files.Remote import GetFilePath, GetThumbnailPath
from lib.Hydrus.Serialisble import SerialisableList
from lib.Hydrus.Server import HydrusResource
from lib.Hydrus.Server.Resources import ResponseContext, ParseFileArguments

from lib.Networking import DumpHydrusArgsToNetworkBytes, ParsedRequestArguments, ParseHydrusNetworkGETArgs, \
	ParseNetworkBytesToParsedHydrusArgs

### CONSTANTS ###

import lib.Hydrus.Constants as HC
import lib.Hydrus.Globals as HG

### REMOTE SERVER CLASSES ###

class HydrusResourceBusyCheck( Resource ):
	def __init__( self ):
		Resource.__init__( self )

		self._server_version_string = HC.service_string_lookup[ HC.SERVER_ADMIN ] + '/' + str( HC.NETWORK_VERSION )

	def render_GET( self, request ):
		request.setResponseCode( 200 )
		request.setHeader( 'Server', self._server_version_string )

		if HG.server_busy:
			return b'1'

		else:
			return b'0'

class HydrusResourceHydrusNetwork( HydrusResource ):
	def _callbackParseGETArgs( self, request ):
		parsed_request_args = ParseHydrusNetworkGETArgs( request.args )
		request.parsed_request_args = parsed_request_args

		return request

	def _callbackParsePOSTArgs( self, request ):
		request.content.seek( 0 )

		if not request.requestHeaders.hasHeader( 'Content-Type' ):
			parsed_request_args = ParsedRequestArguments()

		else:
			content_types = request.requestHeaders.getRawHeaders( 'Content-Type' )
			content_type = content_types[0]

			try:
				mime = HC.mime_enum_lookup[ content_type ]

			except Exception:
				raise BadRequestException( 'Did not recognise Content-Type header!' )

			total_bytes_read = 0

			if mime == HC.APPLICATION_JSON:
				json_string = request.content.read()

				total_bytes_read += len( json_string )

				parsed_request_args = ParseNetworkBytesToParsedHydrusArgs( json_string )

			else:
				( os_file_handle, temp_path ) = GetTempPath()
				request.temp_file_info = ( os_file_handle, temp_path )

				with open( temp_path, 'wb' ) as f:
					for block in ReadFileLikeAsBlocks( request.content ):
						f.write( block )

						total_bytes_read += len( block )

				decompression_bombs_ok = self._DecompressionBombsOK( request )

				parsed_request_args = ParseFileArguments( temp_path, decompression_bombs_ok )

			self._reportDataUsed( request, total_bytes_read )

		request.parsed_request_args = parsed_request_args

		return request

class HydrusResourceAccessKey( HydrusResourceHydrusNetwork ):
	def _threadDoGETJob( self, request ):
		registration_key = request.parsed_request_args[ 'registration_key' ]
		access_key = HG.server_controller.Read( 'access_key', self._service_key, registration_key )

		body = DumpHydrusArgsToNetworkBytes( { 'access_key' : access_key } )
		response_context = ResponseContext( 200, body = body )

		return response_context

class HydrusResourceShutdown( HydrusResourceHydrusNetwork ):
	def _threadDoPOSTJob( self, request ):
		HG.server_controller.ShutdownFromServer()
		response_context = ResponseContext( 200 )

		return response_context

class HydrusResourceAccessKeyVerification( HydrusResourceHydrusNetwork ):
	def _threadDoGETJob( self, request ):
		access_key = self._parseHydrusNetworkAccessKey( request )
		verified = HG.server_controller.Read( 'verify_access_key', self._service_key, access_key )

		body = DumpHydrusArgsToNetworkBytes( { 'verified' :
	verified } )
		response_context = ResponseContext( 200, body = body )

		return response_context

class HydrusResourceSessionKey( HydrusResourceHydrusNetwork ):
	def _threadDoGETJob( self, request ):
		access_key = self._parseHydrusNetworkAccessKey( request )
		( session_key, expires ) = HG.server_controller.server_session_manager.AddSession( self._service_key, access_key )

		now = GetNow()
		max_age = expires - now
		cookies = [ ( 'session_key', session_key.hex(), { 'max_age' : str( max_age ), 'path' : '/' } ) ]
		response_context = ResponseContext( 200, cookies = cookies )

		return response_context

class HydrusResourceRestricted( HydrusResourceHydrusNetwork ):
	def _callbackCheckRestrictions( self, request ):
		HydrusResource._callbackCheckRestrictions( self, request )

		self._checkSession( request )
		self._checkAccount( request )

		return request

	def _checkAccount( self, request ):
		request.hydrus_account.CheckFunctional()

		return request

	def _checkBandwidth( self, request ):
		if not self._service.BandwidthOK():
			raise BandwidthException( 'This service has run out of bandwidth. Please try again later.' )

		if not HG.server_controller.ServerBandwidthOK():
			raise BandwidthException( 'This server has run out of bandwidth. Please try again later.' )

	def _checkSession( self, request ):
		if not request.requestHeaders.hasHeader( 'Cookie' ):
			raise MissingCredentialsException( 'No cookies found!' )

		cookie_texts = request.requestHeaders.getRawHeaders( 'Cookie' )
		cookie_text = cookie_texts[0]

		try:
			cookies = http.cookies.SimpleCookie( cookie_text )

			if 'session_key' not in cookies:
				session_key = None

			else:
				# Morsel, for real, ha ha ha
				morsel = cookies[ 'session_key' ]

				session_key_hex = morsel.value
				session_key = bytes.fromhex( session_key_hex )

		except Exception:
			raise Exception( 'Problem parsing cookies!' )

		account = HG.server_controller.server_session_manager.GetAccount( self._service_key, session_key )
		request.hydrus_account = account

		return request

	def _reportDataUsed( self, request, num_bytes ):
		HydrusResource._reportDataUsed( self, request, num_bytes )

		account = request.hydrus_account

		if account is not None:
			account.ReportDataUsed( num_bytes )

	def _reportRequestUsed( self, request ):
		HydrusResource._reportRequestUsed( self, request )

		account = request.hydrus_account

		if account is not None:
			account.ReportRequestUsed()

class HydrusResourceRestrictedAccount( HydrusResourceRestricted ):
	def _checkAccount( self, request ):
		# you can always fetch your account (e.g. to be notified that you are banned!)
		return request

	def _threadDoGETJob( self, request ):
		account = request.hydrus_account

		body = DumpHydrusArgsToNetworkBytes( { 'account' : account } )

		response_context = ResponseContext( 200, body = body )

		return response_context

class HydrusResourceRestrictedAccountInfo( HydrusResourceRestricted ):
	def _threadDoGETJob( self, request ):
		subject_identifier = request.parsed_request_args[ 'subject_identifier' ]

		if subject_identifier.HasAccountKey():
			subject_account_key = subject_identifier.GetData()

		else:
			raise MissingCredentialsException( 'I was expecting an account key, but did not get one!' )

		subject_account = HG.server_controller.Read( 'account', self._service_key, subject_account_key )
		account_info = HG.server_controller.Read( 'account_info', self._service_key, request.hydrus_account, subject_account )

		body = DumpHydrusArgsToNetworkBytes( { 'account_info' : account_info } )

		response_context = ResponseContext( 200, body = body )

		return response_context

class HydrusResourceRestrictedAccountModification( HydrusResourceRestricted ):
	def _threadDoPOSTJob( self, request ):
		action = request.parsed_request_args[ 'action' ]

		subject_accounts = request.parsed_request_args[ 'accounts' ]

		kwargs = request.parsed_request_args # for things like expires, title, and so on

		with HG.dirty_object_lock:
			HG.server_controller.WriteSynchronous( 'account_modification', self._service_key, request.hydrus_account, action, subject_accounts, **kwargs )
			HG.server_controller.server_session_manager.UpdateAccounts( self._service_key, subject_accounts )

		response_context = ResponseContext( 200 )

		return response_context

class HydrusResourceRestrictedAccountTypes( HydrusResourceRestricted ):
	def _threadDoGETJob( self, request ):
		account_types = HG.server_controller.Read( 'account_types', self._service_key, request.hydrus_account )

		body = DumpHydrusArgsToNetworkBytes( { 'account_types' : account_types } )

		response_context = ResponseContext( 200, body = body )

		return response_context

	def _threadDoPOSTJob( self, request ):
		account_types = request.parsed_request_args[ 'account_types' ]
		deletee_account_type_keys_to_new_account_type_keys = request.parsed_request_args[ 'deletee_account_type_keys_to_new_account_type_keys' ]

		HG.server_controller.WriteSynchronous( 'account_types', self._service_key, request.hydrus_account, account_types, deletee_account_type_keys_to_new_account_type_keys )
		HG.server_controller.server_session_manager.RefreshAccounts( self._service_key )

		response_context = ResponseContext( 200 )

		return response_context

class HydrusResourceRestrictedBackup( HydrusResourceRestricted ):
	def _threadDoPOSTJob( self, request ):
		# check permission here since this is an asynchronous job
		request.hydrus_account.CheckPermission( HC.CONTENT_TYPE_SERVICES, HC.PERMISSION_ACTION_OVERRULE )

		HG.server_controller.Write( 'backup' )

		response_context = ResponseContext( 200 )

		return response_context

class HydrusResourceRestrictedIP( HydrusResourceRestricted ):
	def _threadDoGETJob( self, request ):
		hash = request.parsed_request_args[ 'hash' ]
		( ip, timestamp ) = HG.server_controller.Read( 'ip', self._service_key, request.hydrus_account, hash )

		body = DumpHydrusArgsToNetworkBytes( { 'ip' : ip, 'timestamp' : timestamp } )

		response_context = ResponseContext( 200, body = body )

		return response_context

class HydrusResourceRestrictedNumPetitions( HydrusResourceRestricted ):
	def _threadDoGETJob( self, request ):
		petition_count_info = HG.server_controller.Read( 'num_petitions', self._service_key, request.hydrus_account )
		body = DumpHydrusArgsToNetworkBytes( { 'num_petitions' : petition_count_info } )

		response_context = ResponseContext( 200, body = body )

		return response_context

class HydrusResourceRestrictedPetition( HydrusResourceRestricted ):
	def _threadDoGETJob( self, request ):
		content_type = request.parsed_request_args[ 'content_type' ]
		status = request.parsed_request_args[ 'status' ]

		petition = HG.server_controller.Read( 'petition', self._service_key, request.hydrus_account, content_type, status )

		body = DumpHydrusArgsToNetworkBytes( { 'petition' : petition } )

		response_context = ResponseContext( 200, body = body )

		return response_context

class HydrusResourceRestrictedRegistrationKeys( HydrusResourceRestricted ):
	def _threadDoGETJob( self, request ):
		num = request.parsed_request_args[ 'num' ]
		account_type_key = request.parsed_request_args[ 'account_type_key' ]

		if 'expires' in request.parsed_request_args:
			expires = request.parsed_request_args[ 'expires' ]

		else:
			expires = None

		registration_keys = HG.server_controller.Read( 'registration_keys', self._service_key, request.hydrus_account, num, account_type_key, expires )

		body = DumpHydrusArgsToNetworkBytes( { 'registration_keys' : registration_keys } )

		response_context = ResponseContext( 200, body = body )

		return response_context

class HydrusResourceRestrictedRepositoryFile( HydrusResourceRestricted ):
	def _DecompressionBombsOK( self, request ):
		return request.hydrus_account.HasPermission( HC.CONTENT_TYPE_ACCOUNTS, HC.PERMISSION_ACTION_CREATE )

	def _threadDoGETJob( self, request ):
		self._checkBandwidth( request )

		# no permission check as any functional account can get files
		hash = request.parsed_request_args[ 'hash' ]

		( valid, mime ) = HG.server_controller.Read( 'service_has_file', self._service_key, hash )

		if not valid:
			raise NotFoundException( 'File not found on this service!' )

		path = GetFilePath( hash )

		response_context = ResponseContext( 200, mime = mime, path = path )

		return response_context

	def _threadDoPOSTJob( self, request ):
		file_dict = request.parsed_request_args

		if self._service.LogUploaderIPs():
			file_dict[ 'ip' ] = request.getClientIP()

		HG.server_controller.WriteSynchronous( 'file', self._service, request.hydrus_account, file_dict )

		response_context = ResponseContext( 200 )

		return response_context

class HydrusResourceRestrictedRepositoryThumbnail( HydrusResourceRestricted ):
	def _threadDoGETJob( self, request ):
		self._checkBandwidth( request )
		# no permission check as any functional account can get thumbnails

		hash = request.parsed_request_args[ 'hash' ]
		( valid, mime ) = HG.server_controller.Read( 'service_has_file', self._service_key, hash )

		if not valid:
			raise NotFoundException( 'Thumbnail not found on this service!' )

		if mime not in HC.MIMES_WITH_THUMBNAILS:
			raise NotFoundException( 'That mime should not have a thumbnail!' )

		path = GetThumbnailPath( hash )

		response_context = ResponseContext( 200, mime = HC.APPLICATION_OCTET_STREAM, path = path )

		return response_context

class HydrusResourceRestrictedServices( HydrusResourceRestricted ):
	def _threadDoGETJob( self, request ):
		services = HG.server_controller.Read( 'services_from_account', request.hydrus_account )

		body = DumpHydrusArgsToNetworkBytes( { 'services' : services } )

		response_context = ResponseContext( 200, body = body )

		return response_context

	def _threadDoPOSTJob( self, request ):
		services = request.parsed_request_args[ 'services' ]
		unique_ports = { service.GetPort() for service in services }

		if len( unique_ports ) < len( services ):
			raise BadRequestException( 'It looks like some of those services share ports! Please give them unique ports!' )

		with HG.dirty_object_lock:
			service_keys_to_access_keys = HG.server_controller.WriteSynchronous( 'services', request.hydrus_account, services )

			HG.server_controller.SetServices( services )

		body = DumpHydrusArgsToNetworkBytes( { 'service_keys_to_access_keys' : service_keys_to_access_keys } )

		response_context = ResponseContext( 200, body = body )

		return response_context

class HydrusResourceRestrictedUpdate( HydrusResourceRestricted ):
	def _threadDoGETJob( self, request ):
		self._checkBandwidth( request )
		# no permissions check as any functional account can get updates

		update_hash = request.parsed_request_args[ 'update_hash' ]

		if not self._service.HasUpdateHash( update_hash ):
			raise NotFoundException( 'This update hash does not exist on this service!' )

		path = GetFilePath( update_hash )

		response_context = ResponseContext( 200, mime = HC.APPLICATION_OCTET_STREAM, path = path )

		return response_context

	def _threadDoPOSTJob( self, request ):
		client_to_server_update = request.parsed_request_args[ 'client_to_server_update' ]
		HG.server_controller.WriteSynchronous( 'update', self._service_key, request.hydrus_account, client_to_server_update )

		response_context = ResponseContext( 200 )

		return response_context

class HydrusResourceRestrictedImmediateUpdate( HydrusResourceRestricted ):
	def _threadDoGETJob( self, request ):
		updates = HG.server_controller.Read( 'immediate_update', self._service_key, request.hydrus_account )
		updates = SerialisableList( updates )

		body = DumpHydrusArgsToNetworkBytes( { 'updates' : updates } )

		response_context = ResponseContext( 200, body = body )

		return response_context

class HydrusResourceRestrictedMetadataUpdate( HydrusResourceRestricted ):
	def _threadDoGETJob( self, request ):
		# no permissions check as any functional account can get metadata slices

		since = request.parsed_request_args[ 'since' ]

		metadata_slice = self._service.GetMetadataSlice( since )

		body = DumpHydrusArgsToNetworkBytes( { 'metadata_slice' : metadata_slice } )

		response_context = ResponseContext( 200, body = body )

		return response_context

license: wtfpl


code:
import os, time, traceback

from lib.Handling import GenerateThumbnailBytes, GetFileInfo, \
	GetHashFromPath, GetMime
from lib.Handling.Image import ConvertToPngIfBmp, GetThumbnailResolution, \
	IsDecompressionBomb

from lib.Networking import ParsedRequestArguments

from lib.Hydrus.Data import DebugPrint
from lib.Hydrus.Exceptions import BadRequestException, BandwidthException, \
	DataMissing, DoesNotSupportCORSException, FileMissingException, InsufficientCredentialsException, \
	MissingCredentialsException, NetworkVersionException, NotFoundException, ServerBusyException, \
	SessionException
from lib.Hydrus.Paths import CleanUpTempPath
from lib.Hydrus.Serialisable import SerialisableBase
from lib.Hydrus.Server.Resources.Eris import FormattedEris

from twisted.internet import reactor, defer
from twisted.internet.threads import deferToThread
from twisted.web.resource import Resource
from twisted.web.static import File as FileResource, NoRangeStaticProducer

### CONSTANTS ###

import lib.Hydrus.Constants as HC
import lib.Hydrus.Globals as HG

from twisted.web.server import NOT_DONE_YET

### FUNCTIONS ###

def GenerateEris( service ):
	name = service.GetName()
	service_type = service.GetServiceType()

	allows_non_local_connections = service.AllowsNonLocalConnections()

	welcome_text_1 = 'This is <b>' + name + '</b>,'
	welcome_text_2 = 'a ' + HC.service_string_lookup[ service_type ] + '.'
	welcome_text_3 = 'Software version ' + str( HC.SOFTWARE_VERSION )

	if service_type == HC.CLIENT_API_SERVICE:
		welcome_text_4 = 'API version ' + str( HC.CLIENT_API_VERSION )

	else:
		welcome_text_4 = 'Network version ' + str( HC.NETWORK_VERSION )

	if allows_non_local_connections:
		welcome_text_5 = 'It responds to requests from any host.'

	else:
		welcome_text_5 = 'It only responds to requests from localhost.'

	return FormattedEris(name, welcome_text_1, welcome_text_2, welcome_text_3, welcome_text_4, welcome_text_5)

def ParseFileArguments( path, decompression_bombs_ok = False ):
	ConvertToPngIfBmp( path )

	hash = GetHashFromPath( path )

	try:
		mime = GetMime( path )

		if mime in HC.DECOMPRESSION_BOMB_IMAGES and not decompression_bombs_ok:
			if IsDecompressionBomb( path ):
				raise InsufficientCredentialsException( 'File seemed to be a Decompression Bomb!' )

		( size, mime, width, height, duration, num_frames, num_words ) = GetFileInfo( path, mime )

	except Exception as e:
		raise BadRequestException( 'File ' + hash.hex() + ' could not parse: ' + str( e ) )

	args = ParsedRequestArguments()

	args[ 'path' ] = path
	args[ 'hash' ] = hash
	args[ 'size' ] = size
	args[ 'mime' ] = mime

	if width is not None:
		args[ 'width' ] = width
	if height is not None:
		args[ 'height' ] = height
	if duration is not None:
		args[ 'duration' ] = duration
	if num_frames is not None:
		args[ 'num_frames' ] = num_frames
	if num_words is not None:
		args[ 'num_words' ] = num_words

	if mime in HC.MIMES_WITH_THUMBNAILS:
		try:
			bounding_dimensions = HC.SERVER_THUMBNAIL_DIMENSIONS

			target_resolution = GetThumbnailResolution( ( width, height ), bounding_dimensions )

			thumbnail_bytes = GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames )

		except Exception:
			tb = traceback.format_exc()

			raise BadRequestException( 'Could not generate thumbnail from that file:' + os.linesep + tb )

		args[ 'thumbnail' ] = thumbnail_bytes

	return args

hydrus_favicon = FileResource( os.path.join( HC.STATIC_DIR, 'hydrus.ico' ), defaultType = 'image/x-icon' )

class HydrusDomain( object ):
	def __init__( self, local_only ):
		self._local_only = local_only

	def CheckValid( self, client_ip ):
		if self._local_only and client_ip != '127.0.0.1':
			raise InsufficientCredentialsException( 'Only local access allowed!' )

	def IsLocal( self ):
		return self._local_only

class HydrusResource( Resource ):
	def __init__( self, service, domain ):
		Resource.__init__( self )

		self._service = service
		self._service_key = self._service.GetServiceKey()
		self._domain = domain

		service_type = self._service.GetServiceType()

		self._server_version_string = HC.service_string_lookup[ service_type ] + '/' + str( HC.NETWORK_VERSION )

	def _callbackCheckRestrictions( self, request ):
		self._domain.CheckValid( request.getClientIP() )

		self._checkService( request )

		self._checkUserAgent( request )

		return request

	def _callbackParseGETArgs( self, request ):
		return request

	def _callbackParsePOSTArgs( self, request ):
		return request

	def _checkService( self, request ):
		if HG.server_busy:
			raise ServerBusyException( 'This server is busy, please try again later.' )

		return request

	def _checkUserAgent( self, request ):
		request.is_hydrus_user_agent = False

		if request.requestHeaders.hasHeader( 'User-Agent' ):
			user_agent_texts = request.requestHeaders.getRawHeaders( 'User-Agent' )

			user_agent_text = user_agent_texts[0]

			try:
				user_agents = user_agent_text.split( ' ' )

			except Exception:
					return # crazy user agent string, so just assume not a hydrus client

			for user_agent in user_agents:
					if '/' in user_agent:
						( client, network_version ) = user_agent.split( '/', 1 )

					if client == 'hydrus':
						request.is_hydrus_user_agent = True

						network_version = int( network_version )

						if network_version == HC.NETWORK_VERSION:
							return

						else:
							if network_version < HC.NETWORK_VERSION:
								message = 'Your client is out of date; please download the latest release.'
							else:
								message = 'This server is out of date; please ask its admin to update to the latest release.'

							raise NetworkVersionException( 'Network version mismatch! This server\'s network version is ' + str( HC.NETWORK_VERSION ) + ', whereas your client\'s is ' + str( network_version ) + '! ' + message )

	def _callbackRenderResponseContext( self, request ):
		self._CleanUpTempFile( request )

		if request.channel is None:
			# Connection was lost, it seems.

			request.finish()

			return

		response_context = request.hydrus_response_context

		status_code = response_context.GetStatusCode()

		request.setResponseCode( status_code )

		for ( k, v, kwargs ) in response_context.GetCookies():
			request.addCookie( k, v, **kwargs )

		do_finish = True

		if response_context.HasPath():
			path = response_context.GetPath()

			size = os.path.getsize( path )

			mime = response_context.GetMime()

			content_type = HC.mime_string_lookup[ mime ]

			content_length = size

			( base, filename ) = os.path.split( path )

			content_disposition = 'inline; filename="' + filename + '"'

			request.setHeader( 'Content-Type', str( content_type ) )
			request.setHeader( 'Content-Length', str( content_length ) )
			request.setHeader( 'Content-Disposition', str( content_disposition ) )

			request.setHeader( 'Expires', time.strftime( '%a, %d %b %Y %H:%M:%S GMT', time.gmtime( time.time() + 86400 * 365 ) ) )
			request.setHeader( 'Cache-Control', 'max-age={}'.format( 86400 * 365 ) )

			fileObject = open( path, 'rb' )

			producer = NoRangeStaticProducer( request, fileObject )

			producer.start()

			do_finish = False

		elif response_context.HasBody():
			mime = response_context.GetMime()

			body_bytes = response_context.GetBodyBytes()

			content_type = HC.mime_string_lookup[ mime ]

			content_length = len( body_bytes )

			content_disposition = 'inline'

			request.setHeader( 'Content-Type', content_type )
			request.setHeader( 'Content-Length', str( content_length ) )
			request.setHeader( 'Content-Disposition', content_disposition )

			request.write( body_bytes )

		else:
			content_length = 0

			if status_code != 204:
			# 204 is No Content

				request.setHeader( 'Content-Length', str( content_length ) )

		self._reportDataUsed( request, content_length )
		self._reportRequestUsed( request )

		if do_finish:
			request.finish()

	def _callbackDoGETJob( self, request ):
		def wrap_thread_result( response_context ):
			request.hydrus_response_context = response_context

			return request

		d = deferToThread( self._threadDoGETJob, request )

		d.addCallback( wrap_thread_result )

		return d

	def _callbackDoOPTIONSJob( self, request ):
		def wrap_thread_result( response_context ):
			request.hydrus_response_context = response_context

			return request

		d = deferToThread( self._threadDoOPTIONSJob, request )

		d.addCallback( wrap_thread_result )

		return d

	def _callbackDoPOSTJob( self, request ):
		def wrap_thread_result( response_context ):
			request.hydrus_response_context = response_context

			return request

		d = deferToThread( self._threadDoPOSTJob, request )

		d.addCallback( wrap_thread_result )

		return d

	def _DecompressionBombsOK( self, request ):
		return False

	def _errbackDisconnected( self, failure, request_deferred ):
		request_deferred.cancel()

	def _errbackHandleEmergencyError( self, failure, request ):
		try:
			self._CleanUpTempFile( request )
		except Exception:
			pass

		try:
			DebugPrint( failure.getTraceback() )
		except Exception:
			pass

		if request.channel is not None:
			try:
				request.setResponseCode( 500 )
			except Exception:
				pass

			try:
				request.write( failure.getTraceback() )
			except Exception:
				pass

		if not request.finished:
			try:
				request.finish()
			except Exception:
				pass

	def _errbackHandleProcessingError( self, failure, request ):
		self._CleanUpTempFile( request )

		default_mime = HC.TEXT_HTML
		default_encoding = str

		if failure.type == BadRequestException:
			response_context = ResponseContext( 400, mime = default_mime, body = default_encoding( failure.value ) )

		elif failure.type in ( MissingCredentialsException, DoesNotSupportCORSException ):
			response_context = ResponseContext( 401, mime = default_mime, body = default_encoding( failure.value ) )

		elif failure.type == InsufficientCredentialsException:
			response_context = ResponseContext( 403, mime = default_mime, body = default_encoding( failure.value ) )

		elif failure.type in ( NotFoundException, DataMissing, FileMissingException ):
			response_context = ResponseContext( 404, mime = default_mime, body = default_encoding( failure.value ) )

		elif failure.type == SessionException:
			response_context = ResponseContext( 419, mime = default_mime, body = default_encoding( failure.value ) )

		elif failure.type == NetworkVersionException:
			response_context = ResponseContext( 426, mime = default_mime, body = default_encoding( failure.value ) )

		elif failure.type == ServerBusyException:
			response_context = ResponseContext( 503, mime = default_mime, body = default_encoding( failure.value ) )

		elif failure.type == BandwidthException:
			response_context = ResponseContext( 509, mime = default_mime, body = default_encoding( failure.value ) )

		else:
			DebugPrint( failure.getTraceback() )

			response_context = ResponseContext( 500, mime = default_mime, body = default_encoding( 'The repository encountered an error it could not handle! Here is a dump of what happened, which will also be written to your client.log file. If it persists, please forward it to hydrus.admin@gmail.com:' + os.linesep * 2 + failure.getTraceback() ) )

		request.hydrus_response_context = response_context

		return request

	def _parseHydrusNetworkAccessKey( self, request ):
		if not request.requestHeaders.hasHeader( 'Hydrus-Key' ):
			raise MissingCredentialsException( 'No hydrus key header found!' )

		hex_keys = request.requestHeaders.getRawHeaders( 'Hydrus-Key' )

		hex_key = hex_keys[0]

		try:
			access_key = bytes.fromhex( hex_key )

		except Exception:
			raise InsufficientCredentialsException( 'Could not parse the hydrus key!' )

		return access_key

	def _reportDataUsed( self, request, num_bytes ):
		self._service.ReportDataUsed( num_bytes )

		HG.controller.ReportDataUsed( num_bytes )

	def _reportRequestUsed( self, request ):
		self._service.ReportRequestUsed()

		HG.controller.ReportRequestUsed()

	def _threadDoGETJob( self, request ):
		raise NotFoundException( 'This service does not support that request!' )

	def _threadDoOPTIONSJob( self, request ):
		allowed_methods = []

		if self._threadDoGETJob.__func__ != HydrusResource._threadDoGETJob:
			allowed_methods.append( 'GET' )

		if self._threadDoPOSTJob.__func__ != HydrusResource._threadDoPOSTJob:
			allowed_methods.append( 'POST' )

		allowed_methods_string = ', '.join( allowed_methods )

		if request.requestHeaders.hasHeader( 'Origin' ):
			# this is a CORS request

			if self._service.SupportsCORS():
				request.setHeader( 'Access-Control-Allow-Origin', '*' )
				request.setHeader( 'Access-Control-Allow-Methods', allowed_methods_string )

			else:
					# 401
				raise DoesNotSupportCORSException( 'This service does not support CORS.' )

		else:
			# regular OPTIONS request

			request.setHeader( 'Allow', allowed_methods_string )

		# 204 No Content
		response_context = ResponseContext( 204 )

		return response_context

	def _threadDoPOSTJob( self, request ):
		raise NotFoundException( 'This service does not support that request!' )

	def _CleanUpTempFile( self, request ):
		if hasattr( request, 'temp_file_info' ):
			( os_file_handle, temp_path ) = request.temp_file_info

			CleanUpTempPath( os_file_handle, temp_path )

			del request.temp_file_info

	def render_GET( self, request ):
		request.setHeader( 'Server', self._server_version_string )

		d = defer.Deferred()

		d.addCallback( self._callbackCheckRestrictions )

		d.addCallback( self._callbackParseGETArgs )

		d.addCallback( self._callbackDoGETJob )

		d.addErrback( self._errbackHandleProcessingError, request )

		d.addCallback( self._callbackRenderResponseContext )

		d.addErrback( self._errbackHandleEmergencyError, request )

		reactor.callLater( 0, d.callback, request )

		request.notifyFinish().addErrback( self._errbackDisconnected, d )

		return NOT_DONE_YET

	def render_OPTIONS( self, request ):
		request.setHeader( 'Server', self._server_version_string )

		d = defer.Deferred()

		d.addCallback( self._callbackCheckRestrictions )

		d.addCallback( self._callbackDoOPTIONSJob )

		d.addErrback( self._errbackHandleProcessingError, request )

		d.addCallback( self._callbackRenderResponseContext )

		d.addErrback( self._errbackHandleEmergencyError, request )

		reactor.callLater( 0, d.callback, request )

		request.notifyFinish().addErrback( self._errbackDisconnected, d )

		return NOT_DONE_YET

	def render_POST( self, request ):
		request.setHeader( 'Server', self._server_version_string )

		d = defer.Deferred()

		d.addCallback( self._callbackCheckRestrictions )

		d.addCallback( self._callbackParsePOSTArgs )

		d.addCallback( self._callbackDoPOSTJob )

		d.addErrback( self._errbackHandleProcessingError, request )

		d.addCallback( self._callbackRenderResponseContext )

		d.addErrback( self._errbackHandleEmergencyError, request )

		reactor.callLater( 0, d.callback, request )

		request.notifyFinish().addErrback( self._errbackDisconnected, d )

		return NOT_DONE_YET

class HydrusResourceRobotsTXT( HydrusResource ):
	def _threadDoGETJob( self, request ):
		body = '''User-agent:
		*
Disallow:
/'''

		response_context = ResponseContext( 200, mime = HC.TEXT_PLAIN, body = body )

		return response_context

class HydrusResourceWelcome( HydrusResource ):
	def _threadDoGETJob( self, request ):
		body = GenerateEris( self._service )

		response_context = ResponseContext( 200, mime = HC.TEXT_HTML, body = body )

		return response_context

class ResponseContext( object ):
	def __init__( self, status_code, mime = HC.APPLICATION_JSON, body = None, path = None, cookies = None ):
		if body is None:
			body_bytes = None

		elif isinstance( body, SerialisableBase ):
			body_bytes = body.DumpToNetworkBytes()

		elif isinstance( body, str ):
			body_bytes = bytes( body, 'utf-8' )

		elif isinstance( body, bytes ):
			body_bytes = body

		else:
			raise Exception( 'Was given an incompatible object to respond with: ' + repr( body ) )

		if cookies is None:
			cookies = []

		self._status_code = status_code
		self._mime = mime
		self._body_bytes = body_bytes
		self._path = path
		self._cookies = cookies

	def GetBodyBytes( self ):
		return self._body_bytes

	def GetCookies( self ):
		return self._cookies

	def GetMime( self ):
		return self._mime

	def GetPath( self ):
		return self._path

	def GetStatusCode( self ):
		return self._status_code

	def HasBody( self ):
		return self._body_bytes is not None

	def HasPath( self ):
		return self._path is not None


code:
from .Resources import *

__all__ = [
	"Local",
	"Remote",
	"Eris"
]

from . import *


code:
import time

from lib.Hydrus.Data import Print, TimeDeltaToPrettyTimeDelta
from lib.Hydrus.Server.Resources import HydrusDomain, HydrusResourceRobotsTXT, \
	HydrusResourceWelcome, hydrus_favicon

from twisted.web.server import Request, Site
from twisted.web.resource import Resource

### CONSTANTS ###
LOCAL_DOMAIN = HydrusDomain( True )
REMOTE_DOMAIN = HydrusDomain( False )

### GENERAL SERVER CLASSES ###

class HydrusRequest( Request ):
	def __init__( self, *args, **kwargs ):
		Request.__init__( self, *args, **kwargs )

		self.start_time = time.clock()
		self.parsed_request_args = None
		self.hydrus_response_context = None
		self.hydrus_account = None

class HydrusRequestLogging( HydrusRequest ):
	def finish( self ):
		HydrusRequest.finish( self )

		host = self.getHost()

		if self.hydrus_response_context is not None:
			status_text = str( self.hydrus_response_context.GetStatusCode() )

		elif hasattr( self, 'code' ):
			status_text = str( self.code )

		else:
			status_text = '200'

		message = str( host.port ) + ' ' + str( self.method, 'utf-8' ) + ' ' + str( self.path, 'utf-8' ) + ' ' + status_text + ' in ' + TimeDeltaToPrettyTimeDelta( time.clock() - self.start_time )
		Print( message )

class HydrusService( Site ):
	def __init__( self, service ):
		self._service = service

		root = self._InitRoot()
		Site.__init__( self, root )

		if service.LogsRequests():
			self.requestFactory = HydrusRequestLogging

		else:
			self.requestFactory = HydrusRequest

	def _InitRoot( self ):
		root = Resource()

		root.putChild( b'', HydrusResourceWelcome( self._service, REMOTE_DOMAIN ) )
		root.putChild( b'favicon.ico', hydrus_favicon )
		root.putChild( b'robots.txt', HydrusResourceRobotsTXT( self._service, REMOTE_DOMAIN ) )

		return root


code:
import collections
import lib.Hydrus.Constants as HC
import lib.Handling.Document
import lib.Hydrus.Exceptions
import lib.Handling
import lib.Handling.Flash
import lib.Handling.Image
import lib.Hydrus.Server.Resources
import os
import random
import lib.Hydrus.Files.Remote
import traceback
from twisted.internet import reactor, defer
from twisted.internet.threads import deferToThread
from twisted.protocols import amp
import lib.Hydrus.Data
import lib.Hydrus.Globals as HG
'''

class HydrusAMPCommand( amp.Command ):
	errors = {}
	errors[ lib.Hydrus.Exceptions.InsufficientCredentialsException ] = 'FORBIDDEN'
	errors[ lib.Hydrus.Exceptions.NetworkVersionException ] = 'NETWORK_VERSION'
	errors[ lib.Hydrus.Exceptions.NotFoundException ] = 'NOT_FOUND'
	errors[ lib.Hydrus.Exceptions.MissingCredentialsException ] = 'PERMISSION'
	errors[ lib.Hydrus.Exceptions.SessionException ] = 'SESSION'
	errors[ Exception ] = 'EXCEPTION'

# IMFile, for aes-encrypted file transfers, as negotiated over otr messages
# file_transfer_id (so we can match up the correct aes key)
# file (this is complicated -- AMP should be little things, right? I need to check max packet size.)
  # so, this should be blocks. a block_id and a block
# MMessageReceivedPing -- server to persistent client, saying someone just now sent you a message

class IMLoginPersistent( HydrusAMPCommand ):
	arguments = [ ( 'network_version', amp.Integer() ), ( 'session_key', amp.String() ) ]

class IMLoginTemporary( HydrusAMPCommand ):
	arguments = [ ( 'network_version', amp.Integer() ), ( 'identifier', amp.String() ), ( 'name', amp.String() ) ]

class IMMessageClient( HydrusAMPCommand ):
	arguments = [ ( 'identifier_from', amp.String() ), ( 'name_from', amp.String() ), ( 'identifier_to', amp.String() ), ( 'name_to', amp.String() ), ( 'message', amp.String() ) ]

class IMMessageServer( HydrusAMPCommand ):
	arguments = [ ( 'identifier_to', amp.String() ), ( 'name_to', amp.String() ), ( 'message', amp.String() ) ]

class IMSessionKey( HydrusAMPCommand ):
	arguments = [ ( 'access_key', amp.String() ), ( 'name', amp.String() ) ]
	response = [ ( 'session_key', amp.String() ) ]

class MPublicKey( HydrusAMPCommand ):
	arguments = [ ( 'identifier', amp.String() ) ]
	response = [ ( 'public_key', amp.String() ) ]

class HydrusAMP( amp.AMP ):
	def _errbackHandleError( self, failure ):
		lib.Hydrus.Data.Print( failure.getTraceback() )

		normal_errors = []

		normal_errors.append( lib.Hydrus.Exceptions.InsufficientCredentialsException )
		normal_errors.append( lib.Hydrus.Exceptions.NetworkVersionException )
		normal_errors.append( lib.Hydrus.Exceptions.NotFoundException )
		normal_errors.append( lib.Hydrus.Exceptions.MissingCredentialsException )
		normal_errors.append( lib.Hydrus.Exceptions.SessionException )

		if failure.type in normal_errors:
			failure.raiseException()
		else:
			raise Exception( failure.getTraceback() )

class MessagingClientProtocol( HydrusAMP ):
	def im_message( self, identifier_from, name_from, identifier_to, name_to, message ):
		def do_it( gumpf ):
			HG.controller.pub( 'im_message_received', identifier_from, name_from, identifier_to, name_to, message )

			return {}

		d = defer.Deferred()

		d.addCallback( do_it )

		d.addErrback( self._errbackHandleError )

		reactor.callLater( 0, d.callback, None )

		return d

	IMMessageClient.responder( im_message )

	def connectionLost( self, reason ):
		# report to ui that the connection is lost

		pass

class MessagingServiceProtocol( HydrusAMP ):
	def __init__( self ):
		amp.AMP.__init__( self )

		self._identifier = None
		self._name = None

	def _check_network_version( self, network_version ):
		if network_version != HC.NETWORK_VERSION:
			if network_version < HC.NETWORK_VERSION:
				message = 'Your client is out of date; please download the latest release.'
			else:
				message = 'This server is out of date; please ask its admin to update to the latest release.'

			message = 'Network version mismatch! This server\'s network version is ' + str( HC.NETWORK_VERSION ) + ', whereas your client\'s is ' + str( network_version ) + '! ' + message

			raise lib.Hydrus.Exceptions.NetworkVersionException( message )

	def im_login_persistent( self, network_version, session_key ):
		def do_it( gumpf ):
			self._check_network_version( network_version )

			session_manager = HG.controller.GetManager( 'messaging_sessions' )

			( identifier, name ) = session_manager.GetIdentityAndName( self.factory.service_key, session_key )

			self._identifier = identifier
			self._name = name

			self.factory.AddConnection( True, self._identifier, self._name, self )

			return {}

		d = defer.Deferred()

		d.addCallback( do_it )

		d.addErrback( self._errbackHandleError )

		reactor.callLater( 0, d.callback, None )

		return d

	IMLoginPersistent.responder( im_login_persistent )

	def im_login_temporary( self, network_version, identifier, name ):
		def do_it( gumpf ):
			self._check_network_version( network_version )

			self._identifier = identifier
			self._name = name

			self.factory.AddConnection( False, self._identifier, self._name, self )

			return {}

		d = defer.Deferred()

		d.addCallback( do_it )

		d.addErrback( self._errbackHandleError )

		reactor.callLater( 0, d.callback, None )

		return d

	IMLoginTemporary.responder( im_login_temporary )

	def im_message( self, identifier_to, name_to, message ):
		def do_it( gumpf ):
			if self._identifier is None or self._name is None:
				raise Exception() # who the hell are you? pls temp login

			connection = self.factory.GetConnection( identifier_to, name_to )

			d = connection.callRemote( IMMessageClient, identifier_from = self._identifier, name_from = self._name, identifier_to = identifier_to, name_to = name_to, message = message )

			return d

		d = defer.Deferred()

		d.addCallback( do_it )

		d.addErrback( self._errbackHandleError )

		reactor.callLater( 0, d.callback, None )

		return d

	IMMessageServer.responder( im_message )

	def im_session_key( self, access_key, name ):
		def catch_session_key( session_key ): return { 'session_key' : session_key }

		def do_it( gumpf ):
			session_manager = HG.controller.GetManager( 'messaging_sessions' )

			d = deferToThread( session_manager.AddSession, self.factory.service_key, access_key, name )

			d.addCallback( catch_session_key )

			return d

		d = defer.Deferred()

		d.addCallback( do_it )

		d.addErrback( self._errbackHandleError )

		reactor.callLater( 0, d.callback, None )

		return d

	IMSessionKey.responder( im_session_key )

	def m_public_key( self, identifier ):
		# this will not be useful until we have normal messaging sorted

		public_key = 'public key'

		return { 'public_key' : public_key }

	MPublicKey.responder( m_public_key )

	def connectionLost( self, reason ):
		if self._identifier is not None:
			self.factory.RemoveConnection( self._identifier, self._name )

	'''


code:
from .Server import *

FolderModules = [
	"Resources",
]

FileModules = [
	"Local",
	"RemoteAMP",
]

__all__ = FolderModules + FileModules

from . import *