clubhouse: Strengthen types using WildValue.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg
2021-12-16 22:03:22 -08:00
committed by Tim Abbott
parent 573d264759
commit d5a8e040da

View File

@@ -1,5 +1,5 @@
from functools import partial from functools import partial
from typing import Any, Callable, Dict, Generator, List, Optional from typing import Callable, Dict, Iterable, Iterator, List, Optional
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
@@ -7,6 +7,16 @@ from zerver.decorator import webhook_view
from zerver.lib.exceptions import UnsupportedWebhookEventType from zerver.lib.exceptions import UnsupportedWebhookEventType
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.validator import (
WildValue,
check_bool,
check_int,
check_list,
check_none_or,
check_string,
check_string_or_int,
to_wild_value,
)
from zerver.lib.webhooks.common import check_send_webhook_message from zerver.lib.webhooks.common import check_send_webhook_message
from zerver.models import UserProfile from zerver.models import UserProfile
@@ -66,7 +76,7 @@ STORY_UPDATE_BATCH_CHANGED_SUB_TEMPLATE = "{entity_type} **{old}** to **{new}**"
STORY_UPDATE_BATCH_ADD_REMOVE_TEMPLATE = "{operation} with {entity}" STORY_UPDATE_BATCH_ADD_REMOVE_TEMPLATE = "{operation} with {entity}"
def get_action_with_primary_id(payload: Dict[str, Any]) -> Dict[str, Any]: def get_action_with_primary_id(payload: WildValue) -> WildValue:
for action in payload["actions"]: for action in payload["actions"]:
if payload["primary_id"] == action["id"]: if payload["primary_id"] == action["id"]:
action_with_primary_id = action action_with_primary_id = action
@@ -74,109 +84,121 @@ def get_action_with_primary_id(payload: Dict[str, Any]) -> Dict[str, Any]:
return action_with_primary_id return action_with_primary_id
def get_event(payload: Dict[str, Any], action: Dict[str, Any]) -> Optional[str]: def get_event(payload: WildValue, action: WildValue) -> Optional[str]:
event = "{}_{}".format(action["entity_type"], action["action"]) event = "{}_{}".format(
action["entity_type"].tame(check_string), action["action"].tame(check_string)
)
# We only consider the change to be a batch update only if there are multiple stories (thus there is no primary_id) # We only consider the change to be a batch update only if there are multiple stories (thus there is no primary_id)
if event == "story_update" and payload.get("primary_id") is None: if event == "story_update" and "primary_id" not in payload:
return "{}_{}".format(event, "batch") return "{}_{}".format(event, "batch")
if event in IGNORED_EVENTS: if event in IGNORED_EVENTS:
return None return None
changes = action.get("changes") if "changes" in action:
if changes is not None: changes = action["changes"]
if changes.get("description") is not None: if "description" in changes:
event = "{}_{}".format(event, "description") event = "{}_{}".format(event, "description")
elif changes.get("state") is not None: elif "state" in changes:
event = "{}_{}".format(event, "state") event = "{}_{}".format(event, "state")
elif changes.get("workflow_state_id") is not None: elif "workflow_state_id" in changes:
event = "{}_{}".format(event, "state") event = "{}_{}".format(event, "state")
elif changes.get("name") is not None: elif "name" in changes:
event = "{}_{}".format(event, "name") event = "{}_{}".format(event, "name")
elif changes.get("archived") is not None: elif "archived" in changes:
event = "{}_{}".format(event, "archived") event = "{}_{}".format(event, "archived")
elif changes.get("complete") is not None: elif "complete" in changes:
event = "{}_{}".format(event, "complete") event = "{}_{}".format(event, "complete")
elif changes.get("epic_id") is not None: elif "epic_id" in changes:
event = "{}_{}".format(event, "epic") event = "{}_{}".format(event, "epic")
elif changes.get("estimate") is not None: elif "estimate" in changes:
event = "{}_{}".format(event, "estimate") event = "{}_{}".format(event, "estimate")
elif changes.get("file_ids") is not None: elif "file_ids" in changes:
event = "{}_{}".format(event, "attachment") event = "{}_{}".format(event, "attachment")
elif changes.get("label_ids") is not None: elif "label_ids" in changes:
event = "{}_{}".format(event, "label") event = "{}_{}".format(event, "label")
elif changes.get("project_id") is not None: elif "project_id" in changes:
event = "{}_{}".format(event, "project") event = "{}_{}".format(event, "project")
elif changes.get("story_type") is not None: elif "story_type" in changes:
event = "{}_{}".format(event, "type") event = "{}_{}".format(event, "type")
elif changes.get("owner_ids") is not None: elif "owner_ids" in changes:
event = "{}_{}".format(event, "owner") event = "{}_{}".format(event, "owner")
return event return event
def get_topic_function_based_on_type(payload: Dict[str, Any], action: Dict[str, Any]) -> Any: def get_topic_function_based_on_type(
entity_type = action["entity_type"] payload: WildValue, action: WildValue
) -> Optional[Callable[[WildValue, WildValue], Optional[str]]]:
entity_type = action["entity_type"].tame(check_string)
return EVENT_TOPIC_FUNCTION_MAPPER.get(entity_type) return EVENT_TOPIC_FUNCTION_MAPPER.get(entity_type)
def get_delete_body(payload: Dict[str, Any], action: Dict[str, Any]) -> str: def get_delete_body(payload: WildValue, action: WildValue) -> str:
return DELETE_TEMPLATE.format(**action) return DELETE_TEMPLATE.format(
entity_type=action["entity_type"].tame(check_string),
name=action["name"].tame(check_string),
)
def get_story_create_body(payload: Dict[str, Any], action: Dict[str, Any]) -> str: def get_story_create_body(payload: WildValue, action: WildValue) -> str:
if action.get("epic_id") is None: if "epic_id" not in action:
message = "New story [{name}]({app_url}) of type **{story_type}** was created." message = "New story [{name}]({app_url}) of type **{story_type}** was created."
kwargs = action kwargs = {
"name": action["name"].tame(check_string),
"app_url": action["app_url"].tame(check_string),
"story_type": action["story_type"].tame(check_string),
}
else: else:
message = "New story [{name}]({app_url}) was created and added to the epic **{epic_name}**." message = "New story [{name}]({app_url}) was created and added to the epic **{epic_name}**."
kwargs = { kwargs = {
"name": action["name"], "name": action["name"].tame(check_string),
"app_url": action["app_url"], "app_url": action["app_url"].tame(check_string),
} }
epic_id = action["epic_id"] epic_id = action["epic_id"].tame(check_int)
refs = payload["references"] refs = payload["references"]
for ref in refs: for ref in refs:
if ref["id"] == epic_id: if ref["id"].tame(check_string_or_int) == epic_id:
kwargs["epic_name"] = ref["name"] kwargs["epic_name"] = ref["name"].tame(check_string)
return message.format(**kwargs) return message.format(**kwargs)
def get_epic_create_body(payload: Dict[str, Any], action: Dict[str, Any]) -> str: def get_epic_create_body(payload: WildValue, action: WildValue) -> str:
message = "New epic **{name}**({state}) was created." message = "New epic **{name}**({state}) was created."
return message.format(**action) return message.format(
name=action["name"].tame(check_string),
state=action["state"].tame(check_string),
)
def get_comment_added_body(payload: Dict[str, Any], action: Dict[str, Any], entity: str) -> str: def get_comment_added_body(payload: WildValue, action: WildValue, entity: str) -> str:
actions = payload["actions"] actions = payload["actions"]
kwargs = {"entity": entity} kwargs = {"entity": entity}
for action in actions: for action in actions:
if action["id"] == payload["primary_id"]: if action["id"] == payload["primary_id"]:
kwargs["text"] = action["text"] kwargs["text"] = action["text"].tame(check_string)
elif action["entity_type"] == entity: elif action["entity_type"] == entity:
name_template = get_name_template(entity).format( name_template = get_name_template(entity).format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action.get("app_url"), app_url=action.get("app_url").tame(check_none_or(check_string)),
) )
kwargs["name_template"] = name_template kwargs["name_template"] = name_template
return COMMENT_ADDED_TEMPLATE.format(**kwargs) return COMMENT_ADDED_TEMPLATE.format(**kwargs)
def get_update_description_body( def get_update_description_body(payload: WildValue, action: WildValue, entity: str) -> str:
payload: Dict[str, Any], action: Dict[str, Any], entity: str
) -> str:
desc = action["changes"]["description"] desc = action["changes"]["description"]
kwargs = { kwargs = {
"entity": entity, "entity": entity,
"new": desc["new"], "new": desc["new"].tame(check_string),
"old": desc["old"], "old": desc["old"].tame(check_string),
"name_template": get_name_template(entity).format( "name_template": get_name_template(entity).format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action.get("app_url"), app_url=action.get("app_url").tame(check_none_or(check_string)),
), ),
} }
@@ -190,58 +212,60 @@ def get_update_description_body(
return body return body
def get_epic_update_state_body(payload: Dict[str, Any], action: Dict[str, Any]) -> str: def get_epic_update_state_body(payload: WildValue, action: WildValue) -> str:
state = action["changes"]["state"] state = action["changes"]["state"]
kwargs = { kwargs = {
"entity": "epic", "entity": "epic",
"new": state["new"], "new": state["new"].tame(check_string),
"old": state["old"], "old": state["old"].tame(check_string),
"name_template": EPIC_NAME_TEMPLATE.format(name=action["name"]), "name_template": EPIC_NAME_TEMPLATE.format(
name=action["name"].tame(check_string),
),
} }
return STATE_CHANGED_TEMPLATE.format(**kwargs) return STATE_CHANGED_TEMPLATE.format(**kwargs)
def get_story_update_state_body(payload: Dict[str, Any], action: Dict[str, Any]) -> str: def get_story_update_state_body(payload: WildValue, action: WildValue) -> str:
workflow_state_id = action["changes"]["workflow_state_id"] workflow_state_id = action["changes"]["workflow_state_id"]
references = payload["references"] references = payload["references"]
state = {} state = {}
for ref in references: for ref in references:
if ref["id"] == workflow_state_id["new"]: if ref["id"].tame(check_string_or_int) == workflow_state_id["new"].tame(check_int):
state["new"] = ref["name"] state["new"] = ref["name"].tame(check_string)
if ref["id"] == workflow_state_id["old"]: if ref["id"].tame(check_string_or_int) == workflow_state_id["old"].tame(check_int):
state["old"] = ref["name"] state["old"] = ref["name"].tame(check_string)
kwargs = { kwargs = {
"entity": "story", "entity": "story",
"new": state["new"], "new": state["new"],
"old": state["old"], "old": state["old"],
"name_template": STORY_NAME_TEMPLATE.format( "name_template": STORY_NAME_TEMPLATE.format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action.get("app_url"), app_url=action.get("app_url").tame(check_none_or(check_string)),
), ),
} }
return STATE_CHANGED_TEMPLATE.format(**kwargs) return STATE_CHANGED_TEMPLATE.format(**kwargs)
def get_update_name_body(payload: Dict[str, Any], action: Dict[str, Any], entity: str) -> str: def get_update_name_body(payload: WildValue, action: WildValue, entity: str) -> str:
name = action["changes"]["name"] name = action["changes"]["name"]
kwargs = { kwargs = {
"entity": entity, "entity": entity,
"new": name["new"], "new": name["new"].tame(check_string),
"old": name["old"], "old": name["old"].tame(check_string),
"name_template": get_name_template(entity).format( "name_template": get_name_template(entity).format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action.get("app_url"), app_url=action.get("app_url").tame(check_none_or(check_string)),
), ),
} }
return NAME_CHANGED_TEMPLATE.format(**kwargs) return NAME_CHANGED_TEMPLATE.format(**kwargs)
def get_update_archived_body(payload: Dict[str, Any], action: Dict[str, Any], entity: str) -> str: def get_update_archived_body(payload: WildValue, action: WildValue, entity: str) -> str:
archived = action["changes"]["archived"] archived = action["changes"]["archived"]
if archived["new"]: if archived["new"]:
operation = "archived" operation = "archived"
@@ -251,8 +275,8 @@ def get_update_archived_body(payload: Dict[str, Any], action: Dict[str, Any], en
kwargs = { kwargs = {
"entity": entity, "entity": entity,
"name_template": get_name_template(entity).format( "name_template": get_name_template(entity).format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action.get("app_url"), app_url=action.get("app_url").tame(check_none_or(check_string)),
), ),
"operation": operation, "operation": operation,
} }
@@ -260,58 +284,63 @@ def get_update_archived_body(payload: Dict[str, Any], action: Dict[str, Any], en
return ARCHIVED_TEMPLATE.format(**kwargs) return ARCHIVED_TEMPLATE.format(**kwargs)
def get_story_task_body(payload: Dict[str, Any], action: Dict[str, Any], operation: str) -> str: def get_story_task_body(payload: WildValue, action: WildValue, operation: str) -> str:
kwargs = { kwargs = {
"task_description": action["description"], "task_description": action["description"].tame(check_string),
"operation": operation, "operation": operation,
} }
for a in payload["actions"]: for a in payload["actions"]:
if a["entity_type"] == "story": if a["entity_type"].tame(check_string) == "story":
kwargs["name_template"] = STORY_NAME_TEMPLATE.format( kwargs["name_template"] = STORY_NAME_TEMPLATE.format(
name=a["name"], name=a["name"].tame(check_string),
app_url=a["app_url"], app_url=a["app_url"].tame(check_string),
) )
return STORY_TASK_TEMPLATE.format(**kwargs) return STORY_TASK_TEMPLATE.format(**kwargs)
def get_story_task_completed_body(payload: Dict[str, Any], action: Dict[str, Any]) -> Optional[str]: def get_story_task_completed_body(payload: WildValue, action: WildValue) -> Optional[str]:
kwargs = { kwargs = {
"task_description": action["description"], "task_description": action["description"].tame(check_string),
} }
story_id = action["story_id"] story_id = action["story_id"].tame(check_int)
for ref in payload["references"]: for ref in payload["references"]:
if ref["id"] == story_id: if ref["id"].tame(check_string_or_int) == story_id:
kwargs["name_template"] = STORY_NAME_TEMPLATE.format( kwargs["name_template"] = STORY_NAME_TEMPLATE.format(
name=ref["name"], name=ref["name"].tame(check_string),
app_url=ref["app_url"], app_url=ref["app_url"].tame(check_string),
) )
if action["changes"]["complete"]["new"]: if action["changes"]["complete"]["new"].tame(check_bool):
return STORY_TASK_COMPLETED_TEMPLATE.format(**kwargs) return STORY_TASK_COMPLETED_TEMPLATE.format(**kwargs)
else: else:
return None return None
def get_story_update_epic_body(payload: Dict[str, Any], action: Dict[str, Any]) -> str: def get_story_update_epic_body(payload: WildValue, action: WildValue) -> str:
kwargs = { kwargs = {
"story_name_template": STORY_NAME_TEMPLATE.format( "story_name_template": STORY_NAME_TEMPLATE.format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action["app_url"], app_url=action["app_url"].tame(check_string),
), ),
} }
new_id = action["changes"]["epic_id"].get("new") epic_id = action["changes"]["epic_id"]
old_id = action["changes"]["epic_id"].get("old") new_id = epic_id.get("new").tame(check_none_or(check_int))
old_id = epic_id.get("old").tame(check_none_or(check_int))
for ref in payload["references"]: for ref in payload["references"]:
if ref["id"] == new_id: if ref["id"].tame(check_string_or_int) == new_id:
kwargs["new_epic_name_template"] = EPIC_NAME_TEMPLATE.format(name=ref["name"]) kwargs["new_epic_name_template"] = EPIC_NAME_TEMPLATE.format(
name=ref["name"].tame(check_string),
)
if ref["id"] == old_id: if ref["id"].tame(check_string_or_int) == old_id:
kwargs["old_epic_name_template"] = EPIC_NAME_TEMPLATE.format(name=ref["name"]) kwargs["old_epic_name_template"] = EPIC_NAME_TEMPLATE.format(
name=ref["name"].tame(check_string),
)
if new_id and old_id: if new_id and old_id:
return STORY_EPIC_CHANGED_TEMPLATE.format(**kwargs) return STORY_EPIC_CHANGED_TEMPLATE.format(**kwargs)
@@ -325,16 +354,17 @@ def get_story_update_epic_body(payload: Dict[str, Any], action: Dict[str, Any])
return STORY_ADDED_REMOVED_EPIC_TEMPLATE.format(**kwargs) return STORY_ADDED_REMOVED_EPIC_TEMPLATE.format(**kwargs)
def get_story_update_estimate_body(payload: Dict[str, Any], action: Dict[str, Any]) -> str: def get_story_update_estimate_body(payload: WildValue, action: WildValue) -> str:
kwargs = { kwargs = {
"story_name_template": STORY_NAME_TEMPLATE.format( "story_name_template": STORY_NAME_TEMPLATE.format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action["app_url"], app_url=action["app_url"].tame(check_string),
), ),
} }
new = action["changes"]["estimate"].get("new") estimate = action["changes"]["estimate"]
if new: if "new" in estimate:
new = estimate["new"].tame(check_int)
kwargs["estimate"] = f"{new} points" kwargs["estimate"] = f"{new} points"
else: else:
kwargs["estimate"] = "*Unestimated*" kwargs["estimate"] = "*Unestimated*"
@@ -342,45 +372,51 @@ def get_story_update_estimate_body(payload: Dict[str, Any], action: Dict[str, An
return STORY_ESTIMATE_TEMPLATE.format(**kwargs) return STORY_ESTIMATE_TEMPLATE.format(**kwargs)
def get_reference_by_id(payload: Dict[str, Any], ref_id: int) -> Dict[str, Any]: def get_reference_by_id(payload: WildValue, ref_id: Optional[int]) -> Optional[WildValue]:
ref: Dict[str, Any] = {} ref = None
for reference in payload["references"]: for reference in payload["references"]:
if reference["id"] == ref_id: if reference["id"].tame(check_string_or_int) == ref_id:
ref = reference ref = reference
return ref return ref
def get_secondary_actions_with_param( def get_secondary_actions_with_param(
payload: Dict[str, Any], entity: str, changed_attr: str payload: WildValue, entity: str, changed_attr: str
) -> Generator[Dict[str, Any], None, None]: ) -> Iterator[WildValue]:
# This function is a generator for secondary actions that have the required changed attributes, # This function is a generator for secondary actions that have the required changed attributes,
# i.e.: "story" that has "pull-request_ids" changed. # i.e.: "story" that has "pull-request_ids" changed.
for action in payload["actions"]: for action in payload["actions"]:
if action["entity_type"] == entity and action["changes"].get(changed_attr) is not None: if action["entity_type"].tame(check_string) == entity and changed_attr in action["changes"]:
yield action yield action
def get_story_create_github_entity_body( def get_story_create_github_entity_body(payload: WildValue, action: WildValue, entity: str) -> str:
payload: Dict[str, Any], action: Dict[str, Any], entity: str pull_request_action: WildValue = get_action_with_primary_id(payload)
) -> str:
pull_request_action: Dict[str, Any] = get_action_with_primary_id(payload)
kwargs = { kwargs = {
"name_template": STORY_NAME_TEMPLATE.format(**action), "name_template": STORY_NAME_TEMPLATE.format(
"name": pull_request_action.get("number") name=action["name"].tame(check_string),
app_url=action["app_url"].tame(check_string),
),
"name": pull_request_action["number"].tame(check_int)
if entity == "pull-request" or entity == "pull-request-comment" if entity == "pull-request" or entity == "pull-request-comment"
else pull_request_action.get("name"), else pull_request_action["name"].tame(check_string),
"url": pull_request_action["url"], "url": pull_request_action["url"].tame(check_string),
"workflow_state_template": "", "workflow_state_template": "",
} }
# Sometimes the workflow state of the story will not be changed when linking to a PR. # Sometimes the workflow state of the story will not be changed when linking to a PR.
if action["changes"].get("workflow_state_id") is not None: if "workflow_state_id" in action["changes"]:
new_state_id = action["changes"]["workflow_state_id"]["new"] workflow_state_id = action["changes"]["workflow_state_id"]
old_state_id = action["changes"]["workflow_state_id"]["old"] new_state_id = workflow_state_id["new"].tame(check_int)
new_state = get_reference_by_id(payload, new_state_id)["name"] old_state_id = workflow_state_id["old"].tame(check_int)
old_state = get_reference_by_id(payload, old_state_id)["name"] new_reference = get_reference_by_id(payload, new_state_id)
assert new_reference is not None
new_state = new_reference["name"].tame(check_string)
old_reference = get_reference_by_id(payload, old_state_id)
assert old_reference is not None
old_state = old_reference["name"].tame(check_string)
kwargs["workflow_state_template"] = TRAILING_WORKFLOW_STATE_CHANGE_TEMPLATE.format( kwargs["workflow_state_template"] = TRAILING_WORKFLOW_STATE_CHANGE_TEMPLATE.format(
new=new_state, old=old_state new=new_state, old=old_state
) )
@@ -394,34 +430,33 @@ def get_story_create_github_entity_body(
return template.format(**kwargs) return template.format(**kwargs)
def get_story_update_attachment_body( def get_story_update_attachment_body(payload: WildValue, action: WildValue) -> Optional[str]:
payload: Dict[str, Any], action: Dict[str, Any]
) -> Optional[str]:
kwargs = { kwargs = {
"name_template": STORY_NAME_TEMPLATE.format( "name_template": STORY_NAME_TEMPLATE.format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action["app_url"], app_url=action["app_url"].tame(check_string),
), ),
} }
file_ids_added = action["changes"]["file_ids"].get("adds") file_ids = action["changes"]["file_ids"]
# If this is a payload for when an attachment is removed, ignore it # If this is a payload for when an attachment is removed, ignore it
if not file_ids_added: if "adds" not in file_ids:
return None return None
file_ids_added = file_ids["adds"].tame(check_list(check_int))
file_id = file_ids_added[0] file_id = file_ids_added[0]
for ref in payload["references"]: for ref in payload["references"]:
if ref["id"] == file_id: if ref["id"].tame(check_string_or_int) == file_id:
kwargs.update( kwargs.update(
type=ref["entity_type"], type=ref["entity_type"].tame(check_string),
file_name=ref["name"], file_name=ref["name"].tame(check_string),
) )
return FILE_ATTACHMENT_TEMPLATE.format(**kwargs) return FILE_ATTACHMENT_TEMPLATE.format(**kwargs)
def get_story_joined_label_list( def get_story_joined_label_list(
payload: Dict[str, Any], action: Dict[str, Any], label_ids_added: List[int] payload: WildValue, action: WildValue, label_ids_added: List[int]
) -> str: ) -> str:
labels = [] labels = []
@@ -429,30 +464,32 @@ def get_story_joined_label_list(
label_name = "" label_name = ""
for action in payload["actions"]: for action in payload["actions"]:
if action.get("id") == label_id: if action["id"].tame(check_int) == label_id:
label_name = action.get("name", "") label_name = action.get("name", "").tame(check_string)
if label_name == "": if label_name == "":
label_name = get_reference_by_id(payload, label_id).get("name", "") reference = get_reference_by_id(payload, label_id)
label_name = "" if reference is None else reference["name"].tame(check_string)
labels.append(LABEL_TEMPLATE.format(name=label_name)) labels.append(LABEL_TEMPLATE.format(name=label_name))
return ", ".join(labels) return ", ".join(labels)
def get_story_label_body(payload: Dict[str, Any], action: Dict[str, Any]) -> Optional[str]: def get_story_label_body(payload: WildValue, action: WildValue) -> Optional[str]:
kwargs = { kwargs = {
"name_template": STORY_NAME_TEMPLATE.format( "name_template": STORY_NAME_TEMPLATE.format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action["app_url"], app_url=action["app_url"].tame(check_string),
), ),
} }
label_ids_added = action["changes"]["label_ids"].get("adds") label_ids = action["changes"]["label_ids"]
# If this is a payload for when no label is added, ignore it # If this is a payload for when no label is added, ignore it
if not label_ids_added: if "adds" not in label_ids:
return None return None
label_ids_added = label_ids["adds"].tame(check_list(check_int))
kwargs.update(labels=get_story_joined_label_list(payload, action, label_ids_added)) kwargs.update(labels=get_story_joined_label_list(payload, action, label_ids_added))
return ( return (
@@ -462,58 +499,60 @@ def get_story_label_body(payload: Dict[str, Any], action: Dict[str, Any]) -> Opt
) )
def get_story_update_project_body(payload: Dict[str, Any], action: Dict[str, Any]) -> str: def get_story_update_project_body(payload: WildValue, action: WildValue) -> str:
kwargs = { kwargs = {
"name_template": STORY_NAME_TEMPLATE.format( "name_template": STORY_NAME_TEMPLATE.format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action["app_url"], app_url=action["app_url"].tame(check_string),
), ),
} }
new_project_id = action["changes"]["project_id"]["new"] project_id = action["changes"]["project_id"]
old_project_id = action["changes"]["project_id"]["old"] new_project_id = project_id["new"].tame(check_int)
old_project_id = project_id["old"].tame(check_int)
for ref in payload["references"]: for ref in payload["references"]:
if ref["id"] == new_project_id: if ref["id"].tame(check_string_or_int) == new_project_id:
kwargs.update(new=ref["name"]) kwargs.update(new=ref["name"].tame(check_string))
if ref["id"] == old_project_id: if ref["id"].tame(check_string_or_int) == old_project_id:
kwargs.update(old=ref["name"]) kwargs.update(old=ref["name"].tame(check_string))
return STORY_UPDATE_PROJECT_TEMPLATE.format(**kwargs) return STORY_UPDATE_PROJECT_TEMPLATE.format(**kwargs)
def get_story_update_type_body(payload: Dict[str, Any], action: Dict[str, Any]) -> str: def get_story_update_type_body(payload: WildValue, action: WildValue) -> str:
story_type = action["changes"]["story_type"]
kwargs = { kwargs = {
"name_template": STORY_NAME_TEMPLATE.format( "name_template": STORY_NAME_TEMPLATE.format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action["app_url"], app_url=action["app_url"].tame(check_string),
), ),
"new_type": action["changes"]["story_type"]["new"], "new_type": story_type["new"].tame(check_string),
"old_type": action["changes"]["story_type"]["old"], "old_type": story_type["old"].tame(check_string),
} }
return STORY_UPDATE_TYPE_TEMPLATE.format(**kwargs) return STORY_UPDATE_TYPE_TEMPLATE.format(**kwargs)
def get_story_update_owner_body(payload: Dict[str, Any], action: Dict[str, Any]) -> str: def get_story_update_owner_body(payload: WildValue, action: WildValue) -> str:
kwargs = { kwargs = {
"name_template": STORY_NAME_TEMPLATE.format( "name_template": STORY_NAME_TEMPLATE.format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action["app_url"], app_url=action["app_url"].tame(check_string),
), ),
} }
return STORY_UPDATE_OWNER_TEMPLATE.format(**kwargs) return STORY_UPDATE_OWNER_TEMPLATE.format(**kwargs)
def get_story_update_batch_body(payload: Dict[str, Any], action: Dict[str, Any]) -> Optional[str]: def get_story_update_batch_body(payload: WildValue, action: WildValue) -> Optional[str]:
# When the user selects one or more stories with the checkbox, they can perform # When the user selects one or more stories with the checkbox, they can perform
# a batch update on multiple stories while changing multiple attribtues at the # a batch update on multiple stories while changing multiple attribtues at the
# same time. # same time.
changes = action["changes"] changes = action["changes"]
kwargs = { kwargs = {
"name_template": STORY_NAME_TEMPLATE.format( "name_template": STORY_NAME_TEMPLATE.format(
name=action["name"], name=action["name"].tame(check_string),
app_url=action["app_url"], app_url=action["app_url"].tame(check_string),
), ),
"workflow_state_template": "", "workflow_state_template": "",
} }
@@ -524,20 +563,34 @@ def get_story_update_batch_body(payload: Dict[str, Any], action: Dict[str, Any])
move_sub_templates = [] move_sub_templates = []
if "epic_id" in changes: if "epic_id" in changes:
last_change = "epic" last_change = "epic"
epic_id = changes["epic_id"]
old_reference = get_reference_by_id(
payload, epic_id.get("old").tame(check_none_or(check_int))
)
new_reference = get_reference_by_id(
payload, epic_id.get("new").tame(check_none_or(check_int))
)
move_sub_templates.append( move_sub_templates.append(
STORY_UPDATE_BATCH_CHANGED_SUB_TEMPLATE.format( STORY_UPDATE_BATCH_CHANGED_SUB_TEMPLATE.format(
entity_type="Epic", entity_type="Epic",
old=get_reference_by_id(payload, changes["epic_id"].get("old")).get("name"), old=None if old_reference is None else old_reference["name"].tame(check_string),
new=get_reference_by_id(payload, changes["epic_id"].get("new")).get("name"), new=None if new_reference is None else new_reference["name"].tame(check_string),
) )
) )
if "project_id" in changes: if "project_id" in changes:
last_change = "project" last_change = "project"
project_id = changes["project_id"]
old_reference = get_reference_by_id(
payload, project_id.get("old").tame(check_none_or(check_int))
)
new_reference = get_reference_by_id(
payload, project_id.get("new").tame(check_none_or(check_int))
)
move_sub_templates.append( move_sub_templates.append(
STORY_UPDATE_BATCH_CHANGED_SUB_TEMPLATE.format( STORY_UPDATE_BATCH_CHANGED_SUB_TEMPLATE.format(
entity_type="Project", entity_type="Project",
old=get_reference_by_id(payload, changes["project_id"].get("old")).get("name"), old=None if old_reference is None else old_reference["name"].tame(check_string),
new=get_reference_by_id(payload, changes["project_id"].get("new")).get("name"), new=None if new_reference is None else new_reference["name"].tame(check_string),
) )
) )
if len(move_sub_templates) > 0: if len(move_sub_templates) > 0:
@@ -550,42 +603,47 @@ def get_story_update_batch_body(payload: Dict[str, Any], action: Dict[str, Any])
if "story_type" in changes: if "story_type" in changes:
last_change = "type" last_change = "type"
story_type = changes["story_type"]
templates.append( templates.append(
STORY_UPDATE_BATCH_CHANGED_TEMPLATE.format( STORY_UPDATE_BATCH_CHANGED_TEMPLATE.format(
operation="{} changed".format("was" if len(templates) == 0 else "and"), operation="{} changed".format("was" if len(templates) == 0 else "and"),
sub_templates=STORY_UPDATE_BATCH_CHANGED_SUB_TEMPLATE.format( sub_templates=STORY_UPDATE_BATCH_CHANGED_SUB_TEMPLATE.format(
entity_type="type", entity_type="type",
old=changes["story_type"].get("old"), old=story_type.get("old").tame(check_none_or(check_string)),
new=changes["story_type"].get("new"), new=story_type.get("new").tame(check_none_or(check_string)),
), ),
) )
) )
if "label_ids" in changes: if "label_ids" in changes:
label_ids_added = changes["label_ids"].get("adds") label_ids = changes["label_ids"]
# If this is a payload for when no label is added, ignore it # If this is a payload for when no label is added, ignore it
if label_ids_added is not None: if "adds" in label_ids:
label_ids_added = label_ids["adds"].tame(check_list(check_int))
last_change = "label" last_change = "label"
labels = get_story_joined_label_list(payload, action, label_ids_added) labels = get_story_joined_label_list(payload, action, label_ids_added)
templates.append( templates.append(
STORY_UPDATE_BATCH_ADD_REMOVE_TEMPLATE.format( STORY_UPDATE_BATCH_ADD_REMOVE_TEMPLATE.format(
operation="{} added".format("was" if len(templates) == 0 else "and"), operation="{} added".format("was" if len(templates) == 0 else "and"),
entity="the new label{plural} {labels}".format( entity="the new label{plural} {labels}".format(
plural="s" if len(changes["label_ids"]) > 1 else "", labels=labels plural="s" if len(label_ids) > 1 else "", labels=labels
), ),
) )
) )
if "workflow_state_id" in changes: if "workflow_state_id" in changes:
last_change = "state" last_change = "state"
workflow_state_id = changes["workflow_state_id"]
old_reference = get_reference_by_id(
payload, workflow_state_id.get("old").tame(check_none_or(check_int))
)
new_reference = get_reference_by_id(
payload, workflow_state_id.get("new").tame(check_none_or(check_int))
)
kwargs.update( kwargs.update(
workflow_state_template=TRAILING_WORKFLOW_STATE_CHANGE_TEMPLATE.format( workflow_state_template=TRAILING_WORKFLOW_STATE_CHANGE_TEMPLATE.format(
old=get_reference_by_id(payload, changes["workflow_state_id"].get("old")).get( old=None if old_reference is None else old_reference["name"].tame(check_string),
"name" new=None if new_reference is None else new_reference["name"].tame(check_string),
),
new=get_reference_by_id(payload, changes["workflow_state_id"].get("new")).get(
"name"
),
) )
) )
@@ -604,19 +662,19 @@ def get_story_update_batch_body(payload: Dict[str, Any], action: Dict[str, Any])
def get_entity_name( def get_entity_name(
payload: Dict[str, Any], action: Dict[str, Any], entity: Optional[str] = None payload: WildValue, action: WildValue, entity: Optional[str] = None
) -> Optional[str]: ) -> Optional[str]:
name = action.get("name") name = action["name"].tame(check_string) if "name" in action else None
if name is None or action["entity_type"] == "branch": if name is None or action["entity_type"] == "branch":
for action in payload["actions"]: for action in payload["actions"]:
if action["entity_type"] == entity: if action["entity_type"].tame(check_string) == entity:
name = action["name"] name = action["name"].tame(check_string)
if name is None: if name is None:
for ref in payload["references"]: for ref in payload["references"]:
if ref["entity_type"] == entity: if ref["entity_type"].tame(check_string) == entity:
name = ref["name"] name = ref["name"].tame(check_string)
return name return name
@@ -630,8 +688,8 @@ def get_name_template(entity: str) -> str:
def send_stream_messages_for_actions( def send_stream_messages_for_actions(
request: HttpRequest, request: HttpRequest,
user_profile: UserProfile, user_profile: UserProfile,
payload: Dict[str, Any], payload: WildValue,
action: Dict[str, Any], action: WildValue,
event: str, event: str,
) -> None: ) -> None:
body_func = EVENT_BODY_FUNCTION_MAPPER.get(event) body_func = EVENT_BODY_FUNCTION_MAPPER.get(event)
@@ -646,7 +704,7 @@ def send_stream_messages_for_actions(
check_send_webhook_message(request, user_profile, topic, body, event) check_send_webhook_message(request, user_profile, topic, body, event)
EVENT_BODY_FUNCTION_MAPPER: Dict[str, Callable[[Dict[str, Any], Dict[str, Any]], Optional[str]]] = { EVENT_BODY_FUNCTION_MAPPER: Dict[str, Callable[[WildValue, WildValue], Optional[str]]] = {
"story_update_archived": partial(get_update_archived_body, entity="story"), "story_update_archived": partial(get_update_archived_body, entity="story"),
"epic_update_archived": partial(get_update_archived_body, entity="epic"), "epic_update_archived": partial(get_update_archived_body, entity="epic"),
"story_create": get_story_create_body, "story_create": get_story_create_body,
@@ -681,7 +739,7 @@ EVENT_BODY_FUNCTION_MAPPER: Dict[str, Callable[[Dict[str, Any], Dict[str, Any]],
ALL_EVENT_TYPES = list(EVENT_BODY_FUNCTION_MAPPER.keys()) ALL_EVENT_TYPES = list(EVENT_BODY_FUNCTION_MAPPER.keys())
EVENT_TOPIC_FUNCTION_MAPPER = { EVENT_TOPIC_FUNCTION_MAPPER: Dict[str, Callable[[WildValue, WildValue], Optional[str]]] = {
"story": partial(get_entity_name, entity="story"), "story": partial(get_entity_name, entity="story"),
"pull-request": partial(get_entity_name, entity="story"), "pull-request": partial(get_entity_name, entity="story"),
"branch": partial(get_entity_name, entity="story"), "branch": partial(get_entity_name, entity="story"),
@@ -695,9 +753,7 @@ IGNORED_EVENTS = {
"story-comment_update", "story-comment_update",
} }
EVENTS_SECONDARY_ACTIONS_FUNCTION_MAPPER: Dict[ EVENTS_SECONDARY_ACTIONS_FUNCTION_MAPPER: Dict[str, Callable[[WildValue], Iterator[WildValue]]] = {
str, Callable[[Dict[str, Any]], Generator[Dict[str, Any], None, None]]
] = {
"pull-request_create": partial( "pull-request_create": partial(
get_secondary_actions_with_param, entity="story", changed_attr="pull_request_ids" get_secondary_actions_with_param, entity="story", changed_attr="pull_request_ids"
), ),
@@ -715,19 +771,19 @@ EVENTS_SECONDARY_ACTIONS_FUNCTION_MAPPER: Dict[
def api_clubhouse_webhook( def api_clubhouse_webhook(
request: HttpRequest, request: HttpRequest,
user_profile: UserProfile, user_profile: UserProfile,
payload: Optional[Dict[str, Any]] = REQ(argument_type="body"), payload: WildValue = REQ(argument_type="body", converter=to_wild_value),
) -> HttpResponse: ) -> HttpResponse:
# Clubhouse has a tendency to send empty POST requests to # Clubhouse has a tendency to send empty POST requests to
# third-party endpoints. It is unclear as to which event type # third-party endpoints. It is unclear as to which event type
# such requests correspond to. So, it is best to ignore such # such requests correspond to. So, it is best to ignore such
# requests for now. # requests for now.
if payload is None: if payload.value is None:
return json_success(request) return json_success(request)
if payload.get("primary_id") is not None: if "primary_id" in payload:
action = get_action_with_primary_id(payload) action = get_action_with_primary_id(payload)
primary_actions = [action] primary_actions: Iterable[WildValue] = [action]
else: else:
primary_actions = payload["actions"] primary_actions = payload["actions"]