Skip to content

Commit

Permalink
Properly handle Immunefi asset revisions
Browse files Browse the repository at this point in the history
  • Loading branch information
muellerberndt committed Dec 30, 2024
1 parent 1404a64 commit d122935
Show file tree
Hide file tree
Showing 10 changed files with 599 additions and 524 deletions.
8 changes: 7 additions & 1 deletion src/backend/asset_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ def get_asset_path(base_dir: str, url: str) -> Tuple[str, str]:
Raises:
ValueError: If target directory would be outside base directory
"""
parsed_url = urlparse(url)
# Basic URL validation
try:
parsed_url = urlparse(url)
if not all([parsed_url.scheme, parsed_url.netloc]):
raise ValueError(f"Invalid URL format: {url}")
except Exception as e:
raise ValueError(f"URL parsing failed: {url} - {str(e)}")

# Ensure path components don't start with slash
path_components = [component for component in parsed_url.path.split("/") if component and component != "/"]
Expand Down
250 changes: 250 additions & 0 deletions src/handlers/asset_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
from typing import List, Union, Dict, Any, Optional
from src.handlers.base import Handler, HandlerTrigger, HandlerResult
from src.services.telegram import TelegramService
from src.util.logging import Logger
from src.models.base import Asset, AssetType
import difflib
import io
from datetime import datetime


class AssetEventHandler(Handler):
"""Handler for asset-related events"""

def __init__(self):
super().__init__()
self.logger = Logger("AssetEventHandler")
self.telegram = TelegramService.get_instance()

@classmethod
def get_triggers(cls) -> List[HandlerTrigger]:
"""Get list of triggers this handler listens for"""
return [HandlerTrigger.NEW_ASSET, HandlerTrigger.ASSET_UPDATE, HandlerTrigger.ASSET_REMOVE]

async def handle(self) -> HandlerResult:
"""Handle the event based on trigger type"""
try:
if not self.context or not self.trigger:
return HandlerResult(success=False, data={"error": "Missing context or trigger"})

self.logger.debug(f"Handler received context with keys: {list(self.context.keys())}")

if self.trigger == HandlerTrigger.ASSET_UPDATE:
return await self._handle_asset_update(
asset=self.context.get("asset"),
old_path=self.context.get("old_path"),
new_path=self.context.get("new_path"),
old_revision=self.context.get("old_revision"),
new_revision=self.context.get("new_revision"),
old_code=self.context.get("old_code"),
new_code=self.context.get("new_code"),
)
elif self.trigger == HandlerTrigger.ASSET_REMOVE:
result = await self._handle_asset_removal(self.context.get("asset"))
else:
result = await self._handle_new_asset(self.context.get("asset"))

return HandlerResult(success=True, data=result)

except Exception as e:
self.logger.error(f"Failed to handle asset event: {str(e)}")
return HandlerResult(success=False, data={"error": str(e)})

def _get_asset_attr(self, asset: Union[Asset, Dict[str, Any]], attr: str, default: Any = None) -> Any:
"""Helper method to get attribute from either Asset object or dictionary"""
if isinstance(asset, dict):
return asset.get(attr, default)
return getattr(asset, attr, default)

async def _handle_asset_update(
self,
asset: Union[Asset, Dict[str, Any]],
old_path: Optional[str],
new_path: Optional[str],
old_revision: Optional[str],
new_revision: Optional[str],
old_code: Optional[str] = None,
new_code: Optional[str] = None,
) -> dict:
"""Handle asset update"""
project_name = self._get_asset_attr(asset, "project").name if hasattr(asset, "project") else "Unknown Project"
source_url = self._get_asset_attr(asset, "source_url", "N/A")
asset_type = self._get_asset_attr(asset, "asset_type", "N/A")

self.logger.debug(f"Handler received - old_code: {bool(old_code)}, new_code: {bool(new_code)}")
self.logger.debug(f"Asset type in handler: {asset_type}")

message_parts = ["📝 Asset Updated", f"🔗 Project: {project_name}", f"🔗 URL: {source_url}", f"📁 Type: {asset_type}"]

# Show revision changes and diffs if revisions are different
if old_revision is not None and new_revision is not None and old_revision != new_revision:
message_parts.append(f"📝 Revision: {old_revision}{new_revision}")

# Only create diff if we have both code versions
if old_code and new_code and asset_type in [AssetType.GITHUB_FILE, AssetType.DEPLOYED_CONTRACT]:
try:
self.logger.debug("Creating diff...")
# Create HTML diff
diff_html = self._create_html_diff(
old_code, new_code, f"Revision {old_revision}", f"Revision {new_revision}", project_name, source_url
)

# Create file-like object with the HTML content
html_bytes = diff_html.encode("utf-8")
html_io = io.BytesIO(html_bytes)
html_io.name = f"diff_{project_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"

self.logger.debug("Sending message first...")
# Send the message first
await self.telegram.send_message("\n".join(message_parts))

self.logger.debug("Sending document...")
# Then send the HTML file
await self.telegram.send_document(html_io)
return HandlerResult(
success=True,
data={
"event": "asset_updated",
"project": project_name,
"source_url": source_url,
"old_revision": old_revision,
"new_revision": new_revision,
"message": "Asset update processed with diff",
},
)

except Exception as e:
self.logger.error(f"Failed to generate diff: {e}")
message_parts.append("\nFailed to generate diff")
elif asset_type == AssetType.GITHUB_REPO:
message_parts.append("\nℹ️ No diff available for repository updates")
else:
message_parts.append("\nℹ️ Code comparison not available")

# Only send message if we haven't already sent it with the diff
if not (old_code and new_code and asset_type in [AssetType.GITHUB_FILE, AssetType.DEPLOYED_CONTRACT]):
await self.telegram.send_message("\n".join(message_parts))

return HandlerResult(
success=True,
data={
"event": "asset_updated",
"project": project_name,
"source_url": source_url,
"old_revision": old_revision,
"new_revision": new_revision,
},
)

def _create_html_diff(
self, old_code: str, new_code: str, old_title: str, new_title: str, project_name: str, source_url: str
) -> str:
"""Create an HTML diff view with syntax highlighting"""
old_lines = old_code.splitlines()
new_lines = new_code.splitlines()

diff = difflib.HtmlDiff()
html = diff.make_file(old_lines, new_lines, fromdesc=old_title, todesc=new_title, context=True)

# Enhance the HTML with better styling
enhanced_html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Code Diff - {project_name}</title>
<style>
body {{
font-family: monospace;
margin: 0;
padding: 20px;
background: #f5f5f5;
}}
.header {{
background: #fff;
padding: 20px;
margin-bottom: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}}
.diff {{
background: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
overflow-x: auto;
}}
table.diff {{
font-family: monospace;
border-collapse: collapse;
width: 100%;
}}
.diff td {{
padding: 1px 4px;
vertical-align: top;
white-space: pre;
font-size: 14px;
}}
.diff_header {{
background-color: #f8f8f8;
}}
td.diff_header {{
text-align: right;
padding: 1px 4px;
border-right: 1px solid #ddd;
background-color: #f8f8f8;
width: 40px;
}}
.diff_next {{
background-color: #f8f8f8;
padding: 1px 4px;
}}
.diff_add {{
background-color: #e6ffe6;
}}
.diff_chg {{
background-color: #fff3d4;
}}
.diff_sub {{
background-color: #ffe6e6;
}}
</style>
</head>
<body>
<div class="header">
<h2>{project_name}</h2>
<p>Source: <a href="{source_url}">{source_url}</a></p>
<p>Comparing {old_title} with {new_title}</p>
</div>
<div class="diff">
"""

# Insert our enhanced header and style
html = html.replace("<html>", enhanced_html)

# Close the diff div
html = html.replace("</body>", "</div></body>")

return html

async def _handle_asset_removal(self, asset: Union[Asset, Dict[str, Any]]) -> dict:
"""Handle asset removal"""
project_name = self._get_asset_attr(asset, "project").name if hasattr(asset, "project") else "Unknown Project"
source_url = self._get_asset_attr(asset, "source_url", "N/A")

message = f"❌ Asset Removed\n🔗 Project: {project_name}\n🔗 URL: {source_url}"
await self.telegram.send_message(message)

return {"event": "asset_removed", "project": project_name, "source_url": source_url}

async def _handle_new_asset(self, asset: Union[Asset, Dict[str, Any]]) -> dict:
"""Handle new asset"""
project_name = self._get_asset_attr(asset, "project").name if hasattr(asset, "project") else "Unknown Project"
source_url = self._get_asset_attr(asset, "source_url", "N/A")
asset_type = self._get_asset_attr(asset, "asset_type", "N/A")

message = ["🆕 New Asset Added", f"🔗 Project: {project_name}", f"🔗 URL: {source_url}", f"📁 Type: {asset_type}"]

await self.telegram.send_message("\n".join(message))

return {"event": "new_asset", "project": project_name, "source_url": source_url}
4 changes: 2 additions & 2 deletions src/handlers/builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
from typing import List, Type
from src.handlers.base import Handler
from src.handlers.project_events import ProjectEventHandler
from src.handlers.immunefi_asset_event_handler import ImmunefiAssetEventHandler
from src.handlers.asset_events import AssetEventHandler
from src.handlers.github_event import GitHubEventHandler
from src.handlers.proxy_upgrade import ProxyUpgradeHandler


def get_builtin_handlers() -> List[Type[Handler]]:
"""Get all built-in handlers that should be registered by default"""
return [ProjectEventHandler, ImmunefiAssetEventHandler, GitHubEventHandler, ProxyUpgradeHandler]
return [ProjectEventHandler, AssetEventHandler, GitHubEventHandler, ProxyUpgradeHandler]
101 changes: 0 additions & 101 deletions src/handlers/immunefi_asset_event_handler.py

This file was deleted.

Loading

0 comments on commit d122935

Please sign in to comment.