diff --git a/chanbans/__main__.py b/chanbans/__main__.py
index 33fc28a..9f9c778 100644
--- a/chanbans/__main__.py
+++ b/chanbans/__main__.py
@@ -3,11 +3,16 @@ import argparse
import logging
from .pull import pull
+from .http import run_app
def parse_args():
parser = argparse.ArgumentParser(description="Run 4chan bans archiver")
- parser.add_argument('--log-level', choices=['debug', 'info', 'warning', 'error', 'critical'], default='info')
+ parser.add_argument(
+ "--log-level",
+ choices=["debug", "info", "warning", "error", "critical"],
+ default="info",
+ )
subparsers = parser.add_subparsers(title="Commands", dest="command")
@@ -36,7 +41,7 @@ def parse_args():
return args
-async def main():
+def main():
args = parse_args()
try:
level = getattr(logging, args.log_level.upper())
@@ -44,13 +49,18 @@ async def main():
print(f"ERROR: no such logging level {args.log_level}. Exiting")
raise SystemExit()
- logging.basicConfig(level=level, format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
+ logging.basicConfig(
+ level=level, format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s"
+ )
match args.command:
case "pull":
- await pull()
+ asyncio.run(pull())
case "serve":
- print("TODO: HTTP server")
+ run_app()
case command:
- assert False, f"unknown command {command} that was not caught in add_subcommand"
+ assert (
+ False
+ ), f"unknown command {command} that was not caught in add_subcommand"
-asyncio.run(main())
+
+main()
diff --git a/chanbans/db.py b/chanbans/db.py
index 3f5bb01..ab35e96 100644
--- a/chanbans/db.py
+++ b/chanbans/db.py
@@ -54,6 +54,7 @@ def search_db(
board: str = "",
reason: str = "",
name: str = "",
+ trip: str = "",
com: str = "",
sub: str = "",
time_before: int = sys.maxsize,
@@ -68,9 +69,10 @@ def search_db(
select *
from bans
where
- (:board is null or board like :board_like)
+ (:board = "" or board = :board)
and (:reason = "" or reason like :reason_like)
and (:name = "" or name like :name_like)
+ and (:trip = "" or trip like :trip_like)
and (:com = "" or com like :com_like)
and (:sub = "" or sub like :sub_like)
and (time <= :time_before)
@@ -82,11 +84,12 @@ def search_db(
""",
{
"board": board,
- "board_like": f"%{board}%",
"reason": reason,
"reason_like": f"%{reason}%",
"name": name,
"name_like": f"%{name}%",
+ "trip": trip,
+ "trip_like": f"%{trip}%",
"com": com,
"com_like": f"%{com}%",
"sub": sub,
diff --git a/chanbans/http.py b/chanbans/http.py
new file mode 100644
index 0000000..9178050
--- /dev/null
+++ b/chanbans/http.py
@@ -0,0 +1,174 @@
+from collections import defaultdict
+import functools
+import json
+from typing import Any, MutableMapping, Optional, Sequence
+
+from aiohttp import web
+from jinja2 import Environment, PackageLoader, select_autoescape
+
+from .db import get_db, search_db
+
+# 2023-07-24 - eating stirfry tonight. It's pretty bad.
+# 2023-07-31 - In a meeting. Hope I don't get caught
+
+_env = Environment(
+ loader=PackageLoader("chanbans"),
+ autoescape=select_autoescape(),
+)
+
+TemplateContext = MutableMapping[str, Any]
+
+
+class HtmlResponse(web.Response):
+ def __init__(self, *args, **kwargs):
+ headers = kwargs.get(
+ "headers",
+ {
+ "Content-Type": "text/html",
+ },
+ )
+ kwargs["headers"] = headers
+ super().__init__(*args, **kwargs)
+
+
+class TemplateView(web.View):
+ template_path: str
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ assert not hasattr(self, "_env")
+
+ global _env
+ self._env = _env
+
+ self.template = self.env.get_template(self.template_path)
+
+ @property
+ def env(self) -> Environment:
+ return self._env
+
+ def get_context(self) -> TemplateContext:
+ "Gets the context for this template"
+ return dict()
+
+ def template_render(self) -> str:
+ "Renders the template on this object."
+ context = self.get_context()
+ return self.template.render(**context)
+
+
+################################################################################
+# Template views
+################################################################################
+
+
+def template_view_factory(path: str) -> type:
+ class FactoryView(TemplateView):
+ template_path = path
+
+ async def get(self):
+ html = self.template_render()
+ return HtmlResponse(text=html)
+
+ return FactoryView
+
+
+class IndexView(TemplateView):
+ template_path = "index.html"
+
+ @functools.cache
+ def get_context(self) -> TemplateContext:
+ ctx = super().get_context()
+ query = self.get_search_query()
+ ctx["posts"] = search_db(**query)
+ ctx["query"] = query
+ return ctx
+
+ # Query parameters:
+ # ?board=pol
+ # &reason=evasion
+ # &name=Anonymous
+ # &trip=asdfbgh
+ # &com=comment%20search
+ # &sub=subject%20search
+ # &time_before=123456
+ # &time_after=123456
+ # &md5=md5_sum
+ # &page=0
+
+ def get_search_query(self):
+ allowed_keys = (
+ "board",
+ "reason",
+ "name",
+ "trip",
+ "com",
+ "sub",
+ "time_before",
+ "time_after",
+ "md5",
+ "page",
+ )
+ query = {
+ key: value
+ for key, value in self.request.query.items()
+ if key in allowed_keys and key in self.request.query
+ }
+ if "time_before" in query:
+ try:
+ query["time_before"] = int(query["time_before"])
+ except ValueError:
+ query.pop("time_before")
+ if "time_after" in query:
+ try:
+ query["time_after"] = int(query["time_after"])
+ except ValueError:
+ query.pop("time_after")
+ return query
+
+ async def get(self):
+ html = self.template_render()
+ return HtmlResponse(text=html)
+
+
+################################################################################
+# API views
+################################################################################
+
+
+class BansJson(web.View):
+ async def get(self):
+ db = get_db()
+ page = self.request.match_info["page"]
+
+ # TODO - configure the number of bans per page in settings
+ BANS_PER_PAGE = 10
+
+ result = db.execute(
+ """
+ SELECT *
+ FROM bans
+ ORDER BY id
+ DESC LIMIT :limit
+ """,
+ {"limit": BANS_PER_PAGE},
+ )
+ return web.json_response(list(result.fetchall()))
+
+
+################################################################################
+# Routes
+################################################################################
+
+
+app = web.Application()
+app.add_routes(
+ [
+ web.get(r"/bans", IndexView),
+ web.get(r"/bans/faq", template_view_factory("faq.html")),
+ #web.get(r"/api/v1/bans/{page:\d+}", BansJson),
+ ]
+)
+
+def run_app():
+ web.run_app(app)
diff --git a/chanbans/templates/base.html b/chanbans/templates/base.html
new file mode 100644
index 0000000..d6a5de2
--- /dev/null
+++ b/chanbans/templates/base.html
@@ -0,0 +1,179 @@
+
+
+
+ {% block head %}
+ {% endblock head %}
+
+ {% block title %}
+ {% endblock title %}
+
+
+{% block style %}
+{% endblock style %}
+
+
+ {% block body %}
+
+ {% block header %}
+
+ {% endblock header %}
+
+
+ [ Home / FAQ ]
+
+
+ {% block main %}
+ {% endblock main %}
+
+ {% endblock body %}
+
+
diff --git a/chanbans/templates/faq.html b/chanbans/templates/faq.html
new file mode 100644
index 0000000..2198e96
--- /dev/null
+++ b/chanbans/templates/faq.html
@@ -0,0 +1,38 @@
+{% extends "base.html" %}
+
+{% block title %}4chan bans archive FAQ - hiddenservice.cc{% endblock title %}
+
+{% block style %}
+
+{% endblock style %}
+
+{% block main %}
+
+
What is this place?
+
+ 4chan will periodically (every 15-60
+ minutes) publish a small list of bans that have been handed out on the
+ website for transparency. This website exists to archive those bans and
+ uphold that transparency.
+
+
What happened to 4bans?
+
+ 4bans is still around. I'm not affiliated with them. Their DNS provider was
+ having "major issues" - I don't know the full situation. They are still
+ accessible through their IP address directly:
+ http://185.10.68.107:1776
+
+
+{% endblock %}
diff --git a/chanbans/templates/index.html b/chanbans/templates/index.html
new file mode 100644
index 0000000..9e7333e
--- /dev/null
+++ b/chanbans/templates/index.html
@@ -0,0 +1,69 @@
+{% extends "base.html" %}
+
+{% block title %}4chan bans archive - hiddenservice.cc{% endblock title %}
+
+{% block main %}
+
+
+
+
+
+
+ Post
+ Action
+ Length
+ Reason
+
+
+ {% for post in posts %}
+
+
+
+
+
+
/{{post['board']}}/
+
{{post['sub']|safe}}
+
+ {{post['name']}}
+ {% if post['trip'] %}
+ {{post['trip']}}
+ {% endif %}
+
+
{{post['now']}}
+
No. XXX
+
+ {% if post['thumb_path'] %}
+
+
+
+ File: {{post['tim']}}{{post['ext']}}
+ ({{post['fsize']}}, {{post['w']}}x{{post['h']}}, {{post['filename']}}{{post['ext']}})
+
+
+
+
+
+
+ {% endif %}
+
+ {{post['com']|safe}}
+
+
+
+ {{post['action']}}
+ {{post['length']}}
+ {{post['reason']}}
+
+ {% endfor %}
+
+ {# pagination #}
+{% endblock main %}
diff --git a/poetry.lock b/poetry.lock
index 78a8f94..7377406 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -485,6 +485,24 @@ pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
+[[package]]
+name = "jinja2"
+version = "3.1.2"
+description = "A very fast and expressive template engine."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
+ {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
[[package]]
name = "lazy-object-proxy"
version = "1.9.0"
@@ -531,6 +549,66 @@ files = [
{file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"},
]
+[[package]]
+name = "markupsafe"
+version = "2.1.3"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
+ {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
+]
+
[[package]]
name = "mccabe"
version = "0.7.0"
@@ -1007,4 +1085,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
-content-hash = "e73a1335e7f7de402b4ed8fa81acc6580b7c53cf938e17724837ab1dc31ac6de"
+content-hash = "40f027ee1da4d79b385c85556d6ff43600364194c9352a273c419b96f313a868"
diff --git a/pyproject.toml b/pyproject.toml
index 59312c6..d8c57ae 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,6 +10,7 @@ readme = "README.md"
python = "^3.9"
beautifulsoup4 = "^4.12.2"
aiohttp = "^3.8.4"
+jinja2 = "^3.1.2"
[tool.poetry.group.dev.dependencies]