message deletions now supported on all layers
parent
d53b0dcf21
commit
953929e5a3
|
@ -6,6 +6,14 @@ import json
|
|||
|
||||
|
||||
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
|
||||
mapped to native methods, it maps error responses to exceptions, and
|
||||
|
@ -463,6 +471,16 @@ class BBJ(object):
|
|||
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):
|
||||
"""
|
||||
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 = {
|
||||
"editor": os.getenv("EDITOR", default="nano"),
|
||||
"shift_multiplier": 5,
|
||||
"integrate_external_editor": True,
|
||||
"dramatic_exit": True,
|
||||
"date": "%Y/%m/%d",
|
||||
|
@ -449,6 +450,34 @@ class App(object):
|
|||
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):
|
||||
quotes = self.get_quotes(message)
|
||||
|
@ -463,6 +492,11 @@ class App(object):
|
|||
self.quote_view_menu, quotes))
|
||||
|
||||
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))
|
||||
|
||||
widget = OptionsMenu(
|
||||
|
@ -837,6 +871,12 @@ class App(object):
|
|||
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):
|
||||
"""
|
||||
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"])
|
||||
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"])
|
||||
urwid.connect_signal(editor_display, "change", self.set_new_editor, editor_buttons)
|
||||
for editor in editors:
|
||||
|
@ -928,6 +971,9 @@ class App(object):
|
|||
urwid.Text(("button", "Max message width:")),
|
||||
urwid.AttrMap(width_edit, "opt_prompt"),
|
||||
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("You can type in your own command or use one of these presets."),
|
||||
urwid.Divider(),
|
||||
|
@ -1065,7 +1111,7 @@ class App(object):
|
|||
**frame_theme()
|
||||
),
|
||||
"bar"),
|
||||
self.loop.screen_size[1] // 2),])
|
||||
self.loop.screen_size[1] // 2)])
|
||||
self.switch_editor()
|
||||
|
||||
|
||||
|
@ -1193,6 +1239,7 @@ class FootPrompt(Prompt):
|
|||
app.loop.widget.focus_position = "body"
|
||||
app.set_default_footer()
|
||||
self.callback(self.get_edit_text(), *self.args)
|
||||
|
||||
elif key.lower() in ["esc", "ctrl g", "ctrl c"]:
|
||||
app.loop.widget.focus_position = "body"
|
||||
app.set_default_footer()
|
||||
|
@ -1222,6 +1269,10 @@ class ExternalEditor(urwid.Terminal):
|
|||
|
||||
|
||||
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:
|
||||
app.close_editor()
|
||||
with open(self.path) as _:
|
||||
|
@ -1242,8 +1293,10 @@ class ExternalEditor(urwid.Terminal):
|
|||
self.terminate()
|
||||
app.close_editor()
|
||||
app.refresh()
|
||||
|
||||
elif key == "f2":
|
||||
app.switch_editor()
|
||||
|
||||
elif key == "f3":
|
||||
app.formatting_help()
|
||||
|
||||
|
@ -1271,7 +1324,7 @@ class OptionsMenu(urwid.LineBox):
|
|||
self.keypress(size, "down")
|
||||
|
||||
elif key in ["shift up", "K", "P"]:
|
||||
for x in range(5):
|
||||
for x in range(app.prefs["shift_multiplier"]):
|
||||
self.keypress(size, "up")
|
||||
|
||||
elif key.lower() in ["left", "h", "q"]:
|
||||
|
@ -1286,7 +1339,7 @@ class OptionsMenu(urwid.LineBox):
|
|||
elif key in ["ctrl p", "k", "p"]:
|
||||
return self.keypress(size, "up")
|
||||
|
||||
elif key == "ctrl l":
|
||||
elif key.lower() == "ctrl l":
|
||||
wipe_screen()
|
||||
|
||||
|
||||
|
@ -1308,11 +1361,11 @@ class ActionBox(urwid.ListBox):
|
|||
self._keypress_up(size)
|
||||
|
||||
elif key in ["shift down", "J", "N"]:
|
||||
for x in range(5):
|
||||
for x in range(app.prefs["shift_multiplier"]):
|
||||
self._keypress_down(size)
|
||||
|
||||
elif key in ["shift up", "K", "P"]:
|
||||
for x in range(5):
|
||||
for x in range(app.prefs["shift_multiplier"]):
|
||||
self._keypress_up(size)
|
||||
|
||||
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"])
|
||||
|
||||
|
||||
@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
|
||||
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.
|
||||
"""
|
||||
|
||||
# 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.utils import ordered_keys, schema_values
|
||||
from src import schema
|
||||
|
@ -150,6 +154,43 @@ def thread_reply(connection, author_id, thread_id, body, time_override=None):
|
|||
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):
|
||||
"""
|
||||
Perform all the neccesary sanity checks required to edit a post
|
||||
|
|
Loading…
Reference in New Issue