mirror of
https://github.com/MathiasDPX/blog.git
synced 2025-05-10 07:33:09 +00:00
Compare commits
No commits in common. "865535d3ba47793b7642475d6aea086ac69e4b3b" and "cbe04a86d1c3ebdd60695c8dc208016a62fcd27a" have entirely different histories.
865535d3ba
...
cbe04a86d1
12 changed files with 43 additions and 240 deletions
|
@ -1,2 +1 @@
|
|||
URL="http://127.0.0.1:5000" # URL for rss/atom feed
|
||||
CHANNEL_LIST_TOKEN="xoxb-" # Used in channels.py
|
||||
URL="http://127.0.0.1:5000" # URL for rss/atom feed
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,3 @@
|
|||
channels.json
|
||||
templates/articles/
|
||||
!templates/articles/loremipsum.html
|
||||
|
||||
|
|
46
channels.py
46
channels.py
|
@ -1,46 +0,0 @@
|
|||
"""
|
||||
Create a dict of channel_id: channel_name for scrapbook
|
||||
"""
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
|
||||
load_dotenv()
|
||||
|
||||
ACCESS_TOKEN = os.getenv("CHANNEL_LIST_TOKEN")
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {ACCESS_TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
params = {
|
||||
"types": "public_channel",
|
||||
"limit": 1000
|
||||
}
|
||||
|
||||
channels_map = {}
|
||||
|
||||
while True:
|
||||
response = requests.get("https://slack.com/api/conversations.list", headers=headers, params=params)
|
||||
data = response.json()
|
||||
|
||||
if not data.get("ok"):
|
||||
print("Error:", data.get("error"))
|
||||
break
|
||||
|
||||
for channel in data.get("channels", []):
|
||||
channels_map[channel["id"]] = channel["name"]
|
||||
|
||||
cursor = data.get("response_metadata", {}).get("next_cursor")
|
||||
if cursor:
|
||||
params["cursor"] = cursor
|
||||
else:
|
||||
break
|
||||
|
||||
print(f"{len(channels_map)} public channels found")
|
||||
|
||||
with open("channels.json", "w+") as f:
|
||||
json.dump(channels_map, f)
|
9
main.py
9
main.py
|
@ -3,25 +3,19 @@ import re
|
|||
import os
|
||||
from flask import Flask, render_template, request, send_file, make_response
|
||||
from feedgen.feed import FeedGenerator
|
||||
from flask_moment import Moment
|
||||
from dotenv import load_dotenv
|
||||
from routes.editor import editor_routes
|
||||
from routes.scrapbook import sp_routes
|
||||
from classes import *
|
||||
load_dotenv()
|
||||
|
||||
app = Flask(__name__)
|
||||
moment = Moment(app)
|
||||
|
||||
app.register_blueprint(editor_routes)
|
||||
app.register_blueprint(sp_routes)
|
||||
|
||||
with open("articles.json", "r", encoding="utf-8") as f:
|
||||
articles_data = json.load(f)
|
||||
categories = {} # Category name:str -> [Article]
|
||||
articles = {} # ID:str = Article
|
||||
|
||||
# RSS Feed
|
||||
fg = FeedGenerator()
|
||||
fg.title("Mathias")
|
||||
fg.id("Mathias")
|
||||
|
@ -30,7 +24,6 @@ fg.language("en")
|
|||
fg.link(href=os.getenv("URL"))
|
||||
fg.description("Blog RSS feed")
|
||||
|
||||
# Register articles
|
||||
for article_data in articles_data:
|
||||
category_name = article_data.get("category", "Uncategorized")
|
||||
category_list = categories.get(category_name, [])
|
||||
|
@ -93,4 +86,4 @@ def article(article_id:str):
|
|||
return render_template(f"articles/{article.template}.html")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True)
|
||||
app.run()
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
flask
|
||||
feedgen
|
||||
requests
|
||||
Flask-Moment
|
||||
python-dotenv
|
|
@ -1,61 +0,0 @@
|
|||
"""
|
||||
Display my hackclub scrapbook
|
||||
see https://scrapbook.hackclub.com/about
|
||||
"""
|
||||
|
||||
from flask import Blueprint, render_template
|
||||
from datetime import datetime
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
|
||||
sp_routes = Blueprint('scrapbook', __name__, template_folder='templates')
|
||||
|
||||
lastScrapbookUpdate = 0
|
||||
scrapbookPosts = []
|
||||
|
||||
try:
|
||||
channels_maps = json.load(open('channels.json', 'r', encoding="utf-8"))
|
||||
except:
|
||||
print("channels.json not found")
|
||||
channels_maps = {}
|
||||
|
||||
@sp_routes.app_template_filter('formatDate')
|
||||
def format_date(date_str):
|
||||
date_obj = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||
return date_obj.strftime('%d/%m/%Y @ %Hh%M')
|
||||
|
||||
@sp_routes.app_template_filter('formatContent')
|
||||
def convert_slack_references(text):
|
||||
# Convert channel references
|
||||
channel_pattern = r'<#(C[A-Z0-9]+)\|>'
|
||||
|
||||
def channel_replacement(match):
|
||||
channel_id = match.group(1)
|
||||
channel_name = channels_maps.get(channel_id, channel_id)
|
||||
return f'<a target="_blank" href="htps://hackclub.slack.com/archives/{channel_id}">#{channel_name}</a>'
|
||||
|
||||
url_pattern = r'<(http(?:|s):\/\/[a-zA-Z0-9\.\/_-]+)>'
|
||||
def url_replacement(match):
|
||||
url = match.group(1)
|
||||
return f'<a target="_blank" href="{url}">{url}</a>'
|
||||
|
||||
result = re.sub(url_pattern, url_replacement, text)
|
||||
result = re.sub(channel_pattern, channel_replacement, result)
|
||||
|
||||
return result
|
||||
|
||||
@sp_routes.route("/scrapbook")
|
||||
def scrapbook():
|
||||
"""Page linking to my scrapbook articles"""
|
||||
global lastScrapbookUpdate, scrapbookPosts
|
||||
if lastScrapbookUpdate+3600 < time.time():
|
||||
try:
|
||||
r = requests.get("https://scrapbook.hackclub.com/api/users/mathias")
|
||||
data = r.json()
|
||||
scrapbookPosts = data["posts"]
|
||||
lastScrapbookUpdate = time.time()
|
||||
except: pass
|
||||
|
||||
return render_template("scrapbook.html", posts=scrapbookPosts)
|
|
@ -1,5 +1,6 @@
|
|||
@import url("typography.css");
|
||||
|
||||
|
||||
/* Reset */
|
||||
* {
|
||||
margin: 0;
|
||||
|
@ -51,13 +52,6 @@ header h1 {
|
|||
background-color: #03396C;
|
||||
padding: 5px 3em 5px 0;
|
||||
text-align: right;
|
||||
min-width: 100%;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#navbar a + a::before {
|
||||
content: "// ";
|
||||
padding: 0 0.1em;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
|
@ -66,71 +60,25 @@ header h1 {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
#main details div {
|
||||
padding-left: 1em;
|
||||
line-height: 115%;
|
||||
}
|
||||
|
||||
#main details:open {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
#main details summary {
|
||||
cursor: pointer;
|
||||
padding-bottom: 0.25em;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
#footer {
|
||||
margin-top: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#footer-title {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
background-color: #03396C;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
#footer p {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #005B96;
|
||||
}
|
||||
|
||||
/* Media Queries */
|
||||
@media (max-width: 768px) {
|
||||
html, body {
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
#wrap {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
footer p {
|
||||
footer p{
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,12 +19,11 @@ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
|
|||
}
|
||||
|
||||
/* Heading Hover Effects */
|
||||
h1.chapter a:hover::before,
|
||||
h2.chapter a:hover::before,
|
||||
h3.chapter a:hover::before,
|
||||
h4.chapter a:hover::before,
|
||||
h5.chapter a:hover::before,
|
||||
h6.chapter a:hover::before {
|
||||
h2 a:hover::before,
|
||||
h3 a:hover::before,
|
||||
h4 a:hover::before,
|
||||
h5 a:hover::before,
|
||||
h6 a:hover::before {
|
||||
content: "#";
|
||||
position: absolute;
|
||||
left: -0.75em;
|
||||
|
@ -37,23 +36,26 @@ h6.chapter a:hover::before {
|
|||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
summary > small {
|
||||
font-size: 0.5em;
|
||||
}
|
||||
|
||||
time {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
#main details summary {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: 500;
|
||||
hr {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #005B96;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
#navbar {
|
||||
min-width: 100%;
|
||||
color: #ffffff;
|
||||
text-transform: uppercase;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
@ -62,7 +64,25 @@ time {
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
#navbar a + a::before {
|
||||
content: "// ";
|
||||
padding: 0 0.1em;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
#footer-title {
|
||||
padding: 5px 0;
|
||||
min-width: 100%;
|
||||
color: #ffffff;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
||||
#footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#footer p {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
line-height: 1.25;
|
||||
}
|
|
@ -5,12 +5,11 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 id="lorem-ipsum" class="chapter"><a href="#lorem-ipsum">Lorem Ipsum</a></h1>
|
||||
<h1>Lorem Ipsum</h1>
|
||||
<h4><time datetime="2025-01-23T18:41:03Z">January 23th 2025</time></h4>
|
||||
<hr>
|
||||
|
||||
<p><a href="https://www.lipsum.com" target="_blank">Lorem ipsum</a> dolor sit amet, consectetur adipiscing elit. Nulla mattis, est et laoreet efficitur, nisi leo volutpat nibh, et rutrum erat urna vel nisl. Aenean sodales lorem quis dolor sodales, at convallis purus convallis. Nam interdum tincidunt nunc, sed molestie lectus. Integer a rhoncus enim, eu cursus lacus.</p>
|
||||
|
||||
<p>Donec dignissim consequat augue mattis ornare. Vivamus feugiat odio in sagittis consectetur. Proin nec suscipit dolor, ut consectetur nisi. Aliquam ut ex dapibus, volutpat justo sit amet, dignissim tellus. Curabitur placerat tempor neque congue sollicitudin. Praesent elementum in ligula id semper.</p>
|
||||
|
||||
{% endblock %}
|
|
@ -6,7 +6,6 @@
|
|||
<link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
|
||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}" />
|
||||
{% block head %}{% endblock %}
|
||||
{{ moment.include_moment() }}
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
|
|
|
@ -13,15 +13,18 @@ li {
|
|||
h3 {
|
||||
padding-bottom: 0.1em;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
👋, Welcome here, you can see it's a mess.
|
||||
My scrapbook posts are available at <a href="/scrapbook">/scrapbook</a>
|
||||
👋, Welcome here, you can see it's a mess
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
{% for category_name, articles in categories %}
|
||||
<h3>{{ category_name }}</h3>
|
||||
<ul>
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Scrapbook</title>
|
||||
|
||||
<style>
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h1>Scrapbook posts</h1>
|
||||
<i><h6>
|
||||
<a href="https://scrapbook.hackclub.com/mathias" style="color: #005B96;">See on scrapbook</a>
|
||||
</h6></i>
|
||||
<br>
|
||||
|
||||
{% for post in posts%}
|
||||
{% if loop.first %}
|
||||
<details id="{{ post.id }}" open>
|
||||
{% else%}
|
||||
<details id="{{ post.id }}">
|
||||
{% endif %}
|
||||
<summary>{{ moment(post.postedAt).format('DD/MM/YYYY @ HH[h]mm') }}</summary>
|
||||
<div>{{ post.text | formatContent |safe }}</div>
|
||||
{% for attachment in post.attachments %}
|
||||
<br>
|
||||
{% if attachment.endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp')) %}
|
||||
<img src="{{ attachment }}" alt="Image">
|
||||
{% elif attachment.endswith(('.mp4', '.webm', '.ogg', '.mov', '.avi')) %}
|
||||
<video controls>
|
||||
<source src="{{ attachment }}" type="video/{{ attachment.split('.')[-1] }}">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
{% else %}
|
||||
<p>URL does not appear to be an image or video: {{ attachment }}</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</details>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue