1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
|
import os
from typing import Any, cast
import bottle
from tinydb import Query
from app.DatabaseManager import database_manager
def feed_id_to_td(feed_id: str) -> str:
return f'<td><a href="/{feed_id}">{feed_id}</a></td>'
def feed_last_build_date_to_td(last_build_date: str) -> str:
return f"<td>{last_build_date}</td>"
def feed_to_tr(feed: dict[str, str]) -> str:
return f'<tr>{feed_id_to_td(feed["feed_id"])}{feed_last_build_date_to_td(feed.get("feed_last_build_date", ""))}</tr>'
def opml_to_tr() -> str:
return '<tr><td colspan="2"><a href="/opml">All feeds (OPML)</a></td></tr>'
def feeds_to_table(feeds: list[dict[str, str]]) -> str:
if len(feeds) == 0:
return (
"No feeds (yet). Add feeds by defining them in your Aggrofile. "
+ "If you did that already, you might have to wait a bit for the data to propagate."
)
trs: list[str] = [feed_to_tr(feed) for feed in feeds] + [opml_to_tr()]
thead = f"<thead><tr><td>Feed</td><td>Last build date</td></tr></thead>"
tbody = f'<tbody>{"".join(trs)}</tbody>'
return "<table>" + thead + tbody + "</table>"
@bottle.route("/")
def index():
if database_manager.db is None:
raise Exception("Database is not initialized")
Q = Query()
_feeds = database_manager.feeds.all()
feeds = cast(list[dict[str, str]], _feeds)
bottle.response.set_header("content-type", "text/html")
page = f"""
<!doctype html>
<html lang="en">
<head>
<title>Aggro – Feed manipulator</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {{
font-family: Open Sans, Arial;
color: #050505;
font-size: 16px;
margin: 2em auto;
max-width: 800px;
padding: 1em;
line-height: 1.4;
text-align: justify;
}}
h1 {{
margin-bottom: 0;
}}
table {{
width: 100%;
border-collapse: collapse;
}}
table, th, td {{
border: 1px solid #e0e0e0;
}}
th, td {{
padding: 8px;
}}
footer {{
margin-top: 32px;
}}
</style>
</head>
<body>
<h1>Aggro</h2>
<i>Feed manipulator</i>
<h2>Feeds served at this address</h2>
{feeds_to_table(feeds)}
<footer>
<p>
<a href="https://github.com/jantuomi/aggro">Aggro</a> is MIT licensed open source software.
</p>
</footer>
</body>
</html>
"""
return page
@bottle.route("/<feed_id>")
def feed(feed_id: str):
if database_manager.db is None:
raise Exception("Database is not initialized")
Q = Query()
res = database_manager.feeds.search(Q.feed_id == feed_id)
if len(res) == 0:
bottle.abort(400, f"No feed found with id {feed_id}")
if len(res) > 1:
bottle.abort(400, f"Weird number of feeds found with id {feed_id}: {len(res)}")
bottle.response.set_header("content-type", "application/xml")
feed: Any = res[0]
feed_xml: str = feed["feed_xml"]
return feed_xml
@bottle.route("/opml")
def opml():
if database_manager.db is None:
raise Exception("Database is not initialized")
base_url = os.environ.get("AGGRO_BASE_URL", "http://localhost:8080")
_feeds = database_manager.feeds.all()
feeds = cast(list[dict[str, str]], _feeds)
bottle.response.set_header("content-type", "text/xml")
outlines = "\n".join(
f'<outline text="{feed["feed_id"]}" title="{feed["feed_id"]}" type="rss" xmlUrl="{base_url}/{feed["feed_id"]}" />'
for feed in feeds
)
opml = f"""<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
<head>
<title>Aggro Subscriptions</title>
</head>
<body>
{outlines}
</body>
</opml>
"""
return opml
def run_web_server(host: str, port: int):
bottle.run(host=host, port=port)
|