Skip to content

Commit 21b5287

Browse files
committed
Add method to report fraud
1 parent 5e79117 commit 21b5287

File tree

3 files changed

+331
-4
lines changed

3 files changed

+331
-4
lines changed

libs/labelbox/src/labelbox/client.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,51 @@ def delete_model_config(self, id: str) -> bool:
503503
raise ResourceNotFoundError(Entity.ModelConfig, params)
504504
return result["deleteModelConfig"]["success"]
505505

506+
def delete_project_memberships(
507+
self, project_id: str, user_ids: list[str]
508+
) -> dict:
509+
"""Deletes project memberships for one or more users.
510+
511+
Args:
512+
project_id (str): ID of the project
513+
user_ids (list[str]): List of user IDs to remove from the project
514+
515+
Returns:
516+
dict: Result containing:
517+
- success (bool): True if operation succeeded
518+
- errorMessage (str or None): Error message if operation failed
519+
520+
Example:
521+
>>> result = client.delete_project_memberships(
522+
>>> project_id="project123",
523+
>>> user_ids=["user1", "user2"]
524+
>>> )
525+
>>> if result["success"]:
526+
>>> print("Users removed successfully")
527+
>>> else:
528+
>>> print(f"Error: {result['errorMessage']}")
529+
"""
530+
mutation = """mutation DeleteProjectMembershipsPyApi(
531+
$projectId: ID!
532+
$userIds: [ID!]!
533+
) {
534+
deleteProjectMemberships(where: {
535+
projectId: $projectId
536+
userIds: $userIds
537+
}) {
538+
success
539+
errorMessage
540+
}
541+
}"""
542+
543+
params = {
544+
"projectId": project_id,
545+
"userIds": user_ids,
546+
}
547+
548+
result = self.execute(mutation, params)
549+
return result["deleteProjectMemberships"]
550+
506551
def create_dataset(
507552
self, iam_integration=IAMIntegration._DEFAULT, **kwargs
508553
) -> Dataset:

libs/labelbox/src/labelbox/schema/project.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -317,14 +317,28 @@ def get_resource_tags(self) -> List[ResourceTag]:
317317

318318
return [ResourceTag(self.client, tag) for tag in results]
319319

320-
def labels(self, datasets=None, order_by=None) -> PaginatedCollection:
320+
def labels(self, datasets=None, order_by=None, created_by=None) -> PaginatedCollection:
321321
"""Custom relationship expansion method to support limited filtering.
322322
323323
Args:
324324
datasets (iterable of Dataset): Optional collection of Datasets
325325
whose Labels are sought. If not provided, all Labels in
326326
this Project are returned.
327327
order_by (None or (Field, Field.Order)): Ordering clause.
328+
created_by (str or User): Optional. Filter labels by the user who created them.
329+
Can be a user ID string or a User object.
330+
331+
Returns:
332+
PaginatedCollection of Labels matching the filters.
333+
334+
Example:
335+
>>> # Get all labels
336+
>>> all_labels = project.labels()
337+
>>>
338+
>>> # Get labels by specific user
339+
>>> user_labels = project.labels(created_by=user_id)
340+
>>> # or
341+
>>> user_labels = project.labels(created_by=user_object)
328342
"""
329343
Label = Entity.Label
330344

@@ -335,10 +349,20 @@ def labels(self, datasets=None, order_by=None) -> PaginatedCollection:
335349
stacklevel=2,
336350
)
337351

352+
# Build where clause
353+
where_clauses = []
354+
338355
if datasets is not None:
339-
where = " where:{dataRow: {dataset: {id_in: [%s]}}}" % ", ".join(
340-
'"%s"' % dataset.uid for dataset in datasets
341-
)
356+
dataset_ids = ", ".join('"%s"' % dataset.uid for dataset in datasets)
357+
where_clauses.append(f"dataRow: {{dataset: {{id_in: [{dataset_ids}]}}}}")
358+
359+
if created_by is not None:
360+
# Handle both User object and user_id string
361+
user_id = created_by.uid if hasattr(created_by, 'uid') else created_by
362+
where_clauses.append(f'createdBy: {{id: "{user_id}"}}')
363+
364+
if where_clauses:
365+
where = " where:{" + ", ".join(where_clauses) + "}"
342366
else:
343367
where = ""
344368

@@ -370,6 +394,31 @@ def labels(self, datasets=None, order_by=None) -> PaginatedCollection:
370394
Label,
371395
)
372396

397+
def delete_labels_by_user(self, user_id: str) -> int:
398+
"""Soft deletes all labels created by a specific user in this project.
399+
400+
This performs a soft delete (sets deleted=true in the database).
401+
The labels will no longer appear in queries but remain in the database.
402+
403+
Args:
404+
user_id (str): The ID of the user whose labels to delete.
405+
406+
Returns:
407+
int: Number of labels deleted.
408+
409+
Example:
410+
>>> project = client.get_project(project_id)
411+
>>> deleted_count = project.delete_labels_by_user(user_id)
412+
>>> print(f"Deleted {deleted_count} labels")
413+
"""
414+
labels_to_delete = list(self.labels(created_by=user_id))
415+
416+
if not labels_to_delete:
417+
return 0
418+
419+
Entity.Label.bulk_delete(labels_to_delete)
420+
return len(labels_to_delete)
421+
373422
def export(
374423
self,
375424
task_name: Optional[str] = None,

libs/lbox-alignerr/src/alignerr/alignerr_project.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
ProjectBoostWorkforce,
1414
)
1515
from labelbox.pagination import PaginatedCollection
16+
from labelbox.orm.model import Entity
1617

1718
logger = logging.getLogger(__name__)
1819

@@ -153,6 +154,238 @@ def get_project_owner(self) -> Optional[ProjectBoostWorkforce]:
153154
client=self.client, project_id=self.project.uid
154155
)
155156

157+
def _get_user_labels(self, user_id: str):
158+
"""Get all labels created by a user in this project.
159+
160+
Args:
161+
user_id: ID of the user
162+
163+
Returns:
164+
List of Label objects
165+
166+
Raises:
167+
Exception: If labels cannot be retrieved
168+
"""
169+
labels = list(self.project.labels(created_by=user_id))
170+
logger.info(
171+
"Found %d labels created by user %s in project %s",
172+
len(labels),
173+
user_id,
174+
self.project.uid
175+
)
176+
return labels
177+
178+
def _create_trust_safety_case(self, user_id: str, event_metadata: dict) -> bool:
179+
"""Create a Trust & Safety case for a user.
180+
181+
Args:
182+
user_id: ID of the user being reported
183+
event_metadata: JSON metadata about the event
184+
185+
Returns:
186+
True if case was created successfully
187+
188+
Raises:
189+
Exception: If T&S case creation fails
190+
"""
191+
mutation = """mutation CreateTrustAndSafetyCasePyApi(
192+
$subjectUserId: String!
193+
$eventType: CaseEventGqlType!
194+
$severity: CaseSeverityGqlType!
195+
$eventMetadata: Json!
196+
) {
197+
createTrustAndSafetyCase(input: {
198+
subjectUserId: $subjectUserId
199+
eventType: $eventType
200+
severity: $severity
201+
eventMetadata: $eventMetadata
202+
}) {
203+
success
204+
}
205+
}"""
206+
207+
params = {
208+
"subjectUserId": user_id,
209+
"eventType": "manual",
210+
"severity": "high",
211+
"eventMetadata": event_metadata,
212+
}
213+
214+
result = self.client.execute(mutation, params)
215+
success = result["createTrustAndSafetyCase"]["success"]
216+
217+
if success:
218+
logger.info(
219+
"Created T&S case for user %s in project %s",
220+
user_id,
221+
self.project.uid
222+
)
223+
224+
return success
225+
226+
def _remove_user_from_project(self, user_id: str) -> None:
227+
"""Remove a user from this project.
228+
229+
Args:
230+
user_id: ID of the user to remove
231+
232+
Raises:
233+
ValueError: If user not found in project
234+
Exception: If removal fails
235+
"""
236+
# Check if user is in project members
237+
user_found = False
238+
for member in self.project.members():
239+
if member.user().uid == user_id:
240+
user_found = True
241+
break
242+
243+
if not user_found:
244+
logger.warning("User %s not found in project %s members", user_id, self.project.uid)
245+
raise ValueError(f"User {user_id} not found in project members")
246+
247+
# Remove user using deleteProjectMemberships mutation
248+
result = self.client.delete_project_memberships(
249+
project_id=self.project.uid,
250+
user_ids=[user_id]
251+
)
252+
253+
if not result.get("success"):
254+
error_message = result.get("errorMessage", "Unknown error")
255+
logger.error("Failed to remove user: %s", error_message)
256+
raise Exception(f"Failed to remove user: {error_message}")
257+
258+
logger.info(
259+
"Removed user %s from project %s",
260+
user_id,
261+
self.project.uid
262+
)
263+
264+
def _delete_user_labels(self, labels) -> int:
265+
"""Delete a list of labels.
266+
267+
Args:
268+
labels: List of Label objects to delete
269+
270+
Returns:
271+
Number of labels deleted
272+
273+
Raises:
274+
Exception: If deletion fails
275+
"""
276+
if not labels:
277+
return 0
278+
279+
Entity.Label.bulk_delete(labels)
280+
logger.info(
281+
"Deleted %d labels in project %s",
282+
len(labels),
283+
self.project.uid
284+
)
285+
return len(labels)
286+
287+
def report_fraud(
288+
self,
289+
user_id: str,
290+
reason: str,
291+
custom_metadata: dict = None
292+
) -> dict:
293+
"""Report potential fraud by a user in this project.
294+
295+
This method performs the following actions:
296+
1. Gets all labels created by the user in this project
297+
2. Creates a Trust & Safety case for the user (MANUAL event type, HIGH severity)
298+
3. Removes the user from the project (prevents creating more labels)
299+
4. Deletes all the user's labels
300+
301+
Args:
302+
user_id (str): The ID of the user to report for fraud.
303+
reason (str): Reason for reporting fraud (e.g., "Spam labels", "Low quality work").
304+
custom_metadata (dict, optional): Additional metadata to include in the T&S case.
305+
Will be merged with automatic metadata (project_id, reason, label_count, label_ids).
306+
307+
Returns:
308+
dict: A dictionary containing:
309+
- ts_case_id: Status of T&S case creation ("created" if successful)
310+
- labels_found: Number of labels found by the user
311+
- user_removed: Whether the user was successfully removed
312+
- labels_deleted: Number of labels deleted
313+
- error: Any error message if any step failed
314+
315+
Example:
316+
>>> from alignerr import AlignerrWorkspace
317+
>>> from labelbox import Client
318+
>>>
319+
>>> client = Client(api_key="YOUR_API_KEY")
320+
>>> workspace = AlignerrWorkspace.from_labelbox(client)
321+
>>> project = workspace.project_builder().from_existing(project_id)
322+
>>>
323+
>>> # Report fraud with reason
324+
>>> result = project.report_fraud(user_id, reason="Spam labels detected")
325+
>>> print(f"Removed user: {result['user_removed']}, Deleted {result['labels_deleted']} labels")
326+
>>>
327+
>>> # With additional custom metadata
328+
>>> result = project.report_fraud(
329+
>>> user_id,
330+
>>> reason="Production quality issues",
331+
>>> custom_metadata={"ticket_id": "TICKET-123", "reviewer": "john@example.com"}
332+
>>> )
333+
"""
334+
result = {
335+
"ts_case_id": None,
336+
"labels_found": 0,
337+
"user_removed": False,
338+
"labels_deleted": 0,
339+
"error": None,
340+
}
341+
342+
# Step 1: Get all labels cteated by this user in this project
343+
try:
344+
labels_to_delete = self._get_user_labels(user_id)
345+
result["labels_found"] = len(labels_to_delete)
346+
except Exception as e:
347+
logger.error("Failed to get labels: %s", str(e))
348+
result["error"] = f"Failed to get labels: {str(e)}"
349+
return result
350+
351+
# Step 2: Create T&S case with label information
352+
try:
353+
event_metadata = {
354+
"project_id": self.project.uid,
355+
"reason": reason,
356+
"label_count": len(labels_to_delete),
357+
"label_ids": [label.uid for label in labels_to_delete],
358+
}
359+
if custom_metadata:
360+
event_metadata.update(custom_metadata)
361+
362+
ts_case_created = self._create_trust_safety_case(user_id, event_metadata)
363+
if ts_case_created:
364+
result["ts_case_id"] = "created"
365+
except Exception as e:
366+
logger.error("Failed to create T&S case: %s", str(e))
367+
result["error"] = f"Failed to create T&S case: {str(e)}"
368+
return result
369+
370+
# Step 3: Remove user from project (prevent creating more labels)
371+
try:
372+
self._remove_user_from_project(user_id)
373+
result["user_removed"] = True
374+
except Exception as e:
375+
logger.error("Failed to remove user from project: %s", str(e))
376+
result["error"] = f"Failed to remove user: {str(e)}"
377+
return result
378+
379+
# Step 4: Delete all labels by this user
380+
try:
381+
result["labels_deleted"] = self._delete_user_labels(labels_to_delete)
382+
except Exception as e:
383+
logger.error("Failed to delete labels: %s", str(e))
384+
result["error"] = f"Failed to delete labels: {str(e)}"
385+
return result
386+
387+
return result
388+
156389

157390
class AlignerrWorkspace:
158391
def __init__(self, client: "Client"):

0 commit comments

Comments
 (0)