Compare commits
10 Commits
382c521e77
...
84f1314eaf
Author | SHA1 | Date |
---|---|---|
magical | 84f1314eaf | |
magical | d2d599318f | |
magical | 2718e223c6 | |
magical | 932e39ac0a | |
magical | d43b3f9979 | |
magical | 19269a1ade | |
magical | 6d15940943 | |
magical | 23c8abfae2 | |
magical | 804438f045 | |
magical | ab1550c09c |
|
@ -0,0 +1,32 @@
|
||||||
|
import zipfile
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: should we include .pyc files?
|
||||||
|
# TODO: add urwid source into the repo somewhere
|
||||||
|
|
||||||
|
files = {
|
||||||
|
'__main__.py': 'clients/urwid/main.py',
|
||||||
|
'nntp_client.py': 'clients/nntp_client.py',
|
||||||
|
'urwid': 'env/lib/python3.8/site-packages/urwid/*.py',
|
||||||
|
}
|
||||||
|
|
||||||
|
with open('bbj_demo', 'wb') as f:
|
||||||
|
f.write(b"#!/usr/bin/env python3\n")
|
||||||
|
with zipfile.ZipFile(f, 'w', compression=zipfile.ZIP_DEFLATED) as z:
|
||||||
|
z.comment = b'BBJ'
|
||||||
|
for name, source in files.items():
|
||||||
|
if '*' in source:
|
||||||
|
dirname = name
|
||||||
|
for path in sorted(glob.glob(source)):
|
||||||
|
name = dirname + '/' + os.path.basename(path)
|
||||||
|
z.write(path, name)
|
||||||
|
else:
|
||||||
|
z.write(source, name)
|
||||||
|
try:
|
||||||
|
mask = os.umask(0)
|
||||||
|
os.umask(mask)
|
||||||
|
except OSError:
|
||||||
|
mask = 0
|
||||||
|
os.chmod(z.filename, 0o777&~mask)
|
|
@ -1,15 +1,18 @@
|
||||||
|
from collections import namedtuple
|
||||||
from urllib.error import URLError
|
from urllib.error import URLError
|
||||||
import urllib.request as url
|
import urllib.request as url
|
||||||
import nntplib
|
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
import email.utils
|
||||||
|
import nntplib
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
import ssl
|
import ssl
|
||||||
import email.utils
|
import re
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
__all__ = ('BBJNews','URLError')
|
||||||
|
|
||||||
class BBJNews(object):
|
class BBJNews(object):
|
||||||
# this module isnt exactly complete. The below description claims
|
# this module isnt exactly complete. The below description claims
|
||||||
# `all of its endpoints are mapped to native methods` though this
|
# `all of its endpoints are mapped to native methods` though this
|
||||||
|
@ -506,35 +509,8 @@ class BBJNews(object):
|
||||||
# :bytes - the number of bytes in the article
|
# :bytes - the number of bytes in the article
|
||||||
# :lines - the number of lines in the body (deprecated)
|
# :lines - the number of lines in the body (deprecated)
|
||||||
|
|
||||||
if False:
|
# see also: https://www.jwz.org/doc/threading.html
|
||||||
# build up a map of message references
|
threads = _overviews_to_threads_fancy(overviews)
|
||||||
# we use a disjoint-set data structure
|
|
||||||
# to find the root of each message
|
|
||||||
threadmap = {}
|
|
||||||
rank = {}
|
|
||||||
for num, ov in overviews:
|
|
||||||
msgid = nntplib.decode_header(ov['message-id'])
|
|
||||||
# RFC5536 suggests that whitespace should not occur inside
|
|
||||||
# a message id, which (if true) makes it pretty easy to split
|
|
||||||
# the list of message ids in the references header
|
|
||||||
refs = nntplib.decode_header(ov['references']).split()
|
|
||||||
for r in refs:
|
|
||||||
threadmap[msgid] = r
|
|
||||||
rank[msgid] = 1
|
|
||||||
# TODO
|
|
||||||
else:
|
|
||||||
# make every message its own thread, for prototyping purposes
|
|
||||||
#t = {
|
|
||||||
# 'title': str,
|
|
||||||
# 'reply_count': int, # does this include the OP?
|
|
||||||
# 'pinned': bool,
|
|
||||||
# 'thread_id': uuid
|
|
||||||
# 'author': user_uuid,
|
|
||||||
# 'created': time,
|
|
||||||
# 'last_mod': time,
|
|
||||||
# 'last_author': user_uuid,
|
|
||||||
#}
|
|
||||||
threads = _overview_to_threads(overviews)
|
|
||||||
|
|
||||||
# make usermap
|
# make usermap
|
||||||
usermap = {}
|
usermap = {}
|
||||||
|
@ -545,12 +521,13 @@ class BBJNews(object):
|
||||||
addr = _parse_single_address(userid)
|
addr = _parse_single_address(userid)
|
||||||
usermap[userid] = {
|
usermap[userid] = {
|
||||||
'user_id': userid,
|
'user_id': userid,
|
||||||
'user_name': addr.name,
|
'user_name': addr.name or addr.user,
|
||||||
'address': addr.address,
|
'address': addr.address,
|
||||||
'color': colorhash(userid),
|
'color': colorhash(userid),
|
||||||
'is_admin': False, # TODO: LIST MODERATORS?
|
'is_admin': False, # TODO: LIST MODERATORS?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
threads.sort(key=lambda x: x['last_mod'], reverse=True)
|
||||||
return threads, usermap
|
return threads, usermap
|
||||||
|
|
||||||
|
|
||||||
|
@ -565,7 +542,8 @@ class BBJNews(object):
|
||||||
print(usermap[author_id]["user_name"])
|
print(usermap[author_id]["user_name"])
|
||||||
print(message["body"])
|
print(message["body"])
|
||||||
"""
|
"""
|
||||||
return {}, {}
|
m = self.fake_message('oops...')
|
||||||
|
return {"title":"", "messages":[m], "author":m['author']}, {m['author']: self.user}
|
||||||
|
|
||||||
response = self("thread_load",
|
response = self("thread_load",
|
||||||
format=format, thread_id=thread_id, op_only=op_only)
|
format=format, thread_id=thread_id, op_only=op_only)
|
||||||
|
@ -615,14 +593,14 @@ class BBJNews(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# unused
|
|
||||||
def format_message(self, body, format="sequential"):
|
def format_message(self, body, format="sequential"):
|
||||||
"""
|
"""
|
||||||
Send `body` to the server to be formatted according to `format`,
|
Send `body` to the server to be formatted according to `format`,
|
||||||
defaulting to the sequential parser. Returns the body object.
|
defaulting to the sequential parser. Returns the body object.
|
||||||
"""
|
"""
|
||||||
response = self("format_message", body=body, format=format)
|
return [[(None, body)]]
|
||||||
return response["data"]
|
#response = self("format_message", body=body, format=format)
|
||||||
|
#return response["data"]
|
||||||
|
|
||||||
# unsupported
|
# unsupported
|
||||||
def message_delete(self, thread_id, post_id):
|
def message_delete(self, thread_id, post_id):
|
||||||
|
@ -747,8 +725,78 @@ class BBJNews(object):
|
||||||
"messages": response["data"]["messages"]
|
"messages": response["data"]["messages"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _overviews_to_threads_fancy(overviews):
|
||||||
|
# build up a map of message references
|
||||||
|
# we use a disjoint-set data structure
|
||||||
|
# to find the root of each message
|
||||||
|
threadmap = {}
|
||||||
|
def find(id):
|
||||||
|
parent = threadmap.setdefault(id, id)
|
||||||
|
if parent == id:
|
||||||
|
return id
|
||||||
|
root = find(parent)
|
||||||
|
if root != parent:
|
||||||
|
threadmap[id] = root
|
||||||
|
return root
|
||||||
|
|
||||||
|
messages = {}
|
||||||
|
for num, ov in overviews:
|
||||||
|
try:
|
||||||
|
msgid = nntplib.decode_header(ov['message-id']).strip()
|
||||||
|
refs = _parse_message_ids(nntplib.decode_header(ov['references']))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
messages[msgid] = (num, msgid, ov)
|
||||||
|
for r in refs:
|
||||||
|
threadmap[find(msgid)] = find(r)
|
||||||
|
|
||||||
|
thread_messages = {}
|
||||||
|
for id in messages:
|
||||||
|
root = find(id)
|
||||||
|
l = thread_messages.setdefault(root, [])
|
||||||
|
l.append(messages[id])
|
||||||
|
|
||||||
|
threads = []
|
||||||
|
for id, messages in thread_messages.items():
|
||||||
|
messages.sort(key=lambda x: x[0])
|
||||||
|
first = messages[0][2]
|
||||||
|
last = messages[-1][2]
|
||||||
|
try:
|
||||||
|
d = nntplib.decode_header(first['date'])
|
||||||
|
d = email.utils.mktime_tz(email.utils.parsedate_tz(d))
|
||||||
|
d2 = nntplib.decode_header(last['date'])
|
||||||
|
d2 = email.utils.mktime_tz(email.utils.parsedate_tz(d2))
|
||||||
|
t = {
|
||||||
|
'pinned': False,
|
||||||
|
'title': nntplib.decode_header(first['subject']),
|
||||||
|
'reply_count': len(messages),
|
||||||
|
'thread_id': nntplib.decode_header(first['message-id']),
|
||||||
|
'author': nntplib.decode_header(first['from']),
|
||||||
|
'created': d,
|
||||||
|
'last_author': nntplib.decode_header(last['from']),
|
||||||
|
'last_mod': d2,
|
||||||
|
}
|
||||||
|
except (ValueError, KeyError, IndexError):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
threads.append(t)
|
||||||
|
|
||||||
|
return threads
|
||||||
|
|
||||||
|
|
||||||
def _overview_to_threads(overviews):
|
def _overview_to_threads(overviews):
|
||||||
|
# make every message its own thread, for prototyping purposes
|
||||||
|
#t = {
|
||||||
|
# 'title': str,
|
||||||
|
# 'reply_count': int, # does this include the OP?
|
||||||
|
# 'pinned': bool,
|
||||||
|
# 'thread_id': uuid
|
||||||
|
# 'author': user_uuid,
|
||||||
|
# 'created': time,
|
||||||
|
# 'last_mod': time,
|
||||||
|
# 'last_author': user_uuid,
|
||||||
|
#}
|
||||||
threads = []
|
threads = []
|
||||||
for num, ov in overviews:
|
for num, ov in overviews:
|
||||||
try:
|
try:
|
||||||
|
@ -782,7 +830,20 @@ def _test_overview_to_threads():
|
||||||
print(t)
|
print(t)
|
||||||
|
|
||||||
|
|
||||||
Address = namedtuple('Address', 'name, address')
|
_atext = r"[a-zA-Z0-9!#$%&'\*\+\-/=?^_`{|}~]" # RFC 5322 §3.2.3
|
||||||
|
_dotatext = r"%s+(?:\.%s+)*" % (_atext, _atext)
|
||||||
|
_mdtext = r"\[[!-=\?-Z^-~]\]"
|
||||||
|
_msg_id_re = re.compile(r'<%s@(?:%s|%s)>' % (_dotatext, _dotatext, _mdtext)) # RFC 5536 §3.1.3
|
||||||
|
|
||||||
|
def _parse_message_ids(s):
|
||||||
|
"""parses a list of message ids separated by junk"""
|
||||||
|
return _msg_id_re.findall(s)
|
||||||
|
|
||||||
|
class Address(namedtuple('Address', 'name, address')):
|
||||||
|
@property
|
||||||
|
def user(self):
|
||||||
|
user, _, _ = self.address.partition("@")
|
||||||
|
return user
|
||||||
|
|
||||||
def _parse_single_address(value):
|
def _parse_single_address(value):
|
||||||
# the email.headerregistry api is truly bizarre
|
# the email.headerregistry api is truly bizarre
|
||||||
|
|
|
@ -796,7 +796,7 @@ class App(object):
|
||||||
"""
|
"""
|
||||||
if self.mode == "thread":
|
if self.mode == "thread":
|
||||||
# mark the current position in this thread before going back to the index
|
# mark the current position in this thread before going back to the index
|
||||||
mark()
|
self.mark()
|
||||||
|
|
||||||
self.body.attr_map = {None: "default"}
|
self.body.attr_map = {None: "default"}
|
||||||
self.mode = "index"
|
self.mode = "index"
|
||||||
|
@ -862,7 +862,7 @@ class App(object):
|
||||||
self.walker += self.make_message_body(message)
|
self.walker += self.make_message_body(message)
|
||||||
self.set_default_header()
|
self.set_default_header()
|
||||||
self.set_default_footer()
|
self.set_default_footer()
|
||||||
self.goto_post(mark(thread_id))
|
self.goto_mark(thread_id)
|
||||||
|
|
||||||
|
|
||||||
def toggle_client_pin(self):
|
def toggle_client_pin(self):
|
||||||
|
@ -986,10 +986,10 @@ class App(object):
|
||||||
self.last_index_pos = self.get_focus_post(True).thread["thread_id"]
|
self.last_index_pos = self.get_focus_post(True).thread["thread_id"]
|
||||||
self.index()
|
self.index()
|
||||||
else:
|
else:
|
||||||
mark()
|
self.mark()
|
||||||
thread = self.thread["thread_id"]
|
thread = self.thread["thread_id"]
|
||||||
self.thread_load(None, thread)
|
self.thread_load(None, thread)
|
||||||
self.goto_post(mark(thread))
|
self.goto_mark(thread)
|
||||||
self.temp_footer_message("Refreshed content!", 1)
|
self.temp_footer_message("Refreshed content!", 1)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1024,10 +1024,31 @@ class App(object):
|
||||||
width=30, height=6)
|
width=30, height=6)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
mark()
|
self.mark()
|
||||||
self.index()
|
self.index()
|
||||||
|
|
||||||
|
|
||||||
|
def mark(self, thread_id=None):
|
||||||
|
if self.mode != "thread":
|
||||||
|
return
|
||||||
|
|
||||||
|
if thread_id is None:
|
||||||
|
thread_id = self.thread['thread_id']
|
||||||
|
pos = self.get_focus_post()
|
||||||
|
mark(thread_id, pos, default=0)
|
||||||
|
return pos
|
||||||
|
|
||||||
|
|
||||||
|
def goto_mark(self, thread_id=None):
|
||||||
|
if self.mode != "thread":
|
||||||
|
return
|
||||||
|
|
||||||
|
if thread_id is None:
|
||||||
|
thread_id = self.thread['thread_id']
|
||||||
|
pos = mark(thread_id, default=0)
|
||||||
|
self.goto_post(pos)
|
||||||
|
|
||||||
|
|
||||||
def get_focus_post(self, return_widget=False):
|
def get_focus_post(self, return_widget=False):
|
||||||
pos = self.box.get_focus_path()[0]
|
pos = self.box.get_focus_path()[0]
|
||||||
if self.mode == "thread":
|
if self.mode == "thread":
|
||||||
|
@ -2504,11 +2525,16 @@ def bbjrc(mode, **params):
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
def mark(directive=True):
|
def mark(key, value=None, default=None):
|
||||||
"""
|
"""
|
||||||
Set and retrieve positional marks for threads.
|
Sets a value in the markfile and returns the old value (or default).
|
||||||
This uses a seperate file from the preferences
|
This uses a seperate file from the preferences
|
||||||
to keep it free from clutter.
|
to keep it free from clutter.
|
||||||
|
|
||||||
|
The key must be a string, and the value must be
|
||||||
|
json-encodable. If value isn't provided (or is None)
|
||||||
|
then this doesn't set anything and it is only a
|
||||||
|
read operation.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(markpath, "r") as _in:
|
with open(markpath, "r") as _in:
|
||||||
|
@ -2516,19 +2542,14 @@ def mark(directive=True):
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
values = {}
|
values = {}
|
||||||
|
|
||||||
if directive == True and app.mode == "thread":
|
old = values.get(key, default)
|
||||||
pos = app.get_focus_post()
|
|
||||||
values[app.thread["thread_id"]] = pos
|
if value is not None and value != old:
|
||||||
|
values[key] = value
|
||||||
with open(markpath, "w") as _out:
|
with open(markpath, "w") as _out:
|
||||||
json.dump(values, _out)
|
json.dump(values, _out)
|
||||||
return pos
|
|
||||||
|
|
||||||
elif isinstance(directive, str):
|
|
||||||
try:
|
|
||||||
return values[directive]
|
|
||||||
except KeyError:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
return old
|
||||||
|
|
||||||
def load_client_pins():
|
def load_client_pins():
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue