[27269] in Source-Commits
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']
+ )