diff options
author | ubq323 <ubq323@ubq323.website> | 2022-01-12 01:45:12 +0000 |
---|---|---|
committer | ubq323 <ubq323@ubq323.website> | 2022-01-12 01:45:12 +0000 |
commit | e4935f56341c0ceb613a37d01a979e18ec21e85d (patch) | |
tree | c276dabea07409aea4521c8ac78236a41b392d5f | |
parent | 828aa456e72bcaf6e46d5fd17792f08d4dcfc62f (diff) | |
parent | 53361d4c4880e76a6f1d1b3ebe009422dd5438fc (diff) |
Merge branch 'webhooks' into trunk
-rw-r--r-- | apioforum/__init__.py | 1 | ||||
-rw-r--r-- | apioforum/db.py | 8 | ||||
-rw-r--r-- | apioforum/forum.py | 35 | ||||
-rw-r--r-- | apioforum/static/style.css | 4 | ||||
-rw-r--r-- | apioforum/templates/base.html | 2 | ||||
-rw-r--r-- | apioforum/templates/common.html | 20 | ||||
-rw-r--r-- | apioforum/templates/view_forum.html | 9 | ||||
-rw-r--r-- | apioforum/templates/view_post.html | 2 | ||||
-rw-r--r-- | apioforum/templates/view_thread.html | 3 | ||||
-rw-r--r-- | apioforum/thread.py | 60 | ||||
-rw-r--r-- | apioforum/webhooks.py | 153 |
11 files changed, 273 insertions, 24 deletions
diff --git a/apioforum/__init__.py b/apioforum/__init__.py index 2b49066..86e5ff9 100644 --- a/apioforum/__init__.py +++ b/apioforum/__init__.py @@ -54,6 +54,7 @@ def create_app(): return dict(path_for_next=p) app.jinja_env.globals.update(forum_path=forum.forum_path) + app.jinja_env.globals.update(post_jump=thread.post_jump) from .roles import has_permission, is_bureaucrat, get_user_role app.jinja_env.globals.update( has_permission=has_permission, diff --git a/apioforum/db.py b/apioforum/db.py index c0c8c7e..269bd77 100644 --- a/apioforum/db.py +++ b/apioforum/db.py @@ -206,9 +206,17 @@ ALTER TABLE forums ADD COLUMN unlisted NOT NULL DEFAULT 0; """, """ ALTER TABLE role_config ADD COLUMN p_view_forum INT NOT NULL DEFAULT 1; +""", """ +CREATE TABLE webhooks ( + id INTEGER PRIMARY KEY, + type TEXT NOT NULL, + url TEXT NOT NULL, + forum INTEGER NOT NULL REFERENCES forums(id) +);""", ] + def init_db(): db = get_db() version = db.execute("PRAGMA user_version;").fetchone()[0] diff --git a/apioforum/forum.py b/apioforum/forum.py index 2931df9..3d7611b 100644 --- a/apioforum/forum.py +++ b/apioforum/forum.py @@ -13,8 +13,11 @@ from .roles import get_forum_roles,has_permission,is_bureaucrat,get_user_role, p from .permissions import is_admin from sqlite3 import OperationalError import datetime +import math import functools +THREADS_PER_PAGE = 20 + bp = Blueprint("forum", __name__, url_prefix="/") @bp.route("/") @@ -46,7 +49,7 @@ def forum_path(forum_id): ancestors.reverse() return ancestors -def forum_route(relative_path, **kwargs): +def forum_route(relative_path, pagination=False, **kwargs): def decorator(f): path = "/<int:forum_id>" if relative_path != "": @@ -62,6 +65,9 @@ def forum_route(relative_path, **kwargs): abort(404) return f(forum, *args, **kwargs) + if pagination: + wrapper = bp.route(path+"/page/<int:page>", **kwargs)(wrapper) + return decorator def requires_permission(permission, login_required=True): @@ -83,9 +89,12 @@ def requires_bureaucrat(f): return f(forum, *args, **kwargs) return wrapper -@forum_route("") + +@forum_route("",pagination=True) @requires_permission("p_view_forum", login_required=False) -def view_forum(forum): +def view_forum(forum,page=1): + if page < 1: + abort(400) db = get_db() threads = db.execute( """SELECT @@ -100,8 +109,18 @@ def view_forum(forum): INNER JOIN most_recent_posts ON most_recent_posts.thread = threads.id INNER JOIN number_of_posts ON number_of_posts.thread = threads.id WHERE threads.forum = ? - ORDER BY threads.updated DESC; - """,(forum['id'],)).fetchall() + ORDER BY threads.updated DESC + LIMIT ? OFFSET ?; + """,( + forum['id'], + THREADS_PER_PAGE, + (page-1)*THREADS_PER_PAGE, + )).fetchall() + + # XXX: update this when thread filtering happens + num_threads = db.execute("SELECT count(*) AS count FROM threads WHERE threads.forum = ?",(forum['id'],)).fetchone()['count'] + max_pageno = math.ceil(num_threads/THREADS_PER_PAGE) + thread_tags = {} thread_polls = {} @@ -169,6 +188,8 @@ def view_forum(forum): bureaucrats=bureaucrats, thread_polls=thread_polls, avail_tags=avail_tags, + max_pageno=max_pageno, + page=page, ) @forum_route("create_thread",methods=("GET","POST")) @@ -204,6 +225,10 @@ def create_thread(forum): (thread_id,g.user,content) ) db.commit() + + from . import webhooks + thread = db.execute("select * from threads where id = ?",(thread_id,)).fetchone() + webhooks.do_webhooks_thread(forum['id'],thread) return redirect(url_for('thread.view_thread',thread_id=thread_id)) flash(err) diff --git a/apioforum/static/style.css b/apioforum/static/style.css index 280749b..342db6c 100644 --- a/apioforum/static/style.css +++ b/apioforum/static/style.css @@ -116,6 +116,10 @@ nav#navbar p:first-of-type { margin-left:0.5em } nav#navbar a { color: blue; text-decoration: none } nav#navbar .links { display: flex; } +nav#pages { text-align: center; margin-top: 2vh; margin-bottom: 2vh; } +nav#pages a { margin-left: 2vw; margin-right: 2vw; } +nav#pages .mid { margin-left: 5vw; margin-right: 5vw; } + /* todo: make the navbar less bad */ .flashmsg { border: 1px solid black; background-color: yellow; max-width: max-content; padding: 5px; clear: both;} diff --git a/apioforum/templates/base.html b/apioforum/templates/base.html index ca1dd87..b97117f 100644 --- a/apioforum/templates/base.html +++ b/apioforum/templates/base.html @@ -10,7 +10,7 @@ <link rel="icon" href="//gh0.pw/favicon.ico"> </head> <body> - <nav id="navbar"> + <nav aria-label="main" id="navbar"> <p style="font-family: monospace;"><b>apio</b><i>forum</i>™</p> <form class="inline-form" action="/search"> <input type="search" placeholder="query" name="q"> diff --git a/apioforum/templates/common.html b/apioforum/templates/common.html index f6b6f29..638c423 100644 --- a/apioforum/templates/common.html +++ b/apioforum/templates/common.html @@ -2,10 +2,6 @@ <a href="{{url_for('user.view_user',username=username)}}" class="username">{{username}}</a> {%- endmacro %} -{% macro post_url(post) -%} - {{url_for('thread.view_thread', thread_id=post.thread)}}#post_{{post.id}} -{%- endmacro %} - {% macro disp_post(post, buttons=False, forum=None, footer=None) %} <div class="post {% if post.deleted %}deleted-post{% endif %}" id="post_{{post.id}}"> <div class="post-heading"> @@ -49,7 +45,7 @@ href="{{url_for('thread.view_post',post_id=post.id)}}">src</a> {% endif %} - <a class="post-anchor-link" href="{{post_url(post)}}">#{{post.id}}</a> + <a class="post-anchor-link" href="{{post_jump(post.id)}}">#{{post.id}}</a> </span> </div> <div class="post-content md"> @@ -142,3 +138,17 @@ </desc> </svg> {% endmacro %} + +{% macro pagination_nav(page,max_pageno,view) %} +<nav aria-label="pagination" id="pages"> + {% if page > 1 %} + <a href="{{url_for(view,**kwargs)}}" aria-label="first page"><< first</a> + <a href="{{url_for(view,page=page-1,**kwargs)}}" aria-label="previous page">< prev</a> + {% endif %} + page {{page}} of {{max_pageno}} + {% if page < max_pageno %} {# > #} + <a href="{{url_for(view,page=page+1,**kwargs)}}" aria-label="next page">next ></a> + <a href="{{url_for(view,page=max_pageno,**kwargs)}}" aria-label="last page">last >></a> + {% endif %} +</nav> +{% endmacro %} diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html index 0eada1a..75144c8 100644 --- a/apioforum/templates/view_forum.html +++ b/apioforum/templates/view_forum.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% from 'common.html' import ts, tag, disp_user, post_url, forum_breadcrumb, ab, vote_meter %} +{% from 'common.html' import ts, tag, disp_user, post_url, forum_breadcrumb, ab, vote_meter, pagination_nav %} {% block header %} <h1>{% block title %}{{forum.name}}{% endblock %} <span class="thing-id">#{{forum.id}}</span></h1> {% if forum.id != 1 %} @@ -112,7 +112,7 @@ you do not have permission to create threads in this forum {{ ts(thread.mrp_created) }} </span> <span class="thread-preview-post"> - <a href="{{url_for('thread.view_thread',thread_id=thread.id)}}#post_{{thread.mrp_id}}"> + <a href="{{post_jump(thread.mrp_id)}}"> {{ thread.mrp_content[:500]|e }} </a> </span> @@ -120,7 +120,7 @@ you do not have permission to create threads in this forum {% else %} <div class="listing-caption"> <a class="thread-preview-post" - href="{{url_for('thread.view_thread',thread_id=thread.id)}}#post_{{thread.mrp_id}}"> + href="{{post_jump(thread.mrp_id)}}"> latest post </a> </div> @@ -133,6 +133,9 @@ you do not have permission to create threads in this forum </div> {%endfor%} </div> +{{ pagination_nav(page,max_pageno,'forum.view_forum',forum_id=forum.id) }} + + {% else %} <p>you do not have permission to view threads in this forum</p> {% endif %} diff --git a/apioforum/templates/view_post.html b/apioforum/templates/view_post.html index 993c005..fcaf29b 100644 --- a/apioforum/templates/view_post.html +++ b/apioforum/templates/view_post.html @@ -8,5 +8,5 @@ {{disp_post(post,False)}} <p>post source:</p> <textarea readonly class="new-post-box" name="newcontent">{{post.content}}</textarea> -<a href="{{url_for('thread.view_thread',thread_id=post.thread)}}">i am satisfied</a> +<a href="{{post_jump(post.id)}}">i am satisfied</a> {% endblock %} diff --git a/apioforum/templates/view_thread.html b/apioforum/templates/view_thread.html index 132fd44..fe22cfc 100644 --- a/apioforum/templates/view_thread.html +++ b/apioforum/templates/view_thread.html @@ -1,4 +1,4 @@ -{% from 'common.html' import disp_post,tag,thread_breadcrumb,vote_meter %} +{% from 'common.html' import disp_post,tag,thread_breadcrumb,vote_meter,pagination_nav %} {% extends 'base.html' %} {% block header %} <h1>{%block title %}{{thread.title}}{% endblock %} <span class="thing-id">#{{thread.id}}</span></h1> @@ -69,6 +69,7 @@ {% endif %} {% endfor %} </div> +{{ pagination_nav(page,max_pageno,'thread.view_thread',thread_id=thread.id) }} {% if g.user and has_permission(thread.forum, g.user, "p_reply_threads") %} <form class="new-post" action="{{url_for('thread.create_post',thread_id=thread.id)}}" method="POST"> <textarea class="new-post-box" placeholder="your post here..." name="content"></textarea> diff --git a/apioforum/thread.py b/apioforum/thread.py index 2fc9dca..05abce2 100644 --- a/apioforum/thread.py +++ b/apioforum/thread.py @@ -1,6 +1,6 @@ # view posts in thread -import itertools +import itertools, math from flask import ( Blueprint, render_template, abort, request, g, redirect, @@ -8,15 +8,46 @@ from flask import ( ) from .db import get_db from .roles import has_permission +from . import webhooks from .forum import get_avail_tags bp = Blueprint("thread", __name__, url_prefix="/thread") -def post_jump(thread_id, post_id): - return url_for("thread.view_thread",thread_id=thread_id)+"#post_"+str(post_id) +POSTS_PER_PAGE = 20 + +def which_page(post_id,return_thread_id=False): + # on which page lieth the post in question? + # forget not that page numbers employeth a system that has a base of 1. + # the + # we need impart the knowledgf e into ourselves pertaining to the + # number of things + # before the thing + # yes + + db = get_db() + # ASSUMES THAT post ids are consecutive and things + # this is probably a reasonable assumption + + thread_id = db.execute('select thread from posts where id = ?',(post_id,)).fetchone()['thread'] + + number_of_things_before_the_thing = db.execute('select count(*) as c, thread as t from posts where thread = ? and id < ?;',(thread_id,post_id)).fetchone()['c'] + + + page = 1+math.floor(number_of_things_before_the_thing/POSTS_PER_PAGE) + if return_thread_id: + return page, thread_id + else: + return page + +def post_jump(post_id,*,external=False): + page,thread_id=which_page(post_id,True) + return url_for("thread.view_thread",thread_id=thread_id,page=page,_external=external)+"#post_"+str(post_id) @bp.route("/<int:thread_id>") -def view_thread(thread_id): +@bp.route("/<int:thread_id>/page/<int:page>") +def view_thread(thread_id,page=1): + if page < 1: + abort(400) db = get_db() thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone() if thread is None: @@ -26,8 +57,17 @@ def view_thread(thread_id): posts = db.execute(""" SELECT * FROM posts WHERE posts.thread = ? - ORDER BY created ASC; - """,(thread_id,)).fetchall() + ORDER BY created ASC + LIMIT ? OFFSET ?; + """,( + thread_id, + POSTS_PER_PAGE, + (page-1)*POSTS_PER_PAGE, + )).fetchall() + + num_posts = db.execute("SELECT count(*) as count FROM posts WHERE posts.thread = ?",(thread_id,)).fetchone()['count'] + max_pageno = math.ceil(num_posts/POSTS_PER_PAGE) + tags = db.execute( """SELECT tags.* FROM tags INNER JOIN thread_tags ON thread_tags.tag = tags.id @@ -72,6 +112,8 @@ def view_thread(thread_id): poll=poll, votes=votes, has_voted=has_voted, + page=page, + max_pageno=max_pageno, ) def register_vote(thread,pollval): @@ -214,8 +256,10 @@ def create_post(thread_id): (thread_id,) ) db.commit() + post = db.execute("select * from posts where id = ?",(post_id,)).fetchone() + webhooks.do_webhooks_post(thread['forum'],post) flash("post posted postfully") - return redirect(post_jump(thread_id, post_id)) + return redirect(post_jump(post_id)) return redirect(url_for('thread.view_thread',thread_id=thread_id)) @bp.route("/delete_post/<int:post_id>", methods=["GET","POST"]) @@ -287,7 +331,7 @@ def edit_post(post_id): "UPDATE posts SET content = ?, edited = 1, updated = current_timestamp WHERE id = ?",(newcontent,post_id)) db.commit() flash("post edited editiously") - return redirect(post_jump(post['thread'],post_id)) + return redirect(post_jump(post_id)) else: flash(err) return render_template("edit_post.html",post=post) diff --git a/apioforum/webhooks.py b/apioforum/webhooks.py new file mode 100644 index 0000000..01f539e --- /dev/null +++ b/apioforum/webhooks.py @@ -0,0 +1,153 @@ +import urllib +import abc +import json +from .db import get_db +from flask import url_for, flash + +def abridge_post(text): + MAXLEN = 20 + if len(text) > MAXLEN+3: + return text[:MAXLEN]+"..." + else: + return text + +webhook_types = {} +def webhook_type(t): + def inner(cls): + webhook_types[t] = cls + return cls + return inner + +class WebhookType(abc.ABC): + def __init__(self, url): + self.url = url + + @abc.abstractmethod + def on_new_thread(self,thread): + pass + @abc.abstractmethod + def on_new_post(self,post): + pass + +def get_webhooks(forum_id): + db = get_db() + # todo inheritance (if needed) + webhooks = db.execute("select * from webhooks where webhooks.forum = ?;",(forum_id,)).fetchall() + + for wh in webhooks: + wh_type = wh['type'] + if wh_type not in webhook_types: + print(f"unknown webhook type {wh_type}") + continue + wh_url = wh['url'] + wo = webhook_types[wh_type](wh_url) + yield wo + +def do_webhooks_thread(forum_id,thread): + for wh in get_webhooks(forum_id): + wh.on_new_thread(thread) +def do_webhooks_post(forum_id,post): + for wh in get_webhooks(forum_id): + wh.on_new_post(post) + +@webhook_type("fake") +class FakeWebhook(WebhookType): + def on_new_post(self, post): + print(f'fake wh {self.url} post {post["id"]}') + def on_new_thread(self, thread): + print(f'fake wh {self.url} thread {thread["id"]}') + +@webhook_type("discord") +class DiscordWebhook(WebhookType): + def send(self,payload): + headers = { + "User-Agent":"apioforum (https://g.gh0.pw/apioforum, v0.0)", + "Content-Type":"application/json", + } + req = urllib.request.Request( + self.url, + json.dumps(payload).encode("utf-8"), + headers + ) + # todo: read response and things + urllib.request.urlopen(req) + #try: + # res = urllib.request.urlopen(req) + #except urllib.error.HTTPError as e: + # print(f"error {e.code} {e.read()}") + #else: + # print(f"succ {res.read()}") + + @staticmethod + def field(name,value): + return {"name":name,"value":value,"inline":True} + + def on_new_thread(self,thread): + f = self.field + db = get_db() + forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone() + username = thread['creator'] + userpage = url_for('user.view_user',username=username,_external=True) + + forumpage = url_for('forum.view_forum',forum_id=forum['id'],_external=True) + + post = db.execute("select * from posts where thread = ? order by id asc limit 1",(thread['id'],)).fetchone() + + payload = { + "username":"apioforum", + "avatar_url":"https://d.gh0.pw/lib/exe/fetch.php?media=wiki:logo.png", + "embeds":[ + { + "title":"new thread: "+thread['title'], + "description":abridge_post(post['content']), + "url": url_for('thread.view_thread',thread_id=thread['id'],_external=True), + "color": 0xff00ff, + "fields":[ + f('author',f"[{username}]({userpage})"), + f('forum',f"[{forum['name']}]({forumpage})"), + ], + "footer":{ + "text":thread['created'].isoformat(' '), + }, + }, + ], + } + self.send(payload) + + def on_new_post(self,post): + from .thread import post_jump + f = self.field + db = get_db() + + thread = db.execute("select * from threads where id = ?",(post['thread'],)).fetchone() + threadpage = url_for('thread.view_thread',thread_id=thread['id'],_external=True) + + forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone() + forumpage = url_for('forum.view_forum',forum_id=forum['id'],_external=True) + + username = post['author'] + userpage = url_for('user.view_user',username=username,_external=True) + + payload = { + "username":"apioforum", + "avatar_url":"https://d.gh0.pw/lib/exe/fetch.php?media=wiki:logo.png", + "embeds":[ + { + "title":"re: "+thread['title'], + "description":abridge_post(post['content']), + "url": post_jump(post['id'],external=True), + "color": 0x00ffff, + "fields":[ + f('author',f"[{username}]({userpage})"), + f('thread',f"[{thread['title']}]({threadpage})"), + f('forum',f"[{forum['name']}]({forumpage})"), + ], + "footer":{ + "text":post['created'].isoformat(' '), + }, + }, + ], + } + self.send(payload) + + |