[27269] in Source-Commits

home help back first fref pref prev next nref lref last post

discuss-ng commit: Initial check-in

daemon@ATHENA.MIT.EDU (Victor Vasiliev)
Sat Sep 7 22:48:01 2013

Date: Sat, 7 Sep 2013 22:47:53 -0400
From: Victor Vasiliev <vasilvv@MIT.EDU>
Message-Id: <201309080247.r882lrYF007692@drugstore.mit.edu>
To: source-commits@MIT.EDU

https://github.com/mit-athena/discuss-ng/commit/ffc380d560a536c38d387c20ebfb53faef2e8de4
commit ffc380d560a536c38d387c20ebfb53faef2e8de4
Author: Victor Vasiliev <vasilvv@mit.edu>
Date:   Sat Sep 7 21:41:55 2013 -0400

    Initial check-in

 LICENSE             |   20 ++++
 etc/bash_completion |   34 ++++++
 meeting             |  135 ++++++++++++++++++++++++
 ndsc                |  289 +++++++++++++++++++++++++++++++++++++++++++++++++++
 setup.py            |   10 ++
 5 files changed, 488 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..28c843c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2013 Victor Vasiliev
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/etc/bash_completion b/etc/bash_completion
new file mode 100644
index 0000000..544f2dd
--- /dev/null
+++ b/etc/bash_completion
@@ -0,0 +1,34 @@
+_discuss_mtg()
+{
+    local cur="${COMP_WORDS[$COMP_CWORD]}"
+    COMPREPLY=( $(meeting complete "$cur")  )
+}
+
+_meeting()
+{
+    local cmds="add list listacl setacl"
+    local cur="${COMP_WORDS[$COMP_CWORD]}"
+
+    COMPREPLY=()
+
+    if [ $COMP_CWORD -eq 1 ]; then
+        COMPREPLY=( $(compgen -W "$cmds" -- "$cur") );
+    elif [ $COMP_CWORD -eq 2 ]; then
+        if [ "${COMP_WORDS[1]}" = "listacl" -o "${COMP_WORDS[1]}" = "setacl" ]; then
+            _discuss_mtg;
+        fi
+    fi
+
+    return 0
+
+}
+
+_ndsc()
+{
+    if [ $COMP_CWORD -eq 1 ]; then
+        _discuss_mtg;
+    fi
+}
+
+complete -F _meeting meeting
+complete -F _ndsc ndsc
diff --git a/meeting b/meeting
new file mode 100755
index 0000000..6142e73
--- /dev/null
+++ b/meeting
@@ -0,0 +1,135 @@
+#!/usr/bin/python
+
+#
+# A simple tool to manage the discuss meetings.
+#
+
+import argparse
+import discuss
+import sys
+
+acl_flags = "acdorsw"
+
+def die(text):
+    sys.stderr.write("%s\n" % text)
+    sys.exit(1)
+
+def get_user_realm(client):
+    user = client.who_am_i()
+    return user[user.find('@'):]
+
+def add_meeting():
+    if ":" in args.meeting:
+        server, path = args.meeting.split(":", 2)
+        if not path.startswith("/"):
+            path = "/var/spool/discuss/" + path
+
+        client = discuss.Client(server, timeout = 5)
+        mtg = discuss.Meeting(client, path)
+    else:
+        mtg = discuss.locate(args.meeting)
+        if not mtg:
+            die("Meeting %s was not found." % args.meeting)
+
+    rcfile = discuss.RCFile()
+    rcfile.add(mtg)
+    rcfile.save()
+
+def list_meetings():
+    rcfile = discuss.RCFile()
+    rcfile.load()
+
+    servers = list({ entry['hostname'] for entry in rcfile.entries.values() })
+    servers.sort()
+    for server in servers:
+        meetings = [ "%s [%s]" % ( entry['displayname'], ", ".join(entry['names']) )
+                for entry in rcfile.entries.values() if entry['hostname'] == server ]
+        meetings.sort()
+        print "--- Meetings on %s ---" % server
+        for meeting in meetings:
+            print "* %s" % meeting
+        print ""
+
+def get_meeting(name):
+    rcfile = discuss.RCFile()
+    meeting_location = rcfile.lookup(name)
+    if not meeting_location:
+        sys.stderr.write("Meeting %s not found in .meetings file\n" % name)
+        sys.exit(1)
+
+    server, path = meeting_location
+    client = discuss.Client(server, timeout = 5)
+    return discuss.Meeting(client, path)
+
+def list_acl():
+    meeting = get_meeting(args.meeting)
+    acl = meeting.get_acl()
+    acl.sort(key = lambda acl: acl[0])
+
+    print "%s   Principal" % acl_flags
+    print "%s   ---------" % ("-" * len(acl_flags))
+    for principal, modes in acl:
+        print "%s   %s" % (modes, principal)
+
+def set_acl():
+    meeting = get_meeting(args.meeting)
+
+    if args.bits == "null" or args.bits == "none":
+        bits = ""
+    else:
+        bits = args.bits
+
+    bits = bits.replace(" ", "")
+    if not all(bit in acl_flags for bit in bits):
+        wrong_bits = ", ".join(set(bits) - set(acl_flags))
+        die("Invalid bits present in ACL: %s" % wrong_bits)
+
+    principal = args.principal
+    if "@" not in principal:
+        principal += get_user_realm(meeting.client)
+
+    meeting.set_access(principal, bits)
+
+def complete():
+    rcfile = discuss.RCFile()
+    rcfile.load()
+
+    meetings = [entry['displayname'] for entry in rcfile.entries.values() if entry['displayname'].startswith(args.prefix)]
+    meetings.sort()
+    print " ".join( meetings )
+
+def parse_args():
+    global args
+
+    argparser = argparse.ArgumentParser(description = "Manage discuss meetings")
+    subparsers = argparser.add_subparsers()
+
+    parser_add = subparsers.add_parser('add', help = 'Add a meeting to the personal meetings list')
+    parser_add.add_argument('meeting', help = 'The name of the meeting (may be prefixed by server name using a colon)')
+
+    parser_list = subparsers.add_parser('list', help = 'Show all the meetings in the personal list')
+
+    parser_listacl = subparsers.add_parser('listacl', help = 'Show the ACL of the specified discuss meeting')
+    parser_listacl.add_argument('meeting', help = 'The meeting to display the ACL of')
+
+    parser_setacl = subparsers.add_parser('setacl', help = 'Change the access bits of the specified discuss user')
+    parser_setacl.add_argument('meeting', help = 'The meeting to modify the ACL of')
+    parser_setacl.add_argument('principal', help = 'The name of the Kerberos principal in question')
+    parser_setacl.add_argument('bits', help = 'The access modes to be set for the specified principal')
+
+    parser_complete = subparsers.add_parser('complete', help = 'Suggest the possible meetings which begin with that name')
+    parser_complete.add_argument('prefix')
+
+    parser_add.set_defaults(handler = add_meeting)
+    parser_list.set_defaults(handler = list_meetings)
+    parser_listacl.set_defaults(handler = list_acl)
+    parser_setacl.set_defaults(handler = set_acl)
+    parser_complete.set_defaults(handler = complete)
+
+    args = argparser.parse_args()
+    args.handler()
+
+try:
+    parse_args()
+except Exception as err:
+    die(err)
diff --git a/ndsc b/ndsc
new file mode 100755
index 0000000..fb3fd2c
--- /dev/null
+++ b/ndsc
@@ -0,0 +1,289 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+#
+# This is a very simple discuss client which was created to make reading discuss
+# meetings less painful. It is sort of ugly and contains globals.
+#
+
+from __future__ import division
+from __future__ import print_function
+
+import curses, discuss  # discuss always comes with curses
+
+import argparse
+import locale
+import signal
+import sys
+
+# Globals initialization
+pos_cur = 0
+pos_top = 0
+viewed_transaction = None
+
+# Functions
+
+def die(reason = ""):
+    try:
+        screen.keypad(0)
+        curses.nocbreak()
+        curses.echo()
+        curses.endwin()
+    except:
+        pass
+
+    if reason != "":
+        print(reason, file=sys.stderr)
+        sys.exit(1)
+    else:
+        sys.exit(0)
+
+class ProgressDisplay(object):
+    def __init__(self):
+        self.last_text = ""
+        curses.curs_set(0)
+        screen.nodelay(True)
+
+    def display_progress(this, cur, total, left):
+        done = total - left
+        percent = done / total * 100
+        output = "%.02f %% (%i / %i)" % (percent, done, total)
+
+        max_y, max_x = screen.getmaxyx()
+        mid_y = max_y // 2
+        mid_x = max_x // 2
+
+        screen.erase()
+        screen.addstr(mid_y, mid_x - len(output) // 2, output)
+        screen.refresh()
+
+        ch = screen.getch()
+        if ch == ord('q') or ch == 27:
+            die("Terminated by user while reading the list of the meetings")
+
+def init_meeting():
+    global client, meeting, transactions
+
+    try:
+        client = discuss.Client(server, timeout = 5)
+        meeting = discuss.Meeting(client, path)
+        transactions = list(meeting.transactions(feedback=ProgressDisplay().display_progress))
+    except Exception as err:
+        die(err.message)
+
+def init_ui():
+    global screen
+
+    screen = curses.initscr()
+    curses.noecho()
+    curses.cbreak()
+
+def pad(text, maxlen, right=False):
+    padding = " " * (maxlen - len(text))
+    if right:
+        return padding + text
+    else:
+        return text + padding
+
+def format_transaction_row(trn, width):
+    number_column_len = len(str(max_number))
+    sender_column_len = min(width // 4, max_sender_len)
+    date_column_len = 19 if 19 <= width // 4 else 10
+    subject_column_len = width - number_column_len - sender_column_len - date_column_len - 14
+    if subject_column_len < 0:
+        # Terminal is too narrow, give up
+        return ""
+
+    def truncate_column(text, maxlen):
+        if len(text) <= maxlen:
+            return pad(text, maxlen)
+        else:
+            return text[0:maxlen-1] + '…'
+
+    number_column = pad(str(trn.number), number_column_len, True)
+    sender_column = truncate_column(trn.signature, sender_column_len)
+    subject_column = truncate_column(trn.subject, subject_column_len)
+    date_column = trn.date_entered.isoformat(' ')[0:date_column_len]
+    return " %s    %s    %s    %s " % (number_column, sender_column, subject_column, date_column)
+
+def draw_window_borders(y, x, bottom_y, right_x, erase=False):
+    # Corners of the main window
+    screen.addstr(y, x, '┌')
+    screen.addstr(bottom_y, x, '└')
+    screen.addstr(y, right_x, '┐')
+    screen.addstr(bottom_y, right_x, '┘')
+
+    # Borders of the main window
+    for i in range(y+1, bottom_y):
+        screen.addstr(i, x, '│')
+        screen.addstr(i, right_x, '│')
+    for i in range(x+1, right_x):
+        screen.addstr(y, i, '─')
+        screen.addstr(bottom_y, i, '─')
+
+    if erase:
+        filler = " " * (right_x - x - 1)
+        for i in range(y+1, bottom_y):
+            screen.addstr(i, x+1, filler)
+
+def get_transaction_lines(trn):
+    return trn.text.replace('\t', '    ').split('\n')
+
+def redraw():
+    global pos_cur, pos_top, viewed_transaction, rows
+
+    max_y, max_x = screen.getmaxyx()
+    rows = max_y - 6
+
+    # Scrolling down
+    if pos_cur - pos_top > rows:
+        pos_top += 1
+    # Resizing or scrolling up
+    if pos_cur - pos_top > rows or pos_cur < pos_top:
+        pos_top = pos_cur
+    # Out-of-range safeguards
+    if pos_cur < 0:
+        pos_cur = 0
+        pos_top = 0
+    if pos_cur >= len(transactions):
+        pos_cur = len(transactions) - 1
+    if pos_top > len(transactions) - rows:
+        pos_top = len(transactions) - rows - 1
+    if pos_top < 0:
+        pos_top = 0
+
+    screen.erase()
+
+    # Main window borders
+    draw_window_borders(1, 1, max_y - 2, max_x - 2)
+
+    # Transaction list
+    current_transactions = transactions[pos_top:pos_top + rows + 1]
+    for i in range(0, len(current_transactions)):
+        cur = pos_cur - pos_top == i
+        trn = current_transactions[i]
+        text = format_transaction_row(trn, max_x - 6)
+        if cur:
+            screen.addstr(2 + i, 3, text, curses.A_REVERSE)
+        else:
+            screen.addstr(2 + i, 3, text)
+
+    footer = " %i/%i " % (pos_cur + 1, len(transactions))
+    screen.addstr(max_y-2, 6, footer)
+
+    if viewed_transaction:
+        global textpos_y, textpos_x
+
+        # Scrolling logic
+        if textpos_y < 0:
+            textpos_y = 0
+        if textpos_x < 0:
+            textpos_x = 0
+
+        draw_window_borders(3, 5, max_y - 4, max_x - 6, True)
+        lines = get_transaction_lines(viewed_transaction)
+
+        text_lines = rows - 2
+        if textpos_y + text_lines > len(lines):
+            textpos_y = max(0, len(lines) - text_lines)
+        for i in range(0, rows-2):
+            try:
+                screen.addstr(4+i, 7, lines[textpos_y+i][textpos_x:max_x-15+textpos_x])
+            except IndexError:
+                break
+
+def handle_transaction_view():
+    global textpos_y, textpos_x, viewed_transaction
+    viewed_transaction = transactions[pos_cur]
+    viewed_transaction.text = viewed_transaction.get_text()
+    textpos_y = textpos_x = 0
+
+def main_loop():
+    global pos_cur, pos_top, viewed_transaction
+    global textpos_y, textpos_x
+    global max_number, max_sender_len
+
+    screen.nodelay(False)
+    screen.keypad(True)
+
+    max_number = max(trn.number for trn in transactions)
+    max_sender_len = max(len(trn.signature) for trn in transactions)
+
+    redraw()
+
+    while True:
+        ch = screen.getch()
+
+        if viewed_transaction == None:
+            if ch == ord('q'):
+                return
+            if ch == curses.KEY_DOWN:
+                pos_cur += 1
+            if ch == curses.KEY_UP:
+                pos_cur -= 1
+            if ch == curses.KEY_PPAGE:
+                pos_top -= rows
+                pos_cur -= rows
+            if ch == curses.KEY_NPAGE:
+                pos_top += rows
+                pos_cur += rows
+            if ch == curses.KEY_HOME:
+                pos_cur = 0
+                pos_top = 0
+            if ch == curses.KEY_END:
+                pos_cur = len(transactions) - 1
+                pos_top = pos_cur - rows
+            if ch == ord('\n'):
+                handle_transaction_view()
+        else:
+            if ch == ord('q'):
+                viewed_transaction = None
+            if ch == curses.KEY_DOWN:
+                textpos_y += 1
+            if ch == curses.KEY_UP:
+                textpos_y -= 1
+            if ch == curses.KEY_RIGHT:
+                textpos_x += 5
+            if ch == curses.KEY_LEFT:
+                textpos_x -= 5
+            if ch == ord('\n'):
+                textpos_y += 1
+            if ch == ord(' '):
+                textpos_y += rows - 5
+            if ch == curses.KEY_NPAGE:
+                textpos_y += rows - 2
+            if ch == curses.KEY_PPAGE:
+                textpos_y -= rows - 2
+            if ch == ord('['):
+                pos_cur -= 1
+                handle_transaction_view()
+            if ch == ord(']'):
+                pos_cur += 1
+                handle_transaction_view()
+
+        redraw()
+
+def main():
+    global name, server, path
+
+    locale.setlocale(locale.LC_ALL, '')
+    arg_parser = argparse.ArgumentParser(description="Discuss meeting viewer")
+    arg_parser.add_argument('meeting_name', help="Name of the meeting to view")
+
+    args = arg_parser.parse_args()
+    name = args.meeting_name
+
+    rcfile = discuss.RCFile()
+    meeting_location = rcfile.lookup(name)
+    if not meeting_location:
+        die("Meeting %s not found in .meetings file" % name)
+    server, path = meeting_location
+
+    init_ui()
+    init_meeting()
+
+    main_loop()
+    die()
+
+if __name__ == '__main__':
+    main()
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..240ce26
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,10 @@
+#!/usr/bin/python
+
+from distutils.core import setup
+
+setup(name='discuss-ng',
+      version='1.0',
+      description='User interface front-end to the discuss network forum system',
+      author='Victor Vasiliev',
+      scripts=['meeting', 'ndsc']
+      )

home help back first fref pref prev next nref lref last post