Merge pull request #1098 from thatfuckingbird/api

thank you for this work!
This commit is contained in:
Hydrus Network Developer 2022-03-12 16:50:14 -06:00 committed by GitHub
commit 7561fa357b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 376 additions and 55 deletions

59
apitest.py Normal file
View File

@ -0,0 +1,59 @@
import requests
import cbor2
import base64
import json
import urllib.parse
hydrus_api_url = "http://localhost:45888"
metadata = hydrus_api_url+"/get_files/file_metadata"
del_note = hydrus_api_url+"/add_notes/delete_notes"
set_note = hydrus_api_url+"/add_notes/set_notes"
search = hydrus_api_url+"/get_files/search_files"
hsh="1b625544bcfbd7151000a816e6db6388ba0ef4dc3a664b62e2cb4e9d3036bed8"
key="222f3c82f4f7e8ce57747ff1cccfaf7014357dc509cdb77af20ff910c26ea05b"
# search for notes
print(json.loads((requests.get(url = search, params = {
"Hydrus-Client-API-Access-Key": key,
"tags": urllib.parse.quote("[\"system:has notes\"]")
}).text)))
# retrieve notes
print(json.loads((requests.get(url = metadata, params = {
"Hydrus-Client-API-Access-Key": key,
"include_notes": "true",
"hashes": urllib.parse.quote("[\""+hsh+"\"]")
}).text))["metadata"][0]["notes"])
# retrieve notes, request that the response is CBOR encoded
print(cbor2.loads((requests.get(url = metadata, params = {
"Hydrus-Client-API-Access-Key": key,
"include_notes": base64.urlsafe_b64encode(cbor2.dumps(True)),
"hashes": base64.urlsafe_b64encode(cbor2.dumps([hsh])),
"cbor": ""
}).content))["metadata"][0]["notes"])
# Add notes
headers = {"Hydrus-Client-API-Access-Key": key, "Content-Type": "application/json"}
print(requests.post(url = set_note, headers = headers, data = json.dumps({
"notes": {"note1":"content1", "note2":"content2"},
"hash": hsh
})))
# Delete notes
headers = {"Hydrus-Client-API-Access-Key": key, "Content-Type": "application/json"}
print(requests.post(url = del_note, headers = headers, data = json.dumps({
"note_names": ["note1","note2","asgasgasgasgaa"],
"hash": hsh
})))
# Add notes, but send CBOR instead of json
headers = {"Hydrus-Client-API-Access-Key": key, "Content-Type": "application/cbor"}
print(requests.post(url = set_note, headers = headers, data = cbor2.dumps({
"notes": {"note1":"content1", "note2":"content2"},
"hash": hsh
})))

View File

@ -47,6 +47,11 @@ In general, the API deals with standard UTF-8 JSON. POST requests and 200 OK res
On 200 OK, the API returns JSON for everything except actual file/thumbnail requests. On 4XX and 5XX, assume it will return plain text, which may be a raw traceback that I'd be interested in seeing. You'll typically get 400 for a missing parameter, 401/403/419 for missing/insufficient/expired access, and 500 for a real deal serverside error.
!!! note
For any request sent to the API, the total size of the initial request line (this includes the URL and any parameters) and the headers must not be larger than 2 megabytes.
Exceeding this limit will cause the request to fail. Make sure to use pagination if you are passing very large JSON arrays as parameters in a GET request.
## Access and permissions
The client gives access to its API through different 'access keys', which are the typical 64-character hex used in many other places across hydrus. Each guarantees different permissions such as handling files or tags. Most of the time, a user will provide full access, but do not assume this. If the access header or parameter is not provided, you will get 401, and all insufficient permission problems will return 403 with appropriate error text.
@ -800,6 +805,67 @@ Response:
: 200 with no content. Like when adding tags, this is safely idempotent--do not worry about re-adding URLs associations that already exist or accidentally trying to delete ones that don't.
## Adding Notes
### **POST `/add_notes/set_notes`** { id="add_notes_set_notes" }
_Add or update notes associated with a file._
Restricted access:
: YES. Add Notes permission needed.
Required Headers:
:
* `Content-Type`: `application/json`
Arguments (in percent-encoded JSON):
:
* `notes`: a dictionary mapping note names to note contents
* `hash`: the SHA256 of the target file
* `file_id`: the identifier of the target file (an integer)
You must provide one of `hash` or `file_id`. Existing notes will be overwritten.
```json title="Example request body"
{
"notes": {
"note name": "content of note",
"another note": "asdf"
},
"hash": "3b820114f658d768550e4e3d4f1dced3ff8db77443472b5ad93700647ad2d3ba"
}
```
Response:
: 200 with no content. This operation is idempotent.
### **POST `/add_notes/delete_notes`** { id="add_notes_delete_notes" }
_Remove notes associated with a file._
Restricted access:
: YES. Add Notes permission needed.
Required Headers:
:
* `Content-Type`: `application/json`
Arguments (in percent-encoded JSON):
:
* `note_names`: a list of note names to delete
* `hash`: the SHA256 of the target file
* `file_id`: the identifier of the target file (an integer)
You must provide one of `hash` or `file_id`.
```json title="Example request body"
{
"note_names": ["note name", "another note"],
"hash": "3b820114f658d768550e4e3d4f1dced3ff8db77443472b5ad93700647ad2d3ba"
}
```
Response:
: 200 with no content. This operation is idempotent.
## Managing Cookies and HTTP Headers
This refers to the cookies held in the client's session manager, which are sent with network requests to different domains.
@ -1228,6 +1294,14 @@ Arguments (in percent-encoded JSON):
* system:has a url with class safebooru file page
* system:does not have a url with url class safebooru file page
* system:tag as number page < 5
* system:has notes
* system:no notes
* system:does not have notes
* system:num notes is 5
* system:num notes > 1
* system:has note with name note name
* system:no note with name note name
* system:does not have note with name note name
More system predicate types and input formats will be available in future. Please test out the system predicates you want to send. Reverse engineering system predicate data from text is obviously tricky. If a system predicate does not parse, you'll get 400.
@ -1303,6 +1377,7 @@ Arguments (in percent-encoded JSON):
* `only_return_identifiers`: true or false (optional, defaulting to false)
* `detailed_url_information`: true or false (optional, defaulting to false)
* `hide_service_names_tags`: true or false (optional, defaulting to false)
* `include_notes`: true or false (optional, defaulting to false)
You need one of file_ids or hashes. If your access key is restricted by tag, you cannot search by hashes, and **the file_ids you search for must have been in the most recent search result**.

View File

@ -16,8 +16,9 @@ CLIENT_API_PERMISSION_SEARCH_FILES = 3
CLIENT_API_PERMISSION_MANAGE_PAGES = 4
CLIENT_API_PERMISSION_MANAGE_COOKIES = 5
CLIENT_API_PERMISSION_MANAGE_DATABASE = 6
CLIENT_API_PERMISSION_ADD_NOTES = 7
ALLOWED_PERMISSIONS = ( CLIENT_API_PERMISSION_ADD_FILES, CLIENT_API_PERMISSION_ADD_TAGS, CLIENT_API_PERMISSION_ADD_URLS, CLIENT_API_PERMISSION_SEARCH_FILES, CLIENT_API_PERMISSION_MANAGE_PAGES, CLIENT_API_PERMISSION_MANAGE_COOKIES, CLIENT_API_PERMISSION_MANAGE_DATABASE )
ALLOWED_PERMISSIONS = ( CLIENT_API_PERMISSION_ADD_FILES, CLIENT_API_PERMISSION_ADD_TAGS, CLIENT_API_PERMISSION_ADD_URLS, CLIENT_API_PERMISSION_SEARCH_FILES, CLIENT_API_PERMISSION_MANAGE_PAGES, CLIENT_API_PERMISSION_MANAGE_COOKIES, CLIENT_API_PERMISSION_MANAGE_DATABASE, CLIENT_API_PERMISSION_ADD_NOTES )
basic_permission_to_str_lookup = {}
@ -28,6 +29,7 @@ basic_permission_to_str_lookup[ CLIENT_API_PERMISSION_SEARCH_FILES ] = 'search f
basic_permission_to_str_lookup[ CLIENT_API_PERMISSION_MANAGE_PAGES ] = 'manage pages'
basic_permission_to_str_lookup[ CLIENT_API_PERMISSION_MANAGE_COOKIES ] = 'manage cookies'
basic_permission_to_str_lookup[ CLIENT_API_PERMISSION_MANAGE_DATABASE ] = 'manage database'
basic_permission_to_str_lookup[ CLIENT_API_PERMISSION_ADD_NOTES ] = 'add notes to files'
SEARCH_RESULTS_CACHE_TIMEOUT = 4 * 3600

View File

@ -175,7 +175,12 @@ pred_generators = {
SystemPredicateParser.Predicate.LAST_VIEWED_TIME : lambda o, v, u: date_pred_generator( ClientSearch.PREDICATE_TYPE_SYSTEM_LAST_VIEWED_TIME, o, v ),
SystemPredicateParser.Predicate.TIME_IMPORTED : lambda o, v, u: date_pred_generator( ClientSearch.PREDICATE_TYPE_SYSTEM_AGE, o, v ),
SystemPredicateParser.Predicate.FILE_SERVICE : file_service_pred_generator,
SystemPredicateParser.Predicate.NUM_FILE_RELS : num_file_relationships_pred_generator
SystemPredicateParser.Predicate.NUM_FILE_RELS : num_file_relationships_pred_generator,
SystemPredicateParser.Predicate.HAS_NOTES : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( '>', 0 ) ),
SystemPredicateParser.Predicate.NO_NOTES : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( '=', 0 ) ),
SystemPredicateParser.Predicate.NUM_NOTES : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_NUM_NOTES, ( o, v ) ),
SystemPredicateParser.Predicate.HAS_NOTE_NAME : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME, ( True, v ) ),
SystemPredicateParser.Predicate.NO_NOTE_NAME : lambda o, v, u: ClientSearch.Predicate( ClientSearch.PREDICATE_TYPE_SYSTEM_HAS_NOTE_NAME, ( False, v ) )
}
def ParseSystemPredicateStringsToPredicates( system_predicate_strings: typing.Collection[ str ] ) -> typing.List[ ClientSearch.Predicate ]:

View File

@ -687,6 +687,18 @@ class FrameGUI( ClientGUITopLevelWindows.MainFrameThatResizes ):
library_versions.append( ( 'PyQt5', PYQT_VERSION_STR ) )
library_versions.append( ( 'sip', SIP_VERSION_STR ) )
CBOR_AVAILABLE = False
try:
import cbor2
CBOR_AVAILABLE = True
except:
pass
library_versions.append( ( 'cbor2 present: ', str( CBOR_AVAILABLE ) ) )
from hydrus.client.networking import ClientNetworkingJobs

View File

@ -84,6 +84,13 @@ class HydrusServiceClientAPI( HydrusClientService ):
get_files.putChild( b'file', ClientLocalServerResources.HydrusResourceClientAPIRestrictedGetFilesGetFile( self._service, self._client_requests_domain ) )
get_files.putChild( b'thumbnail', ClientLocalServerResources.HydrusResourceClientAPIRestrictedGetFilesGetThumbnail( self._service, self._client_requests_domain ) )
add_notes = NoResource()
root.putChild( b'add_notes', add_notes )
add_notes.putChild( b'set_notes', ClientLocalServerResources.HydrusResourceClientAPIRestrictedAddNotesSetNotes( self._service, self._client_requests_domain ) )
add_notes.putChild( b'delete_notes', ClientLocalServerResources.HydrusResourceClientAPIRestrictedAddNotesDeleteNotes( self._service, self._client_requests_domain ) )
manage_cookies = NoResource()
root.putChild( b'manage_cookies', manage_cookies )

View File

@ -7,6 +7,13 @@ import time
import traceback
import typing
CBOR_AVAILABLE = False
try:
import cbor2
CBOR_AVAILABLE = True
except:
pass
from twisted.web.static import File as FileResource
from hydrus.core import HydrusConstants as HC
@ -43,10 +50,21 @@ LOCAL_BOORU_JSON_BYTE_LIST_PARAMS = set()
CLIENT_API_INT_PARAMS = { 'file_id', 'file_sort_type' }
CLIENT_API_BYTE_PARAMS = { 'hash', 'destination_page_key', 'page_key', 'Hydrus-Client-API-Access-Key', 'Hydrus-Client-API-Session-Key', 'tag_service_key', 'file_service_key' }
CLIENT_API_STRING_PARAMS = { 'name', 'url', 'domain', 'search', 'file_service_name', 'tag_service_name' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'only_return_identifiers', 'detailed_url_information', 'hide_service_names_tags', 'simple', 'file_sort_asc', 'return_hashes' }
CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'system_inbox', 'system_archive', 'tags', 'file_ids', 'only_return_identifiers', 'detailed_url_information', 'hide_service_names_tags', 'simple', 'file_sort_asc', 'return_hashes', 'include_notes', 'notes', 'note_names' }
CLIENT_API_JSON_BYTE_LIST_PARAMS = { 'hashes' }
CLIENT_API_JSON_BYTE_DICT_PARAMS = { 'service_keys_to_tags', 'service_keys_to_actions_to_tags', 'service_keys_to_additional_tags' }
def Dumps( data, mime ):
if CBOR_AVAILABLE and mime == HC.APPLICATION_CBOR:
return cbor2.dumps( data )
else:
return json.dumps( data )
def CheckHashLength( hashes, hash_type = 'sha256' ):
hash_types_to_length = {
@ -276,7 +294,17 @@ def ParseClientAPIPOSTArgs( request ):
args = json.loads( json_string )
parsed_request_args = ParseClientAPIPOSTByteArgs( args )
elif mime == HC.APPLICATION_CBOR and CBOR_AVAILABLE:
cbor_bytes = request.content.read()
total_bytes_read += len( cbor_bytes )
args = cbor2.loads( cbor_bytes )
parsed_request_args = ParseClientAPIPOSTByteArgs( args )
else:
parsed_request_args = HydrusNetworkVariableHandling.ParsedRequestArguments()
@ -297,7 +325,7 @@ def ParseClientAPIPOSTArgs( request ):
return ( parsed_request_args, total_bytes_read )
return ( parsed_request_args, total_bytes_read, mime )
def ParseClientAPISearchPredicates( request ):
@ -744,17 +772,21 @@ class HydrusResourceClientAPI( HydrusServerResources.HydrusResource ):
request.parsed_request_args = parsed_request_args
request.preferred_mime = HC.APPLICATION_CBOR if CBOR_AVAILABLE and b'cbor' in request.args else HC.APPLICATION_JSON
return request
def _callbackParsePOSTArgs( self, request: HydrusServerRequest.HydrusRequest ):
( parsed_request_args, total_bytes_read ) = ParseClientAPIPOSTArgs( request )
( parsed_request_args, total_bytes_read, mime ) = ParseClientAPIPOSTArgs( request )
self._reportDataUsed( request, total_bytes_read )
request.parsed_request_args = parsed_request_args
request.preferred_mime = mime
return request
@ -810,9 +842,9 @@ class HydrusResourceClientAPIPermissionsRequest( HydrusResourceClientAPI ):
body_dict[ 'access_key' ] = access_key.hex()
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -826,9 +858,9 @@ class HydrusResourceClientAPIVersion( HydrusResourceClientAPI ):
body_dict[ 'version' ] = HC.CLIENT_API_VERSION
body_dict[ 'hydrus_version' ] = HC.SOFTWARE_VERSION
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -971,9 +1003,9 @@ class HydrusResourceClientAPIRestrictedAccountSessionKey( HydrusResourceClientAP
body_dict[ 'session_key' ] = new_session_key.hex()
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -992,9 +1024,9 @@ class HydrusResourceClientAPIRestrictedAccountVerify( HydrusResourceClientAPIRes
body_dict[ 'basic_permissions' ] = list( basic_permissions ) # set->list for json
body_dict[ 'human_description' ] = human_description
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -1007,6 +1039,7 @@ class HydrusResourceClientAPIRestrictedGetServices( HydrusResourceClientAPIRestr
(
ClientAPI.CLIENT_API_PERMISSION_ADD_FILES,
ClientAPI.CLIENT_API_PERMISSION_ADD_TAGS,
ClientAPI.CLIENT_API_PERMISSION_ADD_NOTES,
ClientAPI.CLIENT_API_PERMISSION_MANAGE_PAGES,
ClientAPI.CLIENT_API_PERMISSION_SEARCH_FILES
)
@ -1035,9 +1068,9 @@ class HydrusResourceClientAPIRestrictedGetServices( HydrusResourceClientAPIRestr
body_dict[ name ] = [ { 'name' : service.GetName(), 'service_key' : service.GetServiceKey().hex() } for service in services ]
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -1090,9 +1123,9 @@ class HydrusResourceClientAPIRestrictedAddFilesAddFile( HydrusResourceClientAPIR
body_dict[ 'hash' ] = HydrusData.BytesToNoneOrHex( file_import_status.hash )
body_dict[ 'note' ] = file_import_status.note
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -1245,6 +1278,79 @@ class HydrusResourceClientAPIRestrictedAddFilesUndeleteFiles( HydrusResourceClie
return response_context
class HydrusResourceClientAPIRestrictedAddNotes( HydrusResourceClientAPIRestricted ):
def _CheckAPIPermissions( self, request: HydrusServerRequest.HydrusRequest ):
request.client_api_permissions.CheckPermission( ClientAPI.CLIENT_API_PERMISSION_ADD_NOTES )
class HydrusResourceClientAPIRestrictedAddNotesSetNotes( HydrusResourceClientAPIRestrictedAddNotes ):
def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ):
if 'hash' in request.parsed_request_args:
hash = request.parsed_request_args.GetValue( 'hash', bytes )
elif 'file_id' in request.parsed_request_args:
hash_id = request.parsed_request_args.GetValue( 'file_id', int )
hash_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hash_ids = [ hash_id ] )
hash = hash_ids_to_hashes[ hash_id ]
else:
raise HydrusExceptions.BadRequestException( 'There was no file identifier or hash given!' )
notes = request.parsed_request_args.GetValue( 'notes', dict )
content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_NOTES, HC.CONTENT_UPDATE_SET, ( hash, name, note ) ) for ( name, note ) in notes.items() ]
service_keys_to_content_updates = { CC.LOCAL_NOTES_SERVICE_KEY : content_updates }
HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
response_context = HydrusServerResources.ResponseContext( 200 )
return response_context
class HydrusResourceClientAPIRestrictedAddNotesDeleteNotes( HydrusResourceClientAPIRestrictedAddNotes ):
def _threadDoPOSTJob( self, request: HydrusServerRequest.HydrusRequest ):
if 'hash' in request.parsed_request_args:
hash = request.parsed_request_args.GetValue( 'hash', bytes )
elif 'file_id' in request.parsed_request_args:
hash_id = request.parsed_request_args.GetValue( 'file_id', int )
hash_ids_to_hashes = HG.client_controller.Read( 'hash_ids_to_hashes', hash_ids = [ hash_id ] )
hash = hash_ids_to_hashes[ hash_id ]
else:
raise HydrusExceptions.BadRequestException( 'There was no file identifier or hash given!' )
note_names = request.parsed_request_args.GetValue( 'note_names', list, expected_list_type = str )
content_updates = [ HydrusData.ContentUpdate( HC.CONTENT_TYPE_NOTES, HC.CONTENT_UPDATE_DELETE, ( hash, name ) ) for name in note_names ]
service_keys_to_content_updates = { CC.LOCAL_NOTES_SERVICE_KEY : content_updates }
HG.client_controller.WriteSynchronous( 'content_updates', service_keys_to_content_updates )
response_context = HydrusServerResources.ResponseContext( 200 )
return response_context
class HydrusResourceClientAPIRestrictedAddTags( HydrusResourceClientAPIRestricted ):
def _CheckAPIPermissions( self, request: HydrusServerRequest.HydrusRequest ):
@ -1460,9 +1566,9 @@ class HydrusResourceClientAPIRestrictedAddTagsGetTagServices( HydrusResourceClie
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 )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -1574,9 +1680,9 @@ class HydrusResourceClientAPIRestrictedAddTagsSearchTags( HydrusResourceClientAP
body_dict[ 'tags' ] = tags
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -1595,9 +1701,9 @@ class HydrusResourceClientAPIRestrictedAddTagsCleanTags( HydrusResourceClientAPI
body_dict[ 'tags' ] = tags
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -1762,9 +1868,9 @@ class HydrusResourceClientAPIRestrictedAddURLsGetURLFiles( HydrusResourceClientA
body_dict = { 'normalised_url' : normalised_url, 'url_file_statuses' : json_happy_url_statuses }
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -1798,9 +1904,9 @@ class HydrusResourceClientAPIRestrictedAddURLsGetURLInfo( HydrusResourceClientAP
body_dict[ 'cannot_parse_reason' ] = cannot_parse_reason
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -1907,9 +2013,9 @@ class HydrusResourceClientAPIRestrictedAddURLsImportURL( HydrusResourceClientAPI
body_dict = { 'human_result_text' : result_text, 'normalised_url' : normalised_url }
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -2064,9 +2170,9 @@ class HydrusResourceClientAPIRestrictedGetFilesSearchFiles( HydrusResourceClient
body_dict = { 'file_ids' : list( hash_ids ) }
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -2132,6 +2238,7 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
only_return_identifiers = request.parsed_request_args.GetValue( 'only_return_identifiers', bool, default_value = False )
hide_service_names_tags = request.parsed_request_args.GetValue( 'hide_service_names_tags', bool, default_value = False )
detailed_url_information = request.parsed_request_args.GetValue( 'detailed_url_information', bool, default_value = False )
include_notes = request.parsed_request_args.GetValue( 'include_notes', bool, default_value = False )
try:
@ -2217,6 +2324,10 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
'has_audio' : file_info_manager.has_audio
}
if include_notes:
metadata_row[ 'notes' ] = media_result.GetNotesManager().GetNamesToNotes()
locations_manager = media_result.GetLocationsManager()
metadata_row[ 'file_services' ] = {
@ -2361,8 +2472,8 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
body_dict[ 'metadata' ] = metadata
mime = HC.APPLICATION_JSON
body = json.dumps( body_dict )
mime = request.preferred_mime
body = Dumps( body_dict, mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = mime, body = body )
@ -2460,9 +2571,9 @@ class HydrusResourceClientAPIRestrictedManageCookiesGetCookies( HydrusResourceCl
body_dict = { 'cookies' : body_cookies_list }
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -2632,8 +2743,8 @@ class HydrusResourceClientAPIRestrictedManageDatabaseMrBones( HydrusResourceClie
body_dict = { 'boned_stats' : boned_stats }
mime = HC.APPLICATION_JSON
body = json.dumps( body_dict )
mime = request.preferred_mime
body = Dumps( body_dict, mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = mime, body = body )
@ -2748,9 +2859,9 @@ class HydrusResourceClientAPIRestrictedManagePagesGetPages( HydrusResourceClient
body_dict = { 'pages' : page_info_dict }
body = json.dumps( body_dict )
body = Dumps( body_dict )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context
@ -2777,9 +2888,9 @@ class HydrusResourceClientAPIRestrictedManagePagesGetPageInfo( HydrusResourceCli
body_dict = { 'page_info' : page_info_dict }
body = json.dumps( body_dict )
body = Dumps( body_dict, request.preferred_mime )
response_context = HydrusServerResources.ResponseContext( 200, mime = HC.APPLICATION_JSON, body = body )
response_context = HydrusServerResources.ResponseContext( 200, mime = request.preferred_mime, body = body )
return response_context

View File

@ -551,6 +551,7 @@ VIDEO_OGV = 47
AUDIO_MKV = 48
AUDIO_MP4 = 49
UNDETERMINED_MP4 = 50
APPLICATION_CBOR = 51
APPLICATION_OCTET_STREAM = 100
APPLICATION_UNKNOWN = 101
@ -634,6 +635,7 @@ mime_enum_lookup = {
'application/vnd.rar' : APPLICATION_RAR,
'application/x-7z-compressed' : APPLICATION_7Z,
'application/json' : APPLICATION_JSON,
'application/cbor': APPLICATION_CBOR,
'application/hydrus-encrypted-zip' : APPLICATION_HYDRUS_ENCRYPTED_ZIP,
'application/hydrus-update-content' : APPLICATION_HYDRUS_UPDATE_CONTENT,
'application/hydrus-update-definitions' : APPLICATION_HYDRUS_UPDATE_DEFINITIONS,
@ -679,6 +681,7 @@ mime_string_lookup = {
APPLICATION_OCTET_STREAM : 'application/octet-stream',
APPLICATION_YAML : 'yaml',
APPLICATION_JSON : 'json',
APPLICATION_CBOR : 'cbor',
APPLICATION_PDF : 'pdf',
APPLICATION_PSD : 'photoshop psd',
APPLICATION_CLIP : 'clip',
@ -735,6 +738,7 @@ mime_mimetype_string_lookup = {
APPLICATION_OCTET_STREAM : 'application/octet-stream',
APPLICATION_YAML : 'application/x-yaml',
APPLICATION_JSON : 'application/json',
APPLICATION_CBOR : 'application/cbor',
APPLICATION_PDF : 'application/pdf',
APPLICATION_PSD : 'application/x-photoshop',
APPLICATION_CLIP : 'application/clip',

View File

@ -4,6 +4,14 @@ import traceback
import typing
import urllib
CBOR_AVAILABLE = False
try:
import cbor2
import base64
CBOR_AVAILABLE = True
except:
pass
from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
@ -299,6 +307,8 @@ def ParseTwistedRequestGETArgs( requests_args, int_params, byte_params, string_p
args = ParsedRequestArguments()
cbor_requested = b'cbor' in requests_args
for name_bytes in requests_args:
values_bytes = requests_args[ name_bytes ]
@ -365,7 +375,13 @@ def ParseTwistedRequestGETArgs( requests_args, int_params, byte_params, string_p
try:
args[ name ] = json.loads( urllib.parse.unquote( value ) )
if CBOR_AVAILABLE and cbor_requested:
args[ name ] = cbor2.loads( base64.urlsafe_b64decode( value ) )
else:
args[ name ] = json.loads( urllib.parse.unquote( value ) )
except Exception as e:
@ -376,7 +392,13 @@ def ParseTwistedRequestGETArgs( requests_args, int_params, byte_params, string_p
try:
list_of_hex_strings = json.loads( urllib.parse.unquote( value ) )
if CBOR_AVAILABLE and cbor_requested:
list_of_hex_strings = cbor2.loads( base64.urlsafe_b64decode( value ) )
else:
list_of_hex_strings = json.loads( urllib.parse.unquote( value ) )
args[ name ] = [ bytes.fromhex( hex_string ) for hex_string in list_of_hex_strings ]

View File

@ -12,7 +12,8 @@ REMOTE_DOMAIN = HydrusServerResources.HydrusDomain( False )
class FatHTTPChannel( HTTPChannel ):
totalHeadersSize = 1048576 # :^)
MAX_LENGTH = 2 * 1048576
totalHeadersSize = 2 * 1048576 # :^)
class HydrusService( Site ):

View File

@ -97,6 +97,11 @@ class Predicate( Enum ):
URL_CLASS = auto()
NO_URL_CLASS = auto()
TAG_AS_NUMBER = auto()
HAS_NOTES = auto()
NO_NOTES = auto()
NUM_NOTES = auto()
HAS_NOTE_NAME = auto()
NO_NOTE_NAME = auto()
# This enum lists the possible value formats a predicate can have (if it has a value).
@ -121,6 +126,7 @@ class Value( Enum ):
# Implemented in parse_operator
class Operators( Enum ):
RELATIONAL = auto() # One of '=', '<', '>', '\u2248' ('≈') (takes '~=' too)
RELATIONAL_EXACT = auto() # Like RELATIONAL but without the approximately equal operator
EQUAL = auto() # One of '=' or '!='
FILESERVICE_STATUS = auto() # One of 'is not currently in', 'is currently in', 'is not pending to', 'is pending to'
TAG_RELATIONAL = auto() # A tuple of a string (a potential tag name) and a relational operator (as a string)
@ -186,7 +192,12 @@ SYSTEM_PREDICATES = {
'(does not|doesn\'t) have (a )?(url with )?domain': (Predicate.NO_DOMAIN, None, Value.ANY_STRING, None),
'has (a )?url with (url )?class': (Predicate.URL_CLASS, None, Value.ANY_STRING, None),
'(does not|doesn\'t) have (a )?url with (url )?class': (Predicate.NO_URL_CLASS, None, Value.ANY_STRING, None),
'tag as number': (Predicate.TAG_AS_NUMBER, Operators.TAG_RELATIONAL, Value.INTEGER, None)
'tag as number': (Predicate.TAG_AS_NUMBER, Operators.TAG_RELATIONAL, Value.INTEGER, None),
'has notes?': (Predicate.HAS_NOTES, None, None, None),
'(no|does not have|doesn\'t have) notes': (Predicate.NO_NOTES, None, None, None),
'num(ber of)? notes': (Predicate.NUM_NOTES, Operators.RELATIONAL_EXACT, Value.NATURAL, None),
'(has (a )?)?note with name': (Predicate.HAS_NOTE_NAME, None, Value.ANY_STRING, None),
'(no|does not have|doesn\'t have) note with name': (Predicate.NO_NOTE_NAME, None, Value.ANY_STRING, None),
}
@ -344,17 +355,21 @@ def parse_operator( string: str, spec ):
string = string.strip()
if spec is None:
return string, None
elif spec == Operators.RELATIONAL:
ops = [ '\u2248', '=', '<', '>', '\u2260' ]
elif spec == Operators.RELATIONAL or spec == Operators.RELATIONAL_EXACT:
exact = spec == Operators.RELATIONAL_EXACT
ops = [ '=', '<', '>' ]
if not exact:
ops = ops + [ '\u2260', '\u2248' ]
if string.startswith( '==' ): return string[ 2: ], '='
if string.startswith( '!=' ): return string[ 2: ], '\u2260'
if string.startswith( 'is not' ): return string[ 6: ], '\u2260'
if string.startswith( 'isn\'t' ): return string[ 5: ], '\u2260'
if string.startswith( '~=' ): return string[ 2: ], '\u2248'
if not exact:
if string.startswith( '!=' ): return string[ 2: ], '\u2260'
if string.startswith( 'is not' ): return string[ 6: ], '\u2260'
if string.startswith( 'isn\'t' ): return string[ 5: ], '\u2260'
if string.startswith( '~=' ): return string[ 2: ], '\u2248'
for op in ops:
if string.startswith( op ): return string[ len( op ): ], op
if string.startswith( 'is' ): return string[ 2: ], '='
raise ValueError( "Invalid relation operator" )
raise ValueError( "Invalid relational operator" )
elif spec == Operators.EQUAL:
if string.startswith( '==' ): return string[ 2: ], '='
if string.startswith( '=' ): return string[ 1: ], '='
@ -436,8 +451,8 @@ examples = [
"system:similar to abcdef distance 5",
"system:limit is 5000",
"system:limit = 100",
"system:filetype is jpeg",
"system:filetype = image/jpg, image/png, apng",
#"system:filetype is jpeg",
#"system:filetype = image/jpg, image/png, apng",
"system:hash = abcdef1 abcdef2 abcdef3",
"system:hash = abcdef1 abcdef, abcdef4 md5",
"system:modified date < 7 years 45 days 70h",
@ -487,7 +502,15 @@ examples = [
"system:doesn't have domain test.com",
"system:has a url with class safebooru file page",
"system:doesn't have a url with url class safebooru file page ",
"system:tag as number page < 5"
"system:tag as number page < 5",
"system:has notes",
"system:no notes",
"system:does not have notes",
"system:num notes is 5",
"system:num notes > 1",
"system:has note with name note name",
"system:no note with name note name",
"system:does not have note with name note name"
]
if __name__ == "__main__":