diff options
Diffstat (limited to 'pysite')
-rw-r--r-- | pysite/base_route.py | 5 | ||||
-rw-r--r-- | pysite/tables.py | 8 | ||||
-rw-r--r-- | pysite/views/api/bot/bigbrother.py | 118 | ||||
-rw-r--r-- | pysite/views/api/bot/off_topic_names.py | 2 | ||||
-rw-r--r-- | pysite/views/api/robots_txt.py | 15 | ||||
-rw-r--r-- | pysite/views/api/sitemap_xml.py | 11 | ||||
-rw-r--r-- | pysite/views/main/robots_txt.py | 15 | ||||
-rw-r--r-- | pysite/views/main/sitemap_xml.py | 69 | ||||
-rw-r--r-- | pysite/views/staff/robots_txt.py | 15 | ||||
-rw-r--r-- | pysite/views/staff/sitemap_xml.py | 11 | ||||
-rw-r--r-- | pysite/views/wiki/edit.py | 24 | ||||
-rw-r--r-- | pysite/views/wiki/robots_txt.py | 15 | ||||
-rw-r--r-- | pysite/views/wiki/sitemap_xml.py | 22 |
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") |