message deletions now supported on all layers
parent
d53b0dcf21
commit
953929e5a3
|
@ -6,6 +6,14 @@ import json
|
||||||
|
|
||||||
|
|
||||||
class BBJ(object):
|
class BBJ(object):
|
||||||
|
# YO WHATS UP MAN
|
||||||
|
# this module isnt exactly complete. The below description claims
|
||||||
|
# `all of its endpoints are mapped to native methods` though this
|
||||||
|
# is not yet true. The documentation for the API is not yet
|
||||||
|
# complete, and neither is this client. Currently this module is
|
||||||
|
# being adapted to fit the needs of the urwid client. As it evolves,
|
||||||
|
# and the rest of the project evolves, this client will be completed
|
||||||
|
# and well documented.
|
||||||
"""
|
"""
|
||||||
A python implementation to the BBJ api: all of its endpoints are
|
A python implementation to the BBJ api: all of its endpoints are
|
||||||
mapped to native methods, it maps error responses to exceptions, and
|
mapped to native methods, it maps error responses to exceptions, and
|
||||||
|
@ -463,6 +471,16 @@ class BBJ(object):
|
||||||
return response["data"]
|
return response["data"]
|
||||||
|
|
||||||
|
|
||||||
|
def message_delete(self, thread_id, post_id):
|
||||||
|
"""
|
||||||
|
Delete message `post_id` from `thread_id`. The same rules apply
|
||||||
|
to deletions as they do for edits. The same exceptions are raised
|
||||||
|
with the same descriptions. If post_id is 0, this will also delete
|
||||||
|
the entire thread. Returns True on success.
|
||||||
|
"""
|
||||||
|
return self("delete_post", thread_id=thread_id, post_id=post_id)
|
||||||
|
|
||||||
|
|
||||||
def can_edit(self, thread_id, post_id):
|
def can_edit(self, thread_id, post_id):
|
||||||
"""
|
"""
|
||||||
Return bool True/False that the post at thread_id | post_id
|
Return bool True/False that the post at thread_id | post_id
|
||||||
|
|
|
@ -163,6 +163,7 @@ editors = ["nano", "vim", "emacs", "vim -u NONE", "emacs -Q", "micro", "ed", "jo
|
||||||
|
|
||||||
default_prefs = {
|
default_prefs = {
|
||||||
"editor": os.getenv("EDITOR", default="nano"),
|
"editor": os.getenv("EDITOR", default="nano"),
|
||||||
|
"shift_multiplier": 5,
|
||||||
"integrate_external_editor": True,
|
"integrate_external_editor": True,
|
||||||
"dramatic_exit": True,
|
"dramatic_exit": True,
|
||||||
"date": "%Y/%m/%d",
|
"date": "%Y/%m/%d",
|
||||||
|
@ -449,6 +450,34 @@ class App(object):
|
||||||
self.compose(init_body=">>%d\n\n" % message["post_id"])
|
self.compose(init_body=">>%d\n\n" % message["post_id"])
|
||||||
|
|
||||||
|
|
||||||
|
def deletion_dialog(self, button, message):
|
||||||
|
"""
|
||||||
|
Prompts the user to confirm deletion of an item.
|
||||||
|
This can delete either a thread or a post.
|
||||||
|
"""
|
||||||
|
op = message["post_id"] == 0
|
||||||
|
buttons = [
|
||||||
|
urwid.Text(("bold", "Delete this %s?" % ("whole thred" if op else "post"))),
|
||||||
|
urwid.Divider(),
|
||||||
|
cute_button(("10" , ">> Yes"), lambda _: [
|
||||||
|
network.message_delete(message["thread_id"], message["post_id"]),
|
||||||
|
self.remove_overlays(),
|
||||||
|
self.index() if op else self.refresh(False)
|
||||||
|
]),
|
||||||
|
cute_button(("30", "<< No"), self.remove_overlays)
|
||||||
|
]
|
||||||
|
|
||||||
|
# TODO: create a central routine for creating popups. this is getting really ridiculous
|
||||||
|
popup = OptionsMenu(
|
||||||
|
urwid.ListBox(urwid.SimpleFocusListWalker(buttons)),
|
||||||
|
**frame_theme())
|
||||||
|
|
||||||
|
self.loop.widget = urwid.Overlay(
|
||||||
|
popup, self.loop.widget,
|
||||||
|
align=("relative", 50),
|
||||||
|
valign=("relative", 50),
|
||||||
|
width=30, height=6)
|
||||||
|
|
||||||
|
|
||||||
def on_post(self, button, message):
|
def on_post(self, button, message):
|
||||||
quotes = self.get_quotes(message)
|
quotes = self.get_quotes(message)
|
||||||
|
@ -463,6 +492,11 @@ class App(object):
|
||||||
self.quote_view_menu, quotes))
|
self.quote_view_menu, quotes))
|
||||||
|
|
||||||
if network.can_edit(message["thread_id"], message["post_id"]):
|
if network.can_edit(message["thread_id"], message["post_id"]):
|
||||||
|
if message["post_id"] == 0:
|
||||||
|
msg = "Thread"
|
||||||
|
else: msg = "Post"
|
||||||
|
|
||||||
|
buttons.insert(0, urwid.Button("Delete %s" % msg, self.deletion_dialog, message))
|
||||||
buttons.insert(0, urwid.Button("Edit Post", self.edit_post, message))
|
buttons.insert(0, urwid.Button("Edit Post", self.edit_post, message))
|
||||||
|
|
||||||
widget = OptionsMenu(
|
widget = OptionsMenu(
|
||||||
|
@ -632,7 +666,7 @@ class App(object):
|
||||||
buttons = [
|
buttons = [
|
||||||
urwid.Text(("bold", "Discard current post?")),
|
urwid.Text(("bold", "Discard current post?")),
|
||||||
urwid.Divider(),
|
urwid.Divider(),
|
||||||
cute_button(("10" ,">> Yes"), lambda _: [
|
cute_button(("10" , ">> Yes"), lambda _: [
|
||||||
self.remove_overlays(),
|
self.remove_overlays(),
|
||||||
self.index()
|
self.index()
|
||||||
]),
|
]),
|
||||||
|
@ -837,6 +871,12 @@ class App(object):
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
|
def edit_shift(self, editor, content):
|
||||||
|
self.prefs["shift_multiplier"] = \
|
||||||
|
int(content) if content else 0
|
||||||
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def options_menu(self):
|
def options_menu(self):
|
||||||
"""
|
"""
|
||||||
Create a popup for the user to configure their account and
|
Create a popup for the user to configure their account and
|
||||||
|
@ -889,6 +929,9 @@ class App(object):
|
||||||
width_edit = urwid.IntEdit(default=self.prefs["max_text_width"])
|
width_edit = urwid.IntEdit(default=self.prefs["max_text_width"])
|
||||||
urwid.connect_signal(width_edit, "change", self.edit_width)
|
urwid.connect_signal(width_edit, "change", self.edit_width)
|
||||||
|
|
||||||
|
shift_edit = urwid.IntEdit(default=self.prefs["shift_multiplier"])
|
||||||
|
urwid.connect_signal(shift_edit, "change", self.edit_shift)
|
||||||
|
|
||||||
editor_display = Prompt(edit_text=self.prefs["editor"])
|
editor_display = Prompt(edit_text=self.prefs["editor"])
|
||||||
urwid.connect_signal(editor_display, "change", self.set_new_editor, editor_buttons)
|
urwid.connect_signal(editor_display, "change", self.set_new_editor, editor_buttons)
|
||||||
for editor in editors:
|
for editor in editors:
|
||||||
|
@ -928,6 +971,9 @@ class App(object):
|
||||||
urwid.Text(("button", "Max message width:")),
|
urwid.Text(("button", "Max message width:")),
|
||||||
urwid.AttrMap(width_edit, "opt_prompt"),
|
urwid.AttrMap(width_edit, "opt_prompt"),
|
||||||
urwid.Divider(),
|
urwid.Divider(),
|
||||||
|
urwid.Text(("button", "Scroll multiplier when holding shift:")),
|
||||||
|
urwid.AttrMap(shift_edit, "opt_prompt"),
|
||||||
|
urwid.Divider(),
|
||||||
urwid.Text(("button", "Text editor:")),
|
urwid.Text(("button", "Text editor:")),
|
||||||
urwid.Text("You can type in your own command or use one of these presets."),
|
urwid.Text("You can type in your own command or use one of these presets."),
|
||||||
urwid.Divider(),
|
urwid.Divider(),
|
||||||
|
@ -1065,7 +1111,7 @@ class App(object):
|
||||||
**frame_theme()
|
**frame_theme()
|
||||||
),
|
),
|
||||||
"bar"),
|
"bar"),
|
||||||
self.loop.screen_size[1] // 2),])
|
self.loop.screen_size[1] // 2)])
|
||||||
self.switch_editor()
|
self.switch_editor()
|
||||||
|
|
||||||
|
|
||||||
|
@ -1193,6 +1239,7 @@ class FootPrompt(Prompt):
|
||||||
app.loop.widget.focus_position = "body"
|
app.loop.widget.focus_position = "body"
|
||||||
app.set_default_footer()
|
app.set_default_footer()
|
||||||
self.callback(self.get_edit_text(), *self.args)
|
self.callback(self.get_edit_text(), *self.args)
|
||||||
|
|
||||||
elif key.lower() in ["esc", "ctrl g", "ctrl c"]:
|
elif key.lower() in ["esc", "ctrl g", "ctrl c"]:
|
||||||
app.loop.widget.focus_position = "body"
|
app.loop.widget.focus_position = "body"
|
||||||
app.set_default_footer()
|
app.set_default_footer()
|
||||||
|
@ -1222,6 +1269,10 @@ class ExternalEditor(urwid.Terminal):
|
||||||
|
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
|
if key.lower() == "ctrl l":
|
||||||
|
# always do this, and also pass it to the terminal
|
||||||
|
wipe_screen()
|
||||||
|
|
||||||
if self.terminated:
|
if self.terminated:
|
||||||
app.close_editor()
|
app.close_editor()
|
||||||
with open(self.path) as _:
|
with open(self.path) as _:
|
||||||
|
@ -1242,8 +1293,10 @@ class ExternalEditor(urwid.Terminal):
|
||||||
self.terminate()
|
self.terminate()
|
||||||
app.close_editor()
|
app.close_editor()
|
||||||
app.refresh()
|
app.refresh()
|
||||||
|
|
||||||
elif key == "f2":
|
elif key == "f2":
|
||||||
app.switch_editor()
|
app.switch_editor()
|
||||||
|
|
||||||
elif key == "f3":
|
elif key == "f3":
|
||||||
app.formatting_help()
|
app.formatting_help()
|
||||||
|
|
||||||
|
@ -1271,7 +1324,7 @@ class OptionsMenu(urwid.LineBox):
|
||||||
self.keypress(size, "down")
|
self.keypress(size, "down")
|
||||||
|
|
||||||
elif key in ["shift up", "K", "P"]:
|
elif key in ["shift up", "K", "P"]:
|
||||||
for x in range(5):
|
for x in range(app.prefs["shift_multiplier"]):
|
||||||
self.keypress(size, "up")
|
self.keypress(size, "up")
|
||||||
|
|
||||||
elif key.lower() in ["left", "h", "q"]:
|
elif key.lower() in ["left", "h", "q"]:
|
||||||
|
@ -1286,7 +1339,7 @@ class OptionsMenu(urwid.LineBox):
|
||||||
elif key in ["ctrl p", "k", "p"]:
|
elif key in ["ctrl p", "k", "p"]:
|
||||||
return self.keypress(size, "up")
|
return self.keypress(size, "up")
|
||||||
|
|
||||||
elif key == "ctrl l":
|
elif key.lower() == "ctrl l":
|
||||||
wipe_screen()
|
wipe_screen()
|
||||||
|
|
||||||
|
|
||||||
|
@ -1308,11 +1361,11 @@ class ActionBox(urwid.ListBox):
|
||||||
self._keypress_up(size)
|
self._keypress_up(size)
|
||||||
|
|
||||||
elif key in ["shift down", "J", "N"]:
|
elif key in ["shift down", "J", "N"]:
|
||||||
for x in range(5):
|
for x in range(app.prefs["shift_multiplier"]):
|
||||||
self._keypress_down(size)
|
self._keypress_down(size)
|
||||||
|
|
||||||
elif key in ["shift up", "K", "P"]:
|
elif key in ["shift up", "K", "P"]:
|
||||||
for x in range(5):
|
for x in range(app.prefs["shift_multiplier"]):
|
||||||
self._keypress_up(size)
|
self._keypress_up(size)
|
||||||
|
|
||||||
elif key in ["h", "left"]:
|
elif key in ["h", "left"]:
|
||||||
|
|
20
server.py
20
server.py
|
@ -275,6 +275,26 @@ class API(object):
|
||||||
database, user["user_id"], args["thread_id"], args["post_id"], args["body"])
|
database, user["user_id"], args["thread_id"], args["post_id"], args["body"])
|
||||||
|
|
||||||
|
|
||||||
|
@api_method
|
||||||
|
def delete_post(self, args, database, user, **kwargs):
|
||||||
|
"""
|
||||||
|
Requires the arguments `thread_id` and `post_id`.
|
||||||
|
|
||||||
|
Delete a message from a thread. The same rules apply
|
||||||
|
here as `edit_post` and `edit_query`: the logged in user
|
||||||
|
must either be the one who posted the message within 24hrs,
|
||||||
|
or have admin rights. The same error descriptions and code
|
||||||
|
are returned on falilure. Boolean true is returned on
|
||||||
|
success.
|
||||||
|
"""
|
||||||
|
if user == db.anon:
|
||||||
|
raise BBJUserError("Anons cannot delete messages.")
|
||||||
|
validate(args, ["thread_id", "post_id"])
|
||||||
|
return db.message_delete(
|
||||||
|
database, user["user_id"], args["thread_id"], args["post_id"])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@api_method
|
@api_method
|
||||||
def is_admin(self, args, database, user, **kwargs):
|
def is_admin(self, args, database, user, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
41
src/db.py
41
src/db.py
|
@ -16,6 +16,10 @@ hashes that != 64 characters in length, as a basic measure to enforce the
|
||||||
use of sha256.
|
use of sha256.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# TODO: Move methods from requiring an author id to requiring a
|
||||||
|
# database user object: these user objects are always resolved on
|
||||||
|
# incoming requests and re-resolving them from their ID is wasteful.
|
||||||
|
|
||||||
from src.exceptions import BBJParameterError, BBJUserError
|
from src.exceptions import BBJParameterError, BBJUserError
|
||||||
from src.utils import ordered_keys, schema_values
|
from src.utils import ordered_keys, schema_values
|
||||||
from src import schema
|
from src import schema
|
||||||
|
@ -150,6 +154,43 @@ def thread_reply(connection, author_id, thread_id, body, time_override=None):
|
||||||
return scheme
|
return scheme
|
||||||
|
|
||||||
|
|
||||||
|
def message_delete(connection, author, thread_id, post_id):
|
||||||
|
"""
|
||||||
|
'Delete' a message from a thread. If the message being
|
||||||
|
deleted is an OP [pid 0], delete the whole thread.
|
||||||
|
|
||||||
|
Requires an author id, the thread_id, and post_id.
|
||||||
|
The same rules for edits apply to deletions: the same
|
||||||
|
error objects are returned. Returns True on success.
|
||||||
|
"""
|
||||||
|
message_edit_query(connection, author, thread_id, post_id)
|
||||||
|
|
||||||
|
if post_id == 0:
|
||||||
|
# NUKE NUKE NUKE NUKE
|
||||||
|
connection.execute("DELETE FROM threads WHERE thread_id = ?", (thread_id,))
|
||||||
|
connection.execute("DELETE FROM messages WHERE thread_id = ?", (thread_id,))
|
||||||
|
|
||||||
|
else:
|
||||||
|
connection.execute("""
|
||||||
|
UPDATE messages SET
|
||||||
|
author = ?,
|
||||||
|
body = ?,
|
||||||
|
edited = ?
|
||||||
|
WHERE thread_id = ?
|
||||||
|
AND post_id = ?
|
||||||
|
""", (anon["user_id"], "[deleted]", False, thread_id, post_id))
|
||||||
|
# DONT deincrement the reply_count of this thread,
|
||||||
|
# or even delete the message itself. This breaks
|
||||||
|
# balance between post_id and the post's index when
|
||||||
|
# the thread is served with the messages in an array.
|
||||||
|
# *actually* deleting messages, which would be ideal,
|
||||||
|
# would increase implementation complexity for clients.
|
||||||
|
# IMO, that is not worth it. Threads are fair game.
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def message_edit_query(connection, author, thread_id, post_id):
|
def message_edit_query(connection, author, thread_id, post_id):
|
||||||
"""
|
"""
|
||||||
Perform all the neccesary sanity checks required to edit a post
|
Perform all the neccesary sanity checks required to edit a post
|
||||||
|
|
Loading…
Reference in New Issue