aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorubq323 <ubq323@ubq323.website>2022-01-12 01:45:12 +0000
committerubq323 <ubq323@ubq323.website>2022-01-12 01:45:12 +0000
commite4935f56341c0ceb613a37d01a979e18ec21e85d (patch)
treec276dabea07409aea4521c8ac78236a41b392d5f
parent828aa456e72bcaf6e46d5fd17792f08d4dcfc62f (diff)
parent53361d4c4880e76a6f1d1b3ebe009422dd5438fc (diff)
Merge branch 'webhooks' into trunk
-rw-r--r--apioforum/__init__.py1
-rw-r--r--apioforum/db.py8
-rw-r--r--apioforum/forum.py35
-rw-r--r--apioforum/static/style.css4
-rw-r--r--apioforum/templates/base.html2
-rw-r--r--apioforum/templates/common.html20
-rw-r--r--apioforum/templates/view_forum.html9
-rw-r--r--apioforum/templates/view_post.html2
-rw-r--r--apioforum/templates/view_thread.html3
-rw-r--r--apioforum/thread.py60
-rw-r--r--apioforum/webhooks.py153
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>&trade;</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">&lt;&lt; first</a>
+ <a href="{{url_for(view,page=page-1,**kwargs)}}" aria-label="previous page">&lt; 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 &gt;</a>
+ <a href="{{url_for(view,page=max_pageno,**kwargs)}}" aria-label="last page">last &gt;&gt;</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)
+
+