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

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 

10 

11feeds = flask.Blueprint( 

12 "feeds", 

13 __name__, 

14) 

15 

16session = Session() 

17snap_recommendations = SnapRecommendations(session) 

18 

19 

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 

29 

30 

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) 

38 

39 

40def create_snap_description(snap): 

41 """Create HTML description for RSS item.""" 

42 description_parts = [] 

43 

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 ) 

49 

50 if snap.get("summary"): 

51 summary = escape(snap["summary"]) 

52 description_parts.append(f"<p>{summary}</p>") 

53 

54 additional_info = [] 

55 

56 if snap.get("publisher"): 

57 publisher = escape(snap["publisher"]) 

58 additional_info.append(f"<li>Developer: {publisher}</li>") 

59 

60 if snap.get("version"): 

61 version = escape(snap["version"]) 

62 additional_info.append(f"<li>Version: {version}</li>") 

63 

64 if additional_info: 

65 description_parts.append("<ul>" + "".join(additional_info) + "</ul>") 

66 

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="">') 

76 

77 return "".join(description_parts) 

78 

79 

80@feeds.route("/feeds/updates") 

81def recently_updated_feed(): 

82 """Generate RSS feed for recently updated snaps.""" 

83 

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") 

91 

92 size = int(flask.request.args.get("size", "50")) 

93 

94 page = int(flask.request.args.get("page", "1")) 

95 

96 try: 

97 response = snap_recommendations.get_recently_updated( 

98 page=page, size=size, timeout=10 

99 ) 

100 

101 snaps = response.get("snaps", []) 

102 

103 except (requests.RequestException, ValueError) as e: 

104 flask.current_app.logger.error(f"Failed to fetch recommendations: {e}") 

105 snaps = [] 

106 

107 for snap in snaps: 

108 try: 

109 fe = fg.add_entry() 

110 

111 title = escape(snap.get("title")) 

112 

113 fe.title(title) 

114 

115 snap_name = snap.get("name") 

116 snap_url = f"https://snapcraft.io/{snap_name}" 

117 fe.link(href=snap_url) 

118 

119 description = create_snap_description(snap) 

120 fe.description(description) 

121 

122 pub_date = parse_snap_date(snap["last_updated"]) 

123 fe.pubDate(pub_date) 

124 

125 except Exception as e: 

126 flask.current_app.logger.error( 

127 f"Failed to add snap to RSS feed: {e}" 

128 ) 

129 continue 

130 

131 rss_str = fg.rss_str(pretty=True) 

132 

133 response = Response(rss_str, mimetype="application/rss+xml") 

134 response.headers["Cache-Control"] = "public, max-age=86400" 

135 

136 return response