Coverage for webapp / feeds / feeds.py: 87%
85 statements
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-29 22:06 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-29 22:06 +0000
1import flask
2import requests
3from datetime import datetime, timezone
4from flask import Response
5from feedgen.feed import FeedGenerator
6from requests import Session
7from html import escape
8from urllib.parse import urlparse
9from canonicalwebteam.snap_recommendations import SnapRecommendations
11feeds = flask.Blueprint(
12 "feeds",
13 __name__,
14)
16session = Session()
17snap_recommendations = SnapRecommendations(session)
20def is_safe_url(url):
21 """Check if URL is safe (http/https only)."""
22 if not url:
23 return False
24 try:
25 parsed = urlparse(url)
26 return parsed.scheme in ("http", "https") and parsed.netloc
27 except Exception:
28 return False
31def parse_snap_date(date_string):
32 """Parse date string from API to datetime object."""
33 try:
34 dt = datetime.strptime(date_string, "%a, %d %b %Y %H:%M:%S %Z")
35 return dt.replace(tzinfo=timezone.utc)
36 except ValueError:
37 return datetime.now(timezone.utc)
40def create_snap_description(snap):
41 """Create HTML description for RSS item."""
42 description_parts = []
44 if snap.get("icon") and is_safe_url(snap["icon"]):
45 icon_url = escape(snap["icon"])
46 description_parts.append(
47 f'<img src="{icon_url}" alt="Snap icon" width="128" height="128">'
48 )
50 if snap.get("summary"):
51 summary = escape(snap["summary"])
52 description_parts.append(f"<p>{summary}</p>")
54 additional_info = []
56 if snap.get("publisher"):
57 publisher = escape(snap["publisher"])
58 additional_info.append(f"<li>Developer: {publisher}</li>")
60 if snap.get("version"):
61 version = escape(snap["version"])
62 additional_info.append(f"<li>Version: {version}</li>")
64 if additional_info:
65 description_parts.append("<ul>" + "".join(additional_info) + "</ul>")
67 if snap.get("media"):
68 for media in snap["media"]:
69 if (
70 media.get("type") == "screenshot"
71 and media.get("url")
72 and is_safe_url(media["url"])
73 ):
74 media_url = escape(media["url"])
75 description_parts.append(f'<img src="{media_url}" alt="">')
77 return "".join(description_parts)
80@feeds.route("/feeds/updates")
81def recently_updated_feed():
82 """Generate RSS feed for recently updated snaps."""
84 fg = FeedGenerator()
85 fg.title("Snapcraft - recently updated snaps")
86 fg.link(href="https://snapcraft.io/store", rel="alternate")
87 fg.description("Recently updated snaps published on Snapcraft")
88 fg.language("en")
89 fg.docs("http://www.rssboard.org/rss-specification")
90 fg.generator("python-feedgen")
92 size = int(flask.request.args.get("size", "50"))
94 page = int(flask.request.args.get("page", "1"))
96 try:
97 response = snap_recommendations.get_recently_updated(
98 page=page, size=size, timeout=10
99 )
101 snaps = response.get("snaps", [])
103 except (requests.RequestException, ValueError) as e:
104 flask.current_app.logger.error(f"Failed to fetch recommendations: {e}")
105 snaps = []
107 for snap in snaps:
108 try:
109 fe = fg.add_entry()
111 title = escape(snap.get("title"))
113 fe.title(title)
115 snap_name = snap.get("name")
116 snap_url = f"https://snapcraft.io/{snap_name}"
117 fe.link(href=snap_url)
119 description = create_snap_description(snap)
120 fe.description(description)
122 pub_date = parse_snap_date(snap["last_updated"])
123 fe.pubDate(pub_date)
125 except Exception as e:
126 flask.current_app.logger.error(
127 f"Failed to add snap to RSS feed: {e}"
128 )
129 continue
131 rss_str = fg.rss_str(pretty=True)
133 response = Response(rss_str, mimetype="application/rss+xml")
134 response.headers["Cache-Control"] = "public, max-age=86400"
136 return response