Compare commits
3 Commits
master
...
new-commen
| Author | SHA1 | Date | |
|---|---|---|---|
| 363497d354 | |||
| b97c9ea3ee | |||
| 82def98e7e |
3
Jenkinsfile
vendored
3
Jenkinsfile
vendored
@@ -6,7 +6,6 @@ pipeline {
|
||||
DISCORD = credentials('jc_discord')
|
||||
DISCORD_ERR_STAGING = credentials('jc_discord_err_staging')
|
||||
DISCORD_ERR_PROD = credentials('jc_discord_err_prod')
|
||||
NEWRELIC_KEY = credentials('newrelic')
|
||||
}
|
||||
|
||||
stages{
|
||||
@@ -33,7 +32,7 @@ pipeline {
|
||||
git branch: 'master',
|
||||
credentialsId: 'Git',
|
||||
url: 'git@git.jakecharman.co.uk:jake/jc-ng.git'
|
||||
sh "sed -i s/dummy/$NEWRELIC_KEY/ src/newrelic.ini"
|
||||
|
||||
sh "./build.sh git.jakecharman.co.uk/jake/jakecharman.co.uk $BUILD_NUMBER"
|
||||
sh "./build.sh europe-west2-docker.pkg.dev/jakecharman/web/jakecharman.co.uk $BUILD_NUMBER"
|
||||
}
|
||||
|
||||
@@ -439,8 +439,6 @@ Alias "/static" "/var/www/jc/static/"
|
||||
</Directory>
|
||||
|
||||
Alias "/robots.txt" "/var/www/jc/static/robots.txt"
|
||||
Alias "/.well-known/security.txt" "/var/www/jc/static/security.txt"
|
||||
|
||||
|
||||
<Files jc.wsgi>
|
||||
Require all granted
|
||||
|
||||
@@ -11,14 +11,13 @@ import json
|
||||
from io import BytesIO
|
||||
from requests import post
|
||||
from flask import Flask, render_template, Response, url_for, request, send_from_directory, make_response
|
||||
from newrelic import agent
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from .content import ContentArea
|
||||
from .contact import ContactForm
|
||||
from .storage import LocalStorage
|
||||
from .links import Links
|
||||
from .comments import *
|
||||
|
||||
agent.initialize('/var/www/jc/newrelic.ini')
|
||||
app = Flask(__name__)
|
||||
|
||||
md_path = path.join(path.realpath(path.dirname(__file__)), path.normpath('../projects/'))
|
||||
@@ -31,6 +30,7 @@ projects = ContentArea(
|
||||
app.register_blueprint(projects, url_prefix='/projects')
|
||||
app.register_blueprint(ContactForm('contact', __name__), url_prefix='/contact')
|
||||
app.register_blueprint(Links(path.join(md_path, 'links.json'), 'links', __name__), url_prefix='/links')
|
||||
app.register_blueprint(Approval(path.join(projects.md_directory.uri, 'comments.db'), 'comments', __name__), url_prefix='/comments')
|
||||
|
||||
class DiscordLogger(logging.Handler):
|
||||
''' Simple logging handler to send a message to Discord '''
|
||||
@@ -142,7 +142,7 @@ def sitemap():
|
||||
url = ET.SubElement(root, 'url')
|
||||
ET.SubElement(url, 'loc').text = base_url + route
|
||||
ET.SubElement(url, 'lastmod').text = date
|
||||
for article in projects.get_all_posts():
|
||||
for article in projects.get_live_posts():
|
||||
if 'link' in article.metadata:
|
||||
continue
|
||||
url = ET.SubElement(root, 'url')
|
||||
|
||||
60
src/jakecharman/comments.py
Normal file
60
src/jakecharman/comments.py
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sqlite3
|
||||
from flask import Blueprint, Response, request
|
||||
from uuid import uuid4
|
||||
from requests import post
|
||||
from os import environ
|
||||
|
||||
|
||||
class PostComments():
|
||||
def __init__(self, post_id: str, db_path: str):
|
||||
self.__db_path = db_path
|
||||
self.__post_id = post_id
|
||||
self._webhook = environ['DISCORD_WEBHOOK']
|
||||
with sqlite3.connect(db_path) as db:
|
||||
cursor = db.cursor()
|
||||
cursor.execute("CREATE TABLE IF NOT EXISTS comments (name TEXT, comment TEXT, date INT, post_id TEXT, approved BOOL, key TEXT)")
|
||||
self.comments = cursor.execute(
|
||||
"SELECT name, comment, date FROM comments WHERE approved = 1 AND post_id = ? ORDER BY date DESC",
|
||||
(post_id,)
|
||||
).fetchall()
|
||||
|
||||
def send_to_discord(self, name: str, comment: str, comment_id: int, key: str):
|
||||
''' Send the message '''
|
||||
message_to_send = f'New comment from {name}\n\n{comment}'
|
||||
if len(message_to_send) > 2000:
|
||||
chars_to_lose = len(message_to_send) - 1900
|
||||
message_to_send = message_to_send[-chars_to_lose:]
|
||||
message_to_send += f'\n\n[Approve](https://jakecharman.co.uk/comments/approve/{comment_id}?key={key})'
|
||||
post(self._webhook, data={'content': message_to_send}, timeout=30)
|
||||
|
||||
def make_comment(self, name: str, comment: str):
|
||||
with sqlite3.connect(self.__db_path) as db:
|
||||
key = str(uuid4())
|
||||
cursor = db.cursor()
|
||||
cursor.execute(
|
||||
"INSERT INTO comments (name, comment, date, post_id, approved, key) VALUES (?, ?, datetime('now'), ?, 0, ?)",
|
||||
(name, comment, self.__post_id, key)
|
||||
)
|
||||
db.commit()
|
||||
self.send_to_discord(name, comment, cursor.lastrowid, key)
|
||||
return cursor.lastrowid
|
||||
|
||||
class Approval(Blueprint):
|
||||
def __init__(self, db_path: str, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__db_path = db_path
|
||||
self.add_url_rule('/approve/<comment_id>', view_func=self.approve)
|
||||
|
||||
def approve(self, comment_id: str):
|
||||
with sqlite3.connect(self.__db_path) as db:
|
||||
cursor = db.cursor()
|
||||
key = cursor.execute("SELECT key FROM comments WHERE rowid = ?", (comment_id,)).fetchone()[0]
|
||||
if request.args.get('key') == key:
|
||||
with sqlite3.connect(self.__db_path) as db:
|
||||
cursor = db.cursor()
|
||||
cursor.execute("UPDATE comments SET approved = 1 WHERE rowid = ?", (comment_id,))
|
||||
db.commit()
|
||||
return Response(status=200)
|
||||
return Response(status=403)
|
||||
@@ -6,8 +6,9 @@ from datetime import datetime
|
||||
import frontmatter
|
||||
from markdown import markdown
|
||||
from bs4 import BeautifulSoup
|
||||
from flask import render_template, Response, Blueprint
|
||||
from flask import render_template, Response, Blueprint, request, redirect
|
||||
from .storage import LocalStorage
|
||||
from .comments import PostComments, Approval
|
||||
|
||||
class ContentArea(Blueprint):
|
||||
def __init__(self, directory: LocalStorage, *args, **kwargs):
|
||||
@@ -24,7 +25,7 @@ class ContentArea(Blueprint):
|
||||
self.add_url_rule('/', view_func=self.projects)
|
||||
self.add_url_rule('/category/<category_id>/', view_func=self.category)
|
||||
self.add_url_rule('/<article_id>', view_func=self.article)
|
||||
#self.add_url_rule('/image/<image_name>', view_func=self.image)
|
||||
self.add_url_rule('/<article_id>/comment', view_func=self.comment, methods=['POST'])
|
||||
|
||||
def processor(self) -> dict:
|
||||
''' Jninja processors '''
|
||||
@@ -140,6 +141,16 @@ class ContentArea(Blueprint):
|
||||
return Response(status=500)
|
||||
|
||||
the_article = articles[0]
|
||||
|
||||
comments = PostComments(the_article.metadata['id'], path.join(self.md_directory.uri, 'comments.db')).comments
|
||||
return render_template('article.html', post=markdown(the_article.content),
|
||||
metadata=the_article.metadata,
|
||||
page_title=f'{the_article.metadata["title"]} - ')
|
||||
comments = comments,
|
||||
page_title=f'{the_article.metadata["title"]} - ')
|
||||
|
||||
def comment(self, article_id: str):
|
||||
PostComments(article_id, path.join(self.md_directory.uri, 'comments.db')).make_comment(
|
||||
request.form['name'],
|
||||
request.form['comment']
|
||||
)
|
||||
return redirect(f'/projects/{article_id}?comment=true#comments')
|
||||
@@ -22,4 +22,23 @@
|
||||
{{post|safe}}
|
||||
</section>
|
||||
</main>
|
||||
<section id="comments">
|
||||
<h2>{{comments | length}} Comment{% if comments | length != 1 %}s{% endif %}</h2>
|
||||
{% for comment in comments %}
|
||||
<div class="comment">
|
||||
<strong>{{ comment[0] }} - {{ comment[2] | human_date }}</strong>
|
||||
<p>{{ comment[1] }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<h3>Leave a comment</h3>
|
||||
{% if request.args.get('comment') is none %}
|
||||
<form action="./{{metadata.id}}/comment" method="post">
|
||||
<input type="text", name="name" placeholder="Name" required>
|
||||
<textarea name="comment" placeholder="Comment" required></textarea>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
{% else %}
|
||||
<p>Thank you! Your comment will appear once it is approved</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
255
src/newrelic.ini
255
src/newrelic.ini
@@ -1,255 +0,0 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#
|
||||
# This file configures the New Relic Python Agent.
|
||||
#
|
||||
# The path to the configuration file should be supplied to the function
|
||||
# newrelic.agent.initialize() when the agent is being initialized.
|
||||
#
|
||||
# The configuration file follows a structure similar to what you would
|
||||
# find for Microsoft Windows INI files. For further information on the
|
||||
# configuration file format see the Python ConfigParser documentation at:
|
||||
#
|
||||
# http://docs.python.org/library/configparser.html
|
||||
#
|
||||
# For further discussion on the behaviour of the Python agent that can
|
||||
# be configured via this configuration file see:
|
||||
#
|
||||
# https://docs.newrelic.com/docs/apm/agents/python-agent/configuration/python-agent-configuration/
|
||||
#
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Here are the settings that are common to all environments.
|
||||
|
||||
[newrelic]
|
||||
|
||||
# You must specify the license key associated with your New
|
||||
# Relic account. This may also be set using the NEW_RELIC_LICENSE_KEY
|
||||
# environment variable. This key binds the Python Agent's data to
|
||||
# your account in the New Relic service. For more information on
|
||||
# storing and generating license keys, see
|
||||
# https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/#ingest-license-key
|
||||
license_key = dummy
|
||||
|
||||
# The application name. Set this to be the name of your
|
||||
# application as you would like it to show up in New Relic UI.
|
||||
# You may also set this using the NEW_RELIC_APP_NAME environment variable.
|
||||
# The UI will then auto-map instances of your application into a
|
||||
# entry on your home dashboard page. You can also specify multiple
|
||||
# app names to group your aggregated data. For further details,
|
||||
# please see:
|
||||
# https://docs.newrelic.com/docs/apm/agents/manage-apm-agents/app-naming/use-multiple-names-app/
|
||||
app_name = jakecharman.co.uk
|
||||
|
||||
# When "true", the agent collects performance data about your
|
||||
# application and reports this data to the New Relic UI at
|
||||
# newrelic.com. This global switch is normally overridden for
|
||||
# each environment below. It may also be set using the
|
||||
# NEW_RELIC_MONITOR_MODE environment variable.
|
||||
monitor_mode = true
|
||||
|
||||
# Sets the name of a file to log agent messages to. Whatever you
|
||||
# set this to, you must ensure that the permissions for the
|
||||
# containing directory and the file itself are correct, and
|
||||
# that the user that your web application runs as can write out
|
||||
# to the file. If not able to out a log file, it is also
|
||||
# possible to say "stderr" and output to standard error output.
|
||||
# This would normally result in output appearing in your web
|
||||
# server log. It can also be set using the NEW_RELIC_LOG
|
||||
# environment variable.
|
||||
log_file = stdout
|
||||
|
||||
# Sets the level of detail of messages sent to the log file, if
|
||||
# a log file location has been provided. Possible values, in
|
||||
# increasing order of detail, are: "critical", "error", "warning",
|
||||
# "info" and "debug". When reporting any agent issues to New
|
||||
# Relic technical support, the most useful setting for the
|
||||
# support engineers is "debug". However, this can generate a lot
|
||||
# of information very quickly, so it is best not to keep the
|
||||
# agent at this level for longer than it takes to reproduce the
|
||||
# problem you are experiencing. This may also be set using the
|
||||
# NEW_RELIC_LOG_LEVEL environment variable.
|
||||
log_level = info
|
||||
|
||||
# High Security Mode enforces certain security settings, and prevents
|
||||
# them from being overridden, so that no sensitive data is sent to New
|
||||
# Relic. Enabling High Security Mode means that request parameters are
|
||||
# not collected and SQL can not be sent to New Relic in its raw form.
|
||||
# To activate High Security Mode, it must be set to 'true' in this
|
||||
# local .ini configuration file AND be set to 'true' in the
|
||||
# server-side configuration in the New Relic user interface. It can
|
||||
# also be set using the NEW_RELIC_HIGH_SECURITY environment variable.
|
||||
# For details, see
|
||||
# https://docs.newrelic.com/docs/subscriptions/high-security
|
||||
high_security = false
|
||||
|
||||
# The Python Agent will attempt to connect directly to the New
|
||||
# Relic service. If there is an intermediate firewall between
|
||||
# your host and the New Relic service that requires you to use a
|
||||
# HTTP proxy, then you should set both the "proxy_host" and
|
||||
# "proxy_port" settings to the required values for the HTTP
|
||||
# proxy. The "proxy_user" and "proxy_pass" settings should
|
||||
# additionally be set if proxy authentication is implemented by
|
||||
# the HTTP proxy. The "proxy_scheme" setting dictates what
|
||||
# protocol scheme is used in talking to the HTTP proxy. This
|
||||
# would normally always be set as "http" which will result in the
|
||||
# agent then using a SSL tunnel through the HTTP proxy for end to
|
||||
# end encryption.
|
||||
# See https://docs.newrelic.com/docs/apm/agents/python-agent/configuration/python-agent-configuration/#proxy
|
||||
# for information on proxy configuration via environment variables.
|
||||
# proxy_scheme = http
|
||||
# proxy_host = hostname
|
||||
# proxy_port = 8080
|
||||
# proxy_user =
|
||||
# proxy_pass =
|
||||
|
||||
# Capturing request parameters is off by default. To enable the
|
||||
# capturing of request parameters, first ensure that the setting
|
||||
# "attributes.enabled" is set to "true" (the default value), and
|
||||
# then add "request.parameters.*" to the "attributes.include"
|
||||
# setting. For details about attributes configuration, please
|
||||
# consult the documentation.
|
||||
# attributes.include = request.parameters.*
|
||||
|
||||
# The transaction tracer captures deep information about slow
|
||||
# transactions and sends this to the UI on a periodic basis. The
|
||||
# transaction tracer is enabled by default. Set this to "false"
|
||||
# to turn it off.
|
||||
transaction_tracer.enabled = true
|
||||
|
||||
# Threshold in seconds for when to collect a transaction trace.
|
||||
# When the response time of a controller action exceeds this
|
||||
# threshold, a transaction trace will be recorded and sent to
|
||||
# the UI. Valid values are any positive float value, or (default)
|
||||
# "apdex_f", which will use the threshold for a dissatisfying
|
||||
# Apdex controller action - four times the Apdex T value.
|
||||
transaction_tracer.transaction_threshold = apdex_f
|
||||
|
||||
# When the transaction tracer is on, SQL statements can
|
||||
# optionally be recorded. The recorder has three modes, "off"
|
||||
# which sends no SQL, "raw" which sends the SQL statement in its
|
||||
# original form, and "obfuscated", which strips out numeric and
|
||||
# string literals.
|
||||
transaction_tracer.record_sql = obfuscated
|
||||
|
||||
# Threshold in seconds for when to collect stack trace for a SQL
|
||||
# call. In other words, when SQL statements exceed this
|
||||
# threshold, then capture and send to the UI the current stack
|
||||
# trace. This is helpful for pinpointing where long SQL calls
|
||||
# originate from in an application.
|
||||
transaction_tracer.stack_trace_threshold = 0.5
|
||||
|
||||
# Determines whether the agent will capture query plans for slow
|
||||
# SQL queries. Only supported in MySQL and PostgreSQL. Set this
|
||||
# to "false" to turn it off.
|
||||
transaction_tracer.explain_enabled = true
|
||||
|
||||
# Threshold for query execution time below which query plans
|
||||
# will not not be captured. Relevant only when "explain_enabled"
|
||||
# is true.
|
||||
transaction_tracer.explain_threshold = 0.5
|
||||
|
||||
# Space separated list of function or method names in form
|
||||
# 'module:function' or 'module:class.function' for which
|
||||
# additional function timing instrumentation will be added.
|
||||
transaction_tracer.function_trace =
|
||||
|
||||
# The error collector captures information about uncaught
|
||||
# exceptions or logged exceptions and sends them to UI for
|
||||
# viewing. The error collector is enabled by default. Set this
|
||||
# to "false" to turn it off. For more details on errors, see
|
||||
# https://docs.newrelic.com/docs/apm/agents/manage-apm-agents/agent-data/manage-errors-apm-collect-ignore-or-mark-expected/
|
||||
error_collector.enabled = true
|
||||
|
||||
# To stop specific errors from reporting to the UI, set this to
|
||||
# a space separated list of the Python exception type names to
|
||||
# ignore. The exception name should be of the form 'module:class'.
|
||||
error_collector.ignore_classes =
|
||||
|
||||
# Expected errors are reported to the UI but will not affect the
|
||||
# Apdex or error rate. To mark specific errors as expected, set this
|
||||
# to a space separated list of the Python exception type names to
|
||||
# expected. The exception name should be of the form 'module:class'.
|
||||
error_collector.expected_classes =
|
||||
|
||||
# Browser monitoring is the Real User Monitoring feature of the UI.
|
||||
# For those Python web frameworks that are supported, this
|
||||
# setting enables the auto-insertion of the browser monitoring
|
||||
# JavaScript fragments.
|
||||
browser_monitoring.auto_instrument = true
|
||||
|
||||
# A thread profiling session can be scheduled via the UI when
|
||||
# this option is enabled. The thread profiler will periodically
|
||||
# capture a snapshot of the call stack for each active thread in
|
||||
# the application to construct a statistically representative
|
||||
# call tree. For more details on the thread profiler tool, see
|
||||
# https://docs.newrelic.com/docs/apm/apm-ui-pages/events/thread-profiler-tool/
|
||||
thread_profiler.enabled = true
|
||||
|
||||
# Your application deployments can be recorded through the
|
||||
# New Relic REST API. To use this feature provide your API key
|
||||
# below then use the `newrelic-admin record-deploy` command.
|
||||
# This can also be set using the NEW_RELIC_API_KEY
|
||||
# environment variable.
|
||||
# api_key =
|
||||
|
||||
# Distributed tracing lets you see the path that a request takes
|
||||
# through your distributed system. For more information, please
|
||||
# consult our distributed tracing planning guide.
|
||||
# https://docs.newrelic.com/docs/transition-guide-distributed-tracing
|
||||
distributed_tracing.enabled = true
|
||||
|
||||
# This setting enables log decoration, the forwarding of log events,
|
||||
# and the collection of logging metrics if these sub-feature
|
||||
# configurations are also enabled. If this setting is false, no
|
||||
# logging instrumentation features are enabled. This can also be
|
||||
# set using the NEW_RELIC_APPLICATION_LOGGING_ENABLED environment
|
||||
# variable.
|
||||
# application_logging.enabled = true
|
||||
|
||||
# If true, the agent captures log records emitted by your application
|
||||
# and forwards them to New Relic. `application_logging.enabled` must
|
||||
# also be true for this setting to take effect. You can also set
|
||||
# this using the NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED
|
||||
# environment variable.
|
||||
# application_logging.forwarding.enabled = true
|
||||
|
||||
# If true, the agent decorates logs with metadata to link to entities,
|
||||
# hosts, traces, and spans. `application_logging.enabled` must also
|
||||
# be true for this setting to take effect. This can also be set
|
||||
# using the NEW_RELIC_APPLICATION_LOGGING_LOCAL_DECORATING_ENABLED
|
||||
# environment variable.
|
||||
# application_logging.local_decorating.enabled = true
|
||||
|
||||
# If true, the agent captures metrics related to the log lines
|
||||
# being sent up by your application. This can also be set
|
||||
# using the NEW_RELIC_APPLICATION_LOGGING_METRICS_ENABLED
|
||||
# environment variable.
|
||||
# application_logging.metrics.enabled = true
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#
|
||||
# The application environments. These are specific settings which
|
||||
# override the common environment settings. The settings related to a
|
||||
# specific environment will be used when the environment argument to the
|
||||
# newrelic.agent.initialize() function has been defined to be either
|
||||
# "development", "test", "staging" or "production".
|
||||
#
|
||||
|
||||
[newrelic:development]
|
||||
monitor_mode = false
|
||||
|
||||
[newrelic:test]
|
||||
monitor_mode = false
|
||||
|
||||
[newrelic:staging]
|
||||
app_name = jakecharman.co.uk (Staging)
|
||||
monitor_mode = true
|
||||
|
||||
[newrelic:production]
|
||||
monitor_mode = true
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -7,4 +7,3 @@ python-frontmatter>=1.1.0
|
||||
requests>=2.32.3
|
||||
pillow>=11.0.0
|
||||
mod_wsgi>=5.0.2
|
||||
newrelic
|
||||
@@ -1,3 +0,0 @@
|
||||
Contact: mailto:security@jakecharman.co.uk
|
||||
Expires: 2036-01-01T00:00:00.000Z
|
||||
Preferred-Languages: en
|
||||
@@ -328,4 +328,15 @@ pre{
|
||||
padding: 5px;
|
||||
height: 31vw;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#comments {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.comment {
|
||||
background-color: #4c4c4c;
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
Reference in New Issue
Block a user