-
Notifications
You must be signed in to change notification settings - Fork 322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement NCMEC Upvote/Downvote in reference API #1624
Conversation
UPDATE_FEEDBACK_RESULT_XML = """ | ||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | ||
<submissionResult xmlns="https://hashsharing.ncmec.org/hashsharing/v2"> | ||
<!-- What this returns is not documented --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a good start. Once the CI is passing, let me know and I'll test it with some real credentials to confirm it works as expected.
from threatexchange.exchanges.clients.utils.common import TimeoutHTTPAdapter | ||
|
||
|
||
_DATE_FORMAT_STR = "%Y-%m-%dT%H:%M:%SZ" | ||
_DEFAULT_ELE = ET.Element("") | ||
|
||
T = t.TypeVar("T") | ||
FEEDBACK = t.List[t.Dict[str, str]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
blocking: ALL_CAPS usually denotes a constant. In this case, your type is simple enough that you should just inline it.
@@ -131,6 +132,7 @@ class NCMECEntryUpdate: | |||
deleted: bool | |||
classification: t.Optional[str] | |||
fingerprints: t.Dict[str, str] | |||
feedback: t.Optional[FEEDBACK] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
blocking: We should not use optional here, because empty dictionary expresses the same thing
@@ -148,6 +150,18 @@ def from_xml(cls, xml: _XMLWrapper) -> "NCMECEntryUpdate": | |||
fingerprints={ | |||
x.tag: x.text for x in xml.maybe("fingerprints") if x.has_text | |||
}, | |||
feedback=[ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
blocking; Your type says dict, your code says list!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type is t.List[t.Dict[str, str]]
@@ -401,6 +432,14 @@ def get_entries_iter( | |||
has_more = bool(next_) | |||
yield result | |||
|
|||
# TODO: split into 2, submit upvote and downvote | |||
def submit_feedback(self, entry_id: str, is_good: bool) -> GetEntriesResponse: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unsure: This API also needs the ESP ID, no?
@@ -401,6 +432,14 @@ def get_entries_iter( | |||
has_more = bool(next_) | |||
yield result | |||
|
|||
# TODO: split into 2, submit upvote and downvote |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
blocking; Yup
send_upvote(...):
send_downvote(...):
python-threatexchange/threatexchange/exchanges/clients/ncmec/tests/data.py
Show resolved
Hide resolved
NEXT_UNESCAPED2 = ( | ||
"/v2/entries?from=2017-10-20T00%3A00%3A00.000Z" | ||
"&to=2017-10-30T00%3A00%3A00.000Z&start=3001&size=1000&max=4000" | ||
from threatexchange.exchanges.clients.ncmec.tests.data import ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ignorable: Consider importing by module when you are importing many functions.
), | ||
"reasons": x.maybe( | ||
"reasons" | ||
), # "reason" with "guid", "name", "type" | "members" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
working on expanding members
and reasons
here
@Dcallies this should be testable against the NCMEC API now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the hard work @aryzle ! We'll take it from here and test it against the real API and finish it off! You can consider this one done!
@@ -261,15 +343,19 @@ def __init__( | |||
username: str, | |||
password: str, | |||
environment: NCMECEnvironment, | |||
member_id: t.Optional[str] = None, | |||
reasons_map: t.Dict[str, t.List[t.Dict[str, str]]] = {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
blocking: We won't know this at client construction time, and can poll it afterwards.
) -> None: | ||
assert is_valid_user_pass(username, password) | ||
self.username = username | ||
self.password = password | ||
self._base_url = environment.value | ||
self.member_id = member_id | ||
self.reasons_map = reasons_map or {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm this or {}
is fairly subtle. I'm assuming we are doing it because the default is {}
which is a shared instance across any object, and the or {}
will replace it with a new instance per call, but having it be optional and default to None
is more easy to prove doesn't have a bug.
if not affirmative and not reason_id: | ||
raise ValueError("Negative feedback must have a reason_id") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(note to self): If reason map only has one item in it, choose that item. Also test NCMEC API to make sure it won't accept empty reason, which should be our pref otherwise.
@@ -123,6 +123,19 @@ class NCMECEntryType(Enum): | |||
video = "video" | |||
|
|||
|
|||
@unique | |||
class NCMECFeedbackType(Enum): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(note to self): Add note that these are also the entry types
@@ -131,6 +144,7 @@ class NCMECEntryUpdate: | |||
deleted: bool | |||
classification: t.Optional[str] | |||
fingerprints: t.Dict[str, str] | |||
feedback: t.List[t.Dict[str, t.Any]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(note to self): Consider whether it makes sense to use the enum here, or whether to leave as string in all cases
"members": [ | ||
{"id": m.str("id"), "name": m.text} | ||
for m in x.maybe("members") | ||
if m.has_text | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(note to self): Should this be dict instead of synthesizing a a dict? What about a dedicated class?
{ | ||
"guid": reason.str("guid"), | ||
"name": reason.str("name"), | ||
"type": reason.str("type"), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(note to self): Use a dedicated class here instead of dict
entry_id: str, | ||
feedback_type: NCMECFeedbackType, | ||
affirmative: bool, | ||
reason_id: t.Optional[str] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(note to self): Is a reason string better to use here than the guid?
Continuing in #1648 - there's probably a way to have re-used this number, but I don't know it :/ |
Summary
Implement NCMEC Upvote/Downvote in reference API
docs: https://report.cybertip.org/hashsharing/v2/documentation/#working-with-feedback
fixes #1614
Test Plan