message deletions now supported on all layers

pull/4/head
Blake DeMarcy 2017-04-13 11:49:20 -05:00
parent d53b0dcf21
commit 953929e5a3
4 changed files with 138 additions and 6 deletions

View File

@ -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

View File

@ -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"]:

View File

@ -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):
""" """

View File

@ -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