Skip to content
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

Closed
wants to merge 6 commits into from

Conversation

aryzle
Copy link
Collaborator

@aryzle aryzle commented Sep 18, 2024

Summary

Implement NCMEC Upvote/Downvote in reference API
docs: https://report.cybertip.org/hashsharing/v2/documentation/#working-with-feedback

fixes #1614

Test Plan

  • unit test for success flow
  • David to test on NCMEC API

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 -->
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Dcallies feedback submission has no response documented here

Copy link
Contributor

@Dcallies Dcallies left a 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]]
Copy link
Contributor

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]
Copy link
Contributor

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=[
Copy link
Contributor

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!

Copy link
Collaborator Author

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:
Copy link
Contributor

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
Copy link
Contributor

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(...):

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 (
Copy link
Contributor

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"
Copy link
Collaborator Author

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

@aryzle
Copy link
Collaborator Author

aryzle commented Sep 30, 2024

@Dcallies this should be testable against the NCMEC API now

@aryzle aryzle marked this pull request as ready for review September 30, 2024 13:53
Copy link
Contributor

@Dcallies Dcallies left a 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]]] = {},
Copy link
Contributor

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 {}
Copy link
Contributor

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.

Comment on lines +545 to +546
if not affirmative and not reason_id:
raise ValueError("Negative feedback must have a reason_id")
Copy link
Contributor

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):
Copy link
Contributor

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]]
Copy link
Contributor

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

Comment on lines +171 to +175
"members": [
{"id": m.str("id"), "name": m.text}
for m in x.maybe("members")
if m.has_text
],
Copy link
Contributor

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?

Comment on lines +290 to +294
{
"guid": reason.str("guid"),
"name": reason.str("name"),
"type": reason.str("type"),
}
Copy link
Contributor

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,
Copy link
Contributor

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?

@Dcallies
Copy link
Contributor

Dcallies commented Oct 3, 2024

Continuing in #1648 - there's probably a way to have re-used this number, but I don't know it :/

@Dcallies Dcallies closed this Oct 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[py-tx][hack] Implement NCMEC Upvote/Downvote in reference API
3 participants