Compare commits

..

15 commits

12 changed files with 240 additions and 43 deletions

View file

@ -1 +1,2 @@
URL="http://127.0.0.1:5000" # URL for rss/atom feed URL="http://127.0.0.1:5000" # URL for rss/atom feed
CHANNEL_LIST_TOKEN="xoxb-" # Used in channels.py

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
channels.json
templates/articles/ templates/articles/
!templates/articles/loremipsum.html !templates/articles/loremipsum.html

46
channels.py Normal file
View file

@ -0,0 +1,46 @@
"""
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)

View file

@ -3,19 +3,25 @@ import re
import os import os
from flask import Flask, render_template, request, send_file, make_response from flask import Flask, render_template, request, send_file, make_response
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
from flask_moment import Moment
from dotenv import load_dotenv from dotenv import load_dotenv
from routes.editor import editor_routes from routes.editor import editor_routes
from routes.scrapbook import sp_routes
from classes import * from classes import *
load_dotenv() load_dotenv()
app = Flask(__name__) app = Flask(__name__)
moment = Moment(app)
app.register_blueprint(editor_routes) app.register_blueprint(editor_routes)
app.register_blueprint(sp_routes)
with open("articles.json", "r", encoding="utf-8") as f: with open("articles.json", "r", encoding="utf-8") as f:
articles_data = json.load(f) articles_data = json.load(f)
categories = {} # Category name:str -> [Article] categories = {} # Category name:str -> [Article]
articles = {} # ID:str = Article articles = {} # ID:str = Article
# RSS Feed
fg = FeedGenerator() fg = FeedGenerator()
fg.title("Mathias") fg.title("Mathias")
fg.id("Mathias") fg.id("Mathias")
@ -24,6 +30,7 @@ fg.language("en")
fg.link(href=os.getenv("URL")) fg.link(href=os.getenv("URL"))
fg.description("Blog RSS feed") fg.description("Blog RSS feed")
# Register articles
for article_data in articles_data: for article_data in articles_data:
category_name = article_data.get("category", "Uncategorized") category_name = article_data.get("category", "Uncategorized")
category_list = categories.get(category_name, []) category_list = categories.get(category_name, [])
@ -86,4 +93,4 @@ def article(article_id:str):
return render_template(f"articles/{article.template}.html") return render_template(f"articles/{article.template}.html")
if __name__ == "__main__": if __name__ == "__main__":
app.run() app.run(debug=True)

View file

@ -1,3 +1,5 @@
flask flask
feedgen feedgen
requests
Flask-Moment
python-dotenv python-dotenv

61
routes/scrapbook.py Normal file
View file

@ -0,0 +1,61 @@
"""
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)

View file

@ -1,6 +1,5 @@
@import url("typography.css"); @import url("typography.css");
/* Reset */ /* Reset */
* { * {
margin: 0; margin: 0;
@ -52,6 +51,13 @@ header h1 {
background-color: #03396C; background-color: #03396C;
padding: 5px 3em 5px 0; padding: 5px 3em 5px 0;
text-align: right; text-align: right;
min-width: 100%;
color: #ffffff;
}
#navbar a + a::before {
content: "// ";
padding: 0 0.1em;
} }
/* Main Content */ /* Main Content */
@ -60,25 +66,71 @@ header h1 {
flex: 1; 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 */
#footer { #footer {
margin-top: auto; margin-top: auto;
text-align: center;
} }
#footer-title { #footer-title {
width: 100%; width: 100%;
min-width: 100%;
background-color: #03396C; background-color: #03396C;
color: #ffffff;
text-align: center; text-align: center;
padding: 0.5em 0; 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) { @media (max-width: 768px) {
html, body { html, body {
min-height: 100dvh; min-height: 100dvh;
} }
#wrap { #wrap {
width: 100%; width: 100%;
max-width: none; max-width: none;
} }
footer p{
footer p {
margin-bottom: 0.25em; margin-bottom: 0.25em;
} }

View file

@ -19,11 +19,12 @@ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
} }
/* Heading Hover Effects */ /* Heading Hover Effects */
h2 a:hover::before, h1.chapter a:hover::before,
h3 a:hover::before, h2.chapter a:hover::before,
h4 a:hover::before, h3.chapter a:hover::before,
h5 a:hover::before, h4.chapter a:hover::before,
h6 a:hover::before { h5.chapter a:hover::before,
h6.chapter a:hover::before {
content: "#"; content: "#";
position: absolute; position: absolute;
left: -0.75em; left: -0.75em;
@ -36,26 +37,23 @@ h6 a:hover::before {
margin-bottom: 0.75em; margin-bottom: 0.75em;
} }
summary > small {
font-size: 0.5em;
}
time { time {
font-family: "Montserrat", sans-serif; font-family: "Montserrat", sans-serif;
font-weight: bold; font-weight: bold;
font-size: 75%; font-size: 75%;
} }
hr { #main details summary {
margin-bottom: 1em; font-family: "Montserrat", sans-serif;
} font-weight: 500;
/* Links */
a {
text-decoration: none;
color: #005B96;
} }
/* Navigation */ /* Navigation */
#navbar { #navbar {
min-width: 100%;
color: #ffffff;
text-transform: uppercase; text-transform: uppercase;
font-family: "Montserrat", sans-serif; font-family: "Montserrat", sans-serif;
} }
@ -64,25 +62,7 @@ a {
color: inherit; color: inherit;
} }
#navbar a + a::before {
content: "// ";
padding: 0 0.1em;
}
/* Footer */ /* Footer */
#footer-title { #footer-title {
padding: 5px 0;
min-width: 100%;
color: #ffffff;
font-family: "Montserrat", sans-serif; font-family: "Montserrat", sans-serif;
} }
#footer {
text-align: center;
}
#footer p {
padding-top: 0.5em;
padding-bottom: 0.5em;
line-height: 1.25;
}

View file

@ -5,11 +5,12 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>Lorem Ipsum</h1> <h1 id="lorem-ipsum" class="chapter"><a href="#lorem-ipsum">Lorem Ipsum</a></h1>
<h4><time datetime="2025-01-23T18:41:03Z">January 23th 2025</time></h4> <h4><time datetime="2025-01-23T18:41:03Z">January 23th 2025</time></h4>
<hr> <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><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> <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 %} {% endblock %}

View file

@ -6,6 +6,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}" /> <link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}" />
{% block head %}{% endblock %} {% block head %}{% endblock %}
{{ moment.include_moment() }}
</head> </head>
<body> <body>
<div id="wrap"> <div id="wrap">

View file

@ -13,18 +13,15 @@ li {
h3 { h3 {
padding-bottom: 0.1em; padding-bottom: 0.1em;
} }
a {
color: inherit;
}
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
👋, Welcome here, you can see it's a mess 👋, Welcome here, you can see it's a mess.
My scrapbook posts are available at <a href="/scrapbook">/scrapbook</a>
<br /> <br />
<br /> <br />
{% for category_name, articles in categories %} {% for category_name, articles in categories %}
<h3>{{ category_name }}</h3> <h3>{{ category_name }}</h3>
<ul> <ul>

48
templates/scrapbook.html Normal file
View file

@ -0,0 +1,48 @@
{% 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 %}