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 %} +
+ +
+ {% 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 %} + + + + + + + + + + + {% for post in posts %} + + + + + + + {% endfor %} +
PostActionLengthReason
+
+ + {% 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']}}
+ {# 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]