aboutsummaryrefslogtreecommitdiffstats
path: root/pysite
diff options
context:
space:
mode:
Diffstat (limited to 'pysite')
-rw-r--r--pysite/base_route.py5
-rw-r--r--pysite/tables.py8
-rw-r--r--pysite/views/api/bot/bigbrother.py118
-rw-r--r--pysite/views/api/bot/off_topic_names.py2
-rw-r--r--pysite/views/api/robots_txt.py15
-rw-r--r--pysite/views/api/sitemap_xml.py11
-rw-r--r--pysite/views/main/robots_txt.py15
-rw-r--r--pysite/views/main/sitemap_xml.py69
-rw-r--r--pysite/views/staff/robots_txt.py15
-rw-r--r--pysite/views/staff/sitemap_xml.py11
-rw-r--r--pysite/views/wiki/edit.py24
-rw-r--r--pysite/views/wiki/robots_txt.py15
-rw-r--r--pysite/views/wiki/sitemap_xml.py22
13 files changed, 318 insertions, 12 deletions
diff --git a/pysite/base_route.py b/pysite/base_route.py
index 51e5fbe0..8178b142 100644
--- a/pysite/base_route.py
+++ b/pysite/base_route.py
@@ -128,7 +128,10 @@ class RouteView(BaseView):
"kwargs": kwargs
}
- return redirect(url_for("discord.login"))
+ response = redirect(url_for("discord.login"))
+ response.headers.add("X-Robots-Tag", "noindex")
+
+ return response
class APIView(RouteView):
diff --git a/pysite/tables.py b/pysite/tables.py
index 617a20f0..8f849664 100644
--- a/pysite/tables.py
+++ b/pysite/tables.py
@@ -257,5 +257,13 @@ TABLES = {
"key", # str
"value" # any
])
+ ),
+
+ "watched_users": Table( # Users being monitored by the bot's BigBrother cog
+ primary_key="user_id",
+ keys=sorted([
+ "user_id",
+ "channel_id"
+ ])
)
}
diff --git a/pysite/views/api/bot/bigbrother.py b/pysite/views/api/bot/bigbrother.py
new file mode 100644
index 00000000..89697811
--- /dev/null
+++ b/pysite/views/api/bot/bigbrother.py
@@ -0,0 +1,118 @@
+import json
+
+from flask import jsonify
+from schema import And, Optional, Schema
+
+from pysite.base_route import APIView
+from pysite.constants import ValidationTypes
+from pysite.decorators import api_key, api_params
+from pysite.mixins import DBMixin
+
+
+GET_SCHEMA = Schema({
+ # This is passed as a GET parameter, so it has to be a string
+ Optional('user_id'): And(str, str.isnumeric, error="`user_id` must be a numeric string")
+})
+
+POST_SCHEMA = Schema({
+ 'user_id': And(str, str.isnumeric, error="`user_id` must be a numeric string"),
+ 'channel_id': And(str, str.isnumeric, error="`channel_id` must be a numeric string")
+})
+
+DELETE_SCHEMA = Schema({
+ 'user_id': And(str, str.isnumeric, error="`user_id` must be a numeric string")
+})
+
+
+NOT_A_NUMBER_JSON = json.dumps({
+ 'error_message': "The given `user_id` parameter is not a valid number"
+})
+NOT_FOUND_JSON = json.dumps({
+ 'error_message': "No entry for the requested user ID could be found."
+})
+
+
+class BigBrotherView(APIView, DBMixin):
+ path = '/bot/bigbrother'
+ name = 'bot.bigbrother'
+ table_name = 'watched_users'
+
+ @api_key
+ @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params)
+ def get(self, params):
+ """
+ Without query parameters, returns a list of all monitored users.
+ A parameter `user_id` can be specified to return a single entry,
+ or a dictionary with the string field 'error_message' that tells why it failed.
+
+ If the returned status is 200, has got either a list of entries
+ or a single object (see above).
+
+ If the returned status is 400, the `user_id` parameter was incorrectly specified.
+ If the returned status is 404, the given `user_id` could not be found.
+ See the 'error_message' field in the JSON response for more information.
+
+ The user ID must be provided as query parameter.
+ API key must be provided as header.
+ """
+
+ user_id = params.get('user_id')
+ if user_id is not None:
+ data = self.db.get(self.table_name, user_id)
+ if data is None:
+ return NOT_FOUND_JSON, 404
+ return jsonify(data)
+
+ else:
+ data = self.db.pluck(self.table_name, ('user_id', 'channel_id')) or []
+ return jsonify(data)
+
+ @api_key
+ @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json)
+ def post(self, data):
+ """
+ Adds a new entry to the database.
+ Entries take the following form:
+ {
+ "user_id": ..., # The user ID of the user being monitored, as a string.
+ "channel_id": ... # The channel ID that the user's messages will be relayed to, as a string.
+ }
+
+ If an entry for the given `user_id` already exists, it will be updated with the new channel ID.
+
+ Returns 204 (ok, empty response) on success.
+
+ Data must be provided as JSON.
+ API key must be provided as header.
+ """
+
+ self.db.insert(
+ self.table_name,
+ {
+ 'user_id': data['user_id'],
+ 'channel_id': data['channel_id']
+ },
+ conflict='update'
+ )
+
+ return '', 204
+
+ @api_key
+ @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.params)
+ def delete(self, params):
+ """
+ Removes an entry for the given `user_id`.
+
+ Returns 204 (ok, empty response) on success.
+ Returns 400 if the given `user_id` is invalid.
+
+ The user ID must be provided as query parameter.
+ API key must be provided as header.
+ """
+
+ self.db.delete(
+ self.table_name,
+ params['user_id']
+ )
+
+ return '', 204
diff --git a/pysite/views/api/bot/off_topic_names.py b/pysite/views/api/bot/off_topic_names.py
index e0871067..f353ab02 100644
--- a/pysite/views/api/bot/off_topic_names.py
+++ b/pysite/views/api/bot/off_topic_names.py
@@ -13,7 +13,7 @@ POST_SCHEMA = Schema({
'name': And(
str,
len,
- lambda name: all(c.isalpha() or c == '-' for c in name),
+ lambda name: all(c.isalnum() or c == '-' for c in name),
str.islower,
lambda name: len(name) <= 96,
error=(
diff --git a/pysite/views/api/robots_txt.py b/pysite/views/api/robots_txt.py
new file mode 100644
index 00000000..d4406d54
--- /dev/null
+++ b/pysite/views/api/robots_txt.py
@@ -0,0 +1,15 @@
+from flask import Response, url_for
+
+from pysite.base_route import RouteView
+
+
+class RobotsTXT(RouteView):
+ path = "/robots.txt"
+ name = "robots_txt"
+
+ def get(self):
+ return Response(
+ self.render(
+ "robots.txt", sitemap_url=url_for("api.sitemap_xml", _external=True), rules={"*": ["/"]}
+ ), content_type="text/plain"
+ )
diff --git a/pysite/views/api/sitemap_xml.py b/pysite/views/api/sitemap_xml.py
new file mode 100644
index 00000000..26a786b0
--- /dev/null
+++ b/pysite/views/api/sitemap_xml.py
@@ -0,0 +1,11 @@
+from flask import Response
+
+from pysite.base_route import RouteView
+
+
+class SitemapXML(RouteView):
+ path = "/sitemap.xml"
+ name = "sitemap_xml"
+
+ def get(self):
+ return Response(self.render("sitemap.xml", urls=[]), content_type="application/xml")
diff --git a/pysite/views/main/robots_txt.py b/pysite/views/main/robots_txt.py
new file mode 100644
index 00000000..308fe2a2
--- /dev/null
+++ b/pysite/views/main/robots_txt.py
@@ -0,0 +1,15 @@
+from flask import Response, url_for
+
+from pysite.base_route import RouteView
+
+
+class RobotsTXT(RouteView):
+ path = "/robots.txt"
+ name = "robots_txt"
+
+ def get(self):
+ return Response(
+ self.render(
+ "robots.txt", sitemap_url=url_for("api.sitemap_xml", _external=True)
+ ), content_type="text/plain"
+ )
diff --git a/pysite/views/main/sitemap_xml.py b/pysite/views/main/sitemap_xml.py
new file mode 100644
index 00000000..98893c21
--- /dev/null
+++ b/pysite/views/main/sitemap_xml.py
@@ -0,0 +1,69 @@
+from flask import Response, url_for
+
+from pysite.base_route import RouteView
+
+
+class SitemapXML(RouteView):
+ path = "/sitemap.xml"
+ name = "sitemap_xml"
+
+ def get(self):
+ urls = [
+ {
+ "type": "url",
+ "url": url_for("main.index", _external=True),
+ "priority": 1.0, # Max priority
+
+ "images": [
+ {
+ "caption": "Python Discord Logo",
+ "url": url_for("static", filename="logos/logo_discord.png", _external=True)
+ },
+ {
+ "caption": "Python Discord Banner",
+ "url": url_for("static", filename="logos/logo_banner.png", _external=True)
+ }
+ ]
+ },
+
+ {
+ "type": "url",
+ "url": url_for("main.jams.index", _external=True),
+ "priority": 0.9 # Above normal priority
+ },
+
+ {
+ "type": "url",
+ "url": url_for("main.about.privacy", _external=True),
+ "priority": 0.8 # Above normal priority
+ },
+ {
+ "type": "url",
+ "url": url_for("main.about.rules", _external=True),
+ "priority": 0.8 # Above normal priority
+ },
+
+ {
+ "type": "url",
+ "url": url_for("main.info.help", _external=True),
+ "priority": 0.7 # Above normal priority
+ },
+ {
+ "type": "url",
+ "url": url_for("main.info.faq", _external=True),
+ "priority": 0.7 # Above normal priority
+ },
+ {
+ "type": "url",
+ "url": url_for("main.info.resources", _external=True),
+ "priority": 0.7 # Above normal priority
+ },
+
+ {
+ "type": "url",
+ "url": url_for("main.about.partners", _external=True),
+ "priority": 0.6 # Normal priority
+ },
+ ]
+
+ return Response(self.render("sitemap.xml", urls=urls), content_type="application/xml")
diff --git a/pysite/views/staff/robots_txt.py b/pysite/views/staff/robots_txt.py
new file mode 100644
index 00000000..308fe2a2
--- /dev/null
+++ b/pysite/views/staff/robots_txt.py
@@ -0,0 +1,15 @@
+from flask import Response, url_for
+
+from pysite.base_route import RouteView
+
+
+class RobotsTXT(RouteView):
+ path = "/robots.txt"
+ name = "robots_txt"
+
+ def get(self):
+ return Response(
+ self.render(
+ "robots.txt", sitemap_url=url_for("api.sitemap_xml", _external=True)
+ ), content_type="text/plain"
+ )
diff --git a/pysite/views/staff/sitemap_xml.py b/pysite/views/staff/sitemap_xml.py
new file mode 100644
index 00000000..26a786b0
--- /dev/null
+++ b/pysite/views/staff/sitemap_xml.py
@@ -0,0 +1,11 @@
+from flask import Response
+
+from pysite.base_route import RouteView
+
+
+class SitemapXML(RouteView):
+ path = "/sitemap.xml"
+ name = "sitemap_xml"
+
+ def get(self):
+ return Response(self.render("sitemap.xml", urls=[]), content_type="application/xml")
diff --git a/pysite/views/wiki/edit.py b/pysite/views/wiki/edit.py
index 65378807..949c9942 100644
--- a/pysite/views/wiki/edit.py
+++ b/pysite/views/wiki/edit.py
@@ -40,16 +40,20 @@ class EditView(RouteView, DBMixin, RMQMixin):
lock_expiry = datetime.datetime.utcnow() + datetime.timedelta(minutes=5)
- if not DEBUG_MODE: # If we are in debug mode we have no user logged in, therefore we can skip locking
- self.db.insert(
- self.table_name,
- {
- "slug": page,
- "lock_expiry": lock_expiry.timestamp(),
- "lock_user": self.user_data.get("user_id")
- },
- conflict="update"
- )
+ # There are a couple of cases where we will not need to lock a page. One of these is if the application is
+ # current set to debug mode. The other of these cases is if the page is empty, because if the page is empty
+ # we will only have a partially filled out page if the user quits before saving.
+ if obj:
+ if not DEBUG_MODE and obj.get("rst"):
+ self.db.insert(
+ self.table_name,
+ {
+ "slug": page,
+ "lock_expiry": lock_expiry.timestamp(),
+ "lock_user": self.user_data.get("user_id")
+ },
+ conflict="update"
+ )
return self.render("wiki/page_edit.html", page=page, rst=rst, title=title, preview=preview, can_edit=True)
diff --git a/pysite/views/wiki/robots_txt.py b/pysite/views/wiki/robots_txt.py
new file mode 100644
index 00000000..308fe2a2
--- /dev/null
+++ b/pysite/views/wiki/robots_txt.py
@@ -0,0 +1,15 @@
+from flask import Response, url_for
+
+from pysite.base_route import RouteView
+
+
+class RobotsTXT(RouteView):
+ path = "/robots.txt"
+ name = "robots_txt"
+
+ def get(self):
+ return Response(
+ self.render(
+ "robots.txt", sitemap_url=url_for("api.sitemap_xml", _external=True)
+ ), content_type="text/plain"
+ )
diff --git a/pysite/views/wiki/sitemap_xml.py b/pysite/views/wiki/sitemap_xml.py
new file mode 100644
index 00000000..9b7f0980
--- /dev/null
+++ b/pysite/views/wiki/sitemap_xml.py
@@ -0,0 +1,22 @@
+from flask import Response, url_for
+
+from pysite.base_route import RouteView
+from pysite.mixins import DBMixin
+
+
+class SitemapXML(RouteView, DBMixin):
+ path = "/sitemap.xml"
+ name = "sitemap_xml"
+ table_name = "wiki"
+
+ def get(self):
+ urls = []
+
+ for page in self.db.get_all(self.table_name):
+ urls.append({
+ "change_frequency": "weekly",
+ "type": "url",
+ "url": url_for("wiki.page", page=page["slug"], _external=True)
+ })
+
+ return Response(self.render("sitemap.xml", urls=urls), content_type="application/xml")