nodechan

decentralized peer-to-peer anonymous messageboard client
git clone git://squid-tech.com/nodechan.git
Log | Files | Refs | README

commit dac9673cbd71b82f70f49a503c156c12e4a7f353
parent 1f1893f2c0622608df97f450a16ec47385c28b33
Author: Josh <jxm5210@g.rit.edu>
Date:   Sun, 11 Aug 2019 01:39:15 -0400

Merge pull request #1 from joshiemoore/frontend

Frontend
Diffstat:
MREADME.md | 31+++++++++++++++----------------
Mrun.sh | 2+-
Asrc/GUIAddPeer.java | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/GUICreateNewThread.java | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/GUIMain.java | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/GUIRightClickMenu.java | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/GUIThreadView.java | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/IncomingThread.java | 22+++++++++++++++++++++-
Msrc/NodeChan.java | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
9 files changed, 728 insertions(+), 53 deletions(-)

diff --git a/README.md b/README.md @@ -13,11 +13,11 @@ If you would like to build NodeChan from source, clone this repository and run t ### Running If you are running the pre-compiled binary, all you have to do is type "java -jar NodeChan.jar \[-options\]" on your command line. -If you are running from source, run the "run.sh" script, or otherwise run the NodeChan.class file that was created as a result of compilation. Again, make sure to include the "lib/*" directory in your classpath. +If you are running from source, run "./run.sh \[-options\]", or otherwise run the NodeChan.class file that was created as a result of compilation. Again, make sure to include the "lib/*" directory in your classpath. * Include the command-line argument "-nogui" to run NodeChan in console mode * Include the command-line argument "-local" to run NodeChan in LAN mode (see section "LAN Mode") -* Include the command-line argument "-nohello" to not send 'hello-packets' (see format.txt) +* Include the command-line argument "-nohello" to not send 'hello-packets' (not recommended, see format.txt) * Include the command-line argument "-noinitpeer" to not connect to an initial peer from the tracker @@ -26,41 +26,40 @@ When NodeChan starts, the program will first attempt to enable UPnP port mapping Next, if you are not in LAN mode, you will be prompted to enter a peer tracker URL. It is recommended to leave this as default, unless you are hosting your own peer tracker. See section "Peer Tracker" for more information. -Finally, if you are not in LAN mode, NodeChan will attempt to retrieve an initial peer from the peer tracker. After that, you will be sent to the console interface or the GUI, depending on command-line options. +Finally, if you are not in LAN mode, NodeChan will attempt to retrieve an initial peer from the peer tracker. After that, you will be sent to the main interface. -### Console Mode -In the main console, you can create threads, read threads, reply to threads, add peers, and perform other operations. Simply enter a command and enter necessary information when prompted. Type "help" to see a list of all commands and their functions. +### Graphical Mode +When you reach the main screen, you will gradually see thread titles start to list themselves as you receive threads from your peers. Double click on a displayed thread to read that thread or reply to it. Your current number of peers is listed at the bottom-right. -Threads are specified by their "TID", a random 8-character code that is generated when a thread is posted. A thread's TID can be observed when the "threadlist" command is entered. This is the value that you need to enter when you are prompted during the "reply" command. +If you find that threads are loading and moving around too quickly, you can disable autorefresh by unchecking the "Threads->Autorefresh" main menu option. Then you will need to manually refresh the thread list by clicking "Threads->Refresh". -Thread titles are currently limited to 50 characters, and post texts are currently limited to 256 characters. This limit is planned to be increased in future releases. +Click on the "New Thread" button at the bottom-left side of the screen to post your own thread. Thread titles are currently limited to 50 characters, and post texts are currently limited to 256 characters. This limit is planned to be increased in future releases. Note that you may type your posts outside of these limits, but your peers will receive your posts trimmed to the limits. ### Blocking -When in console mode, you can block a user based on a post they have made by using the "block" command. You must know the TID of the thread the post was made in, and you must also know the PID of the abusive post. Enter these values when prompted. +You can block a user by right-clicking on one of the threads they have posted or one of the posts they have made and clicking "Block". When you block a user, all posts and threads they have created will be hidden. You will not receive any additional posts or threads from the blocked user. The user will be removed from your peer list if they are on it, and you will not be able to re-add the blocked user as a peer. Blocked users will remain blocked until you restart NodeChan. -### GUI Mode -There is currently no graphical interface for NodeChan, but this is a planned feature - coming soon! -For now, use the "-nogui" option when you run NodeChan to access console mode. +### Console Mode +Console mode is currently unsupported, and is likely to be removed in future versions. Presently, console mode is only used for debugging purposes. Use the "-nogui" command line argument to access console mode, and enter "help" for a list of commands. ### Peers Peers are the most important aspect of NodeChan. They are the other network users that you have connected to. When you create a thread or post, your content will be sent to your peers, who will send it to all of their peers, and so on. In this way, posts are able to be propagated across the entire network without the use of a central server. -The more peers a client has, the further and more thoroughly their posts will be able to propagate. However, having an excessive number of peers could lead to reduced local performace, as well as cluttered traffic on the NodeChan network. It is recommended to choose a reasonable number of peers that balances these considerations. +The more peers a client has, the further and more thoroughly their posts will be able to propagate. However, having an excessive number of peers could lead to reduced local performance, as well as cluttered traffic on the NodeChan network. It is recommended to choose a reasonable number of peers that balances these considerations. Peers will time out and be removed from your peer list if they do not send you any data for a length of time. This prevents you from wasting resources sending content to peers that may have disconnected from the network. -To add a peer with a known IP address, use the "addpeer" command. Use the "getpeer" command to retrieve a peer from the peer tracker. When you add a peer, they also automatically add you as a peer as well, unless you have specified the "-nohello" argument. +To add a peer with a known IP address, use the "Peers->Add Peer..." option from the main menu. Use the "Peers->Get Peer From Tracker" option to retrieve a peer from the peer tracker. When you add a peer, they also automatically add you as a peer, unless you have specified the "-nohello" argument. ### Peer Tracker -The peer tracker is a PHP script that I am hosting on my website. When the script is loaded (and your IP is provided in the "?ip=" query string), the script returns the IP address of a random node that is also connected to the NodeChan network. Your IP is also added to the database, so other users can add you as a peer. When you run the "getpeer" command, the client automatically does all of this and then adds the retrieved peer to your peer list. +The peer tracker is a PHP script that I am hosting on my website. When the script is loaded (and your IP is provided in the "?ip=" query string), the script returns the IP address of a random node that is also connected to the NodeChan network. Your IP is also added to the database, so other users can add you as a peer. When you click the "Peers->"Get Peer From Tracker" main menu option, the client automatically does all of this and then adds the retrieved peer to your peer list. -IP addresses in the peer tracker time out after 10 minutes and are removed from the database. +IP addresses in the peer tracker time out if they are not heard from for 10 minutes and are removed from the database. The peer tracker is the only part of the NodeChan system that could be considered somewhat "centralized", but it is also completely optional. Users could share their IP addresses and connect directly to one another, forming their own small networks without ever interacting with a traditional web server. @@ -75,7 +74,7 @@ If the argument "-local" is included when NodeChan is run, you will be able to c While NodeChan is already almost completely functional, it is not finished, nor is it particularly pretty or user-friendly. We are seeking contributors to help improve existing code and implement new features. A few planned features include: -* An intuitive GUI mode similar to other messageboard clients (important for user-friendliness) +* Improvements to the graphical interface * Security features, such as end-to-end encryption, etc. * More social features, such as direct messaging thread participants * Longer messages than 256 characters diff --git a/run.sh b/run.sh @@ -1 +1 @@ -sudo java -cp build/:lib/* com.squidtech.nodechan.NodeChan -nogui +sudo java -cp build/:lib/* com.squidtech.nodechan.NodeChan $1 $2 $3 $4 diff --git a/src/GUIAddPeer.java b/src/GUIAddPeer.java @@ -0,0 +1,58 @@ +package com.squidtech.nodechan; + +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; + +import javax.swing.JFrame; +import javax.swing.JTextField; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.AbstractAction; + +/** + * This is the view for when the user wants to add a Peer by specific IP. + */ +public class GUIAddPeer extends JFrame { + /** IP address input **/ + private JLabel infoLabel; + private JTextField textField; + private JButton button; + + /** main screen (for refreshing purposes) **/ + private GUIMain mainGui; + + public GUIAddPeer(GUIMain mainGui) { + super("Add Peer"); + this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + this.setSize(320, 120); + this.setResizable(false); + this.setLayout(new FlowLayout(FlowLayout.CENTER)); + + this.mainGui = mainGui; + + infoLabel = new JLabel("IP: "); + + textField = new JTextField("", 20); + + button = new JButton(new AbstractAction("Add") { + public void actionPerformed(ActionEvent e) { + if (!textField.getText().equals("") && + NodeChan.addPeer(textField.getText())) { + mainGui.refreshThreads(); + closePeerAdder(); + } + } + }); + + this.add(infoLabel); + this.add(textField); + this.add(button); + + this.setVisible(true); + } + + public void closePeerAdder() { + this.setVisible(false); + this.dispose(); + } +} diff --git a/src/GUICreateNewThread.java b/src/GUICreateNewThread.java @@ -0,0 +1,95 @@ +package com.squidtech.nodechan; + +import java.awt.event.ActionEvent; +import java.awt.GridLayout; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; + +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JButton; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.AbstractAction; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; + +/** + * This is the dialog window for creating and sending a new thread. + */ +public class GUICreateNewThread extends JFrame { + /** Thread title **/ + JLabel titleLabel; + JTextField titleField; + JPanel titlePanel; + + /** Thread text **/ + JLabel textLabel; + JTextArea textField; + JPanel textPanel; + + /** Submit thread **/ + JButton submit; + + JPanel container; + + public GUICreateNewThread() { + super("New Thread"); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setSize(480, 240); + setResizable(false); + + container = new JPanel(new BorderLayout()); + container.setBorder(new EmptyBorder(10, 10, 10, 10)); + + titleLabel = new JLabel("Title:"); + titleField = new JTextField(); + titlePanel = new JPanel(); + titlePanel.setLayout(new BorderLayout()); + titlePanel.add(titleLabel, BorderLayout.WEST); + titlePanel.add(titleField, BorderLayout.CENTER); + + textLabel = new JLabel("Text:"); + textField = new JTextArea(); + textField.setLineWrap(true); + textField.setBackground(new Color(255, 255, 255)); + textField.setBorder(new LineBorder(Color.DARK_GRAY, 1)); + textPanel = new JPanel(); + textPanel.setLayout(new BorderLayout()); + textPanel.add(textLabel, BorderLayout.WEST); + textPanel.add(textField, BorderLayout.CENTER); + + submit = new JButton(new AbstractAction("Post Thread") { + public void actionPerformed(ActionEvent e) { + ChanThread newThread = NodeChan.createThreadAndSend(titleField.getText(), textField.getText()); + + // automatically open the thread the user has created + if (newThread != null) { + new GUIThreadView(newThread); + closeThreadCreateScreen(); + } + } + }); + + JPanel buttonPanel = new JPanel(new BorderLayout()); + buttonPanel.setBorder(new EmptyBorder(5, 120, 5, 120)); + + container.add(titlePanel, BorderLayout.NORTH); + container.add(textPanel, BorderLayout.CENTER); + container.add(buttonPanel, BorderLayout.SOUTH); + + buttonPanel.add(submit, BorderLayout.CENTER); + + this.add(container, BorderLayout.CENTER); + + + setVisible(true); + } + + public void closeThreadCreateScreen() { + this.setVisible(false); + this.dispose(); + } +} diff --git a/src/GUIMain.java b/src/GUIMain.java @@ -0,0 +1,215 @@ +package com.squidtech.nodechan; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseAdapter; +import java.awt.BorderLayout; +import java.awt.Color; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JList; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JScrollPane; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JMenu; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.DefaultListCellRenderer; +import javax.swing.AbstractAction; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import javax.swing.JPopupMenu; + +import java.util.ArrayList; +import java.util.Vector; + +/** + * This is the main GUI class that runs when NodeChan starts. From this screen, + * users can select threads to read and reply to, as well as manage preferences + * and other options. + */ +public class GUIMain extends JFrame { + /** This user's list of threads **/ + private ArrayList<ChanThread> threads; + /** This user's list of peers **/ + private ArrayList<Peer> peers; + + /** The list of threads that will be displayed to the user **/ + private JList<ChanThread> threadList; + private JScrollPane scrollPane; + + private JMenuBar menuBar; + + /** "File" menu options **/ + JMenu fileMenu; + JMenuItem exit; + + /** "Threads" menu options **/ + JMenu threadsMenu; + JMenuItem refresh; + JCheckBoxMenuItem autorefresh; + + /** "Peers" menu options **/ + JMenu peersMenu; + JMenuItem addPeer; + JMenuItem getPeer; + JCheckBoxMenuItem keepAlive; + + /** Bottom status bar that displays the num of peers this user has **/ + JPanel statusBar; + JButton newThread; + JLabel statusNumPeers; + + public GUIMain(ArrayList<ChanThread> threads, ArrayList<Peer> peers) { + super("NodeChan"); + this.threads = threads; + this.peers = peers; + + this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + this.setLayout(new BorderLayout()); + this.setSize(640, 480); + + menuBar = new JMenuBar(); + this.setJMenuBar(menuBar); + + fileMenu = new JMenu("File"); + + exit = new JMenuItem(new AbstractAction("Exit") { + public void actionPerformed(ActionEvent e) { + System.exit(0); + } + }); + fileMenu.add(exit); + + + threadsMenu = new JMenu("Threads"); + + refresh = new JMenuItem(new AbstractAction("Refresh") { + public void actionPerformed(ActionEvent e) { + refreshThreads(); + } + }); + threadsMenu.add(refresh); + + autorefresh = new JCheckBoxMenuItem(new AbstractAction("Autorefresh") { + public void actionPerformed(ActionEvent e) { + NodeChan.autorefresh = autorefresh.getState(); + } + }); + autorefresh.setState(true); + threadsMenu.add(autorefresh); + + + peersMenu = new JMenu("Peers"); + + addPeer = new JMenuItem(new AbstractAction("Add Peer...") { + public void actionPerformed(ActionEvent e) { + new GUIAddPeer(getRef()); + } + }); + peersMenu.add(addPeer); + + getPeer = new JMenuItem(new AbstractAction("Get Peer From Tracker") { + public void actionPerformed(ActionEvent e) { + if (NodeChan.getPeerFromTracker(NodeChan.peerTrackerURL)) { + System.out.println("Added peer from tracker."); + } else { + System.out.println("No peer available from tracker."); + } + } + }); + peersMenu.add(getPeer); + + keepAlive = new JCheckBoxMenuItem(new AbstractAction("Keep Alive") { + public void actionPerformed(ActionEvent e) { + NodeChan.keepAlive = keepAlive.getState(); + } + }); + keepAlive.setState(true); + peersMenu.add(keepAlive); + + + menuBar.add(fileMenu); + menuBar.add(threadsMenu); + menuBar.add(peersMenu); + + + statusBar = new JPanel(new BorderLayout()); + statusBar.setBorder(new CompoundBorder(new LineBorder(Color.DARK_GRAY), + new EmptyBorder(4, 4, 4, 4))); + + newThread = new JButton(new AbstractAction("New Thread") { + public void actionPerformed(ActionEvent e) { + new GUICreateNewThread(); + } + }); + statusBar.add(newThread, BorderLayout.WEST); + + statusNumPeers = new JLabel("Peers: " + peers.size()); + statusBar.add(statusNumPeers, BorderLayout.EAST); + + this.add(statusBar, BorderLayout.SOUTH); + + + threadList = new JList<>(new Vector<ChanThread>(threads)); + + threadList.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + // only open threads on double-click + if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() >= 2) { + int index = threadList.locationToIndex(e.getPoint()); + ChanThread openThread = threadList.getModel().getElementAt(index); + + new GUIThreadView(openThread); + } else if (e.getButton() == MouseEvent.BUTTON3) { + int index = threadList.locationToIndex(e.getPoint()); + + if (index > -1) { + ChanThread openThread = threadList.getModel().getElementAt(index); + + new GUIRightClickMenu(openThread, openThread.getPost(0), e, (JFrame)getRef()); + } + } + } + }); + + threadList.setCellRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + Component renderer = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + + if (renderer instanceof JLabel && value instanceof ChanThread) { + ((JLabel)renderer).setText(((ChanThread)value).getTitle()); + ((JLabel)renderer).setBorder(new LineBorder(new Color(0, 0, 0))); + } + + return renderer; + } + }); + + threadList.setFixedCellHeight(40); + + scrollPane = new JScrollPane(threadList); + this.add(scrollPane); + + setVisible(true); + } + + public void refreshThreads() { + threadList.setListData(new Vector<ChanThread>(threads)); + statusNumPeers.setText("Peers: " + peers.size()); + } + + /** + * this is really weird, but it works... maybe a better way to do it? + * I'm doing this so that this can be accessed from inside anonymous + * classes + */ + public GUIMain getRef() { + return this; + } +} diff --git a/src/GUIRightClickMenu.java b/src/GUIRightClickMenu.java @@ -0,0 +1,47 @@ +package com.squidtech.nodechan; + +import java.awt.event.MouseEvent; +import java.awt.event.ActionEvent; + +import javax.swing.JPopupMenu; +import javax.swing.JMenuItem; +import javax.swing.AbstractAction; +import javax.swing.JFrame; + +/** + * This menu appears when the user right-clicks on a thread or post. + */ +public class GUIRightClickMenu extends JPopupMenu { + /** The thread that has been right-clicked **/ + private ChanThread selectedThread; + /** The post that has been-clicked, or the first post in the thread that has been right-clicked **/ + private ChanPost selectedPost; + /** The triggering MouseEvent **/ + private MouseEvent e; + /** The frame to show the JPopupMenu in **/ + private JFrame frame; + + public GUIRightClickMenu(ChanThread selectedThread, ChanPost selectedPost, MouseEvent e, JFrame frame) { + super(); + this.selectedThread = selectedThread; + this.selectedPost = selectedPost; + this.e = e; + this.frame = frame; + + JMenuItem block = new JMenuItem(new AbstractAction("Block") { + public void actionPerformed(ActionEvent a) { + NodeChan.blockUser(selectedThread.getTid(), selectedPost.getPid()); + + if (frame instanceof GUIThreadView) { + ((GUIThreadView)frame).refreshPosts(); + } + } + }); + + this.add(block); + + frame.add(this); + + this.show(frame, e.getX(), e.getY()); + } +} diff --git a/src/GUIThreadView.java b/src/GUIThreadView.java @@ -0,0 +1,152 @@ +package com.squidtech.nodechan; + +import java.util.ArrayList; +import java.util.Vector; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseAdapter; + +import javax.swing.JFrame; +import javax.swing.JButton; +import javax.swing.JTextField; +import javax.swing.JScrollPane; +import javax.swing.ScrollPaneConstants; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JLabel; +import javax.swing.JTextArea; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import javax.swing.DefaultListCellRenderer; +import javax.swing.AbstractAction; + +/** + * This is the main view for displaying and interacting with the posts of a + * single thread. + */ +public class GUIThreadView extends JFrame { + /** The thread to be displayed/interacted with **/ + private ChanThread thread; + + /** List of all posts in the thread **/ + private ArrayList<ChanPost> threadPosts; + + private JScrollPane scrollPane; + private JList<ChanPost> chanPostJList; + + /** reply/refresh buttons **/ + private JButton replyButton; + private JButton refreshButton; + + /** reply text field **/ + private JTextField replyText; + + /** reply bar **/ + private JPanel replyBar; + + public GUIThreadView(ChanThread thread) { + super(thread.getTitle()); + this.thread = thread; + + this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + this.setLayout(new BorderLayout()); + this.setSize(540, 380); + + threadPosts = new ArrayList<ChanPost>(); + + // initialize the posts + for (int i = 0; i < thread.getNumPosts(); i++) { + threadPosts.add(thread.getPost(i)); + } + + replyBar = new JPanel(new BorderLayout()); + replyBar.setBorder(new CompoundBorder(new LineBorder(Color.DARK_GRAY), + new EmptyBorder(5, 5, 10, 5))); + + replyText = new JTextField(); + + replyButton = new JButton(new AbstractAction("Reply") { + public void actionPerformed(ActionEvent e) { + NodeChan.createReplyAndSend(thread, replyText.getText()); + replyText.setText(""); + refreshPosts(); + } + }); + + refreshButton = new JButton(new AbstractAction("Refresh") { + public void actionPerformed(ActionEvent e) { + refreshPosts(); + } + }); + + JPanel buttons = new JPanel(new FlowLayout()); + buttons.add(replyButton); + buttons.add(refreshButton); + + replyBar.add(replyText, BorderLayout.CENTER); + replyBar.add(buttons, BorderLayout.EAST); + + this.add(replyBar, BorderLayout.SOUTH); + + + chanPostJList = new JList<>(new Vector<ChanPost>(threadPosts)); + + chanPostJList.setCellRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + Component renderer = new JTextArea(); + + ((JTextArea)renderer).setLineWrap(true); + ((JTextArea)renderer).setText(((ChanPost)value).getPid() + "\n" + ((ChanPost)value).getText()); + ((JTextArea)renderer).setBorder(new LineBorder(new Color(0, 0, 0))); + + return renderer; + } + }); + + chanPostJList.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON3) { + int index = chanPostJList.locationToIndex(e.getPoint()); + + if (index > -1) { + ChanPost openPost = chanPostJList.getModel().getElementAt(index); + + new GUIRightClickMenu(thread, openPost, e, (JFrame)getRef()); + } + } + } + }); + + chanPostJList.setFixedCellHeight(100); + + scrollPane = new JScrollPane(chanPostJList); + scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + this.add(scrollPane); + + this.setVisible(true); + } + + public void refreshPosts() { + threadPosts = new ArrayList<ChanPost>(); + + for (int i = 0; i < thread.getNumPosts(); i++) { + threadPosts.add(thread.getPost(i)); + } + + chanPostJList.setListData(new Vector<ChanPost>(threadPosts)); + } + + /** + * For accessing this object from within an anonymous class + */ + public GUIThreadView getRef() { + return this; + } +} diff --git a/src/IncomingThread.java b/src/IncomingThread.java @@ -9,6 +9,9 @@ import java.util.ArrayList; import java.io.IOException; +import java.util.Collections; +import java.util.Comparator; + /** * This class handles all incoming NodeChan packet traffic, and routes the * incoming data as necessary. @@ -75,7 +78,7 @@ public class IncomingThread extends Thread { } } - if (!havePeer) { + if (!havePeer && peers.size() < NodeChan.AUTO_ADD_PEER_LIMIT) { // new peer, add them to our list peers.add(new Peer(incoming.getHostAddress())); } @@ -152,11 +155,28 @@ public class IncomingThread extends Thread { post.received(); + // sort our thread list by most recent activity first + Collections.sort(threads, new Comparator<ChanThread>() { + @Override + public int compare(ChanThread thread1, ChanThread thread2) { + return thread1.compareTo(thread2); + } + }); + break; case 'H': // do nothing, the hello-packet is just for adding new peers break; } + + // check for peers that have timed out + NodeChan.checkPeerTimeouts(); + + // update the GUI when we receive packets, if GUI mode and auto-refresh + // are both enabled + if (!NodeChan.nogui && NodeChan.autorefresh) { + NodeChan.mainGui.refreshThreads(); + } } } } diff --git a/src/NodeChan.java b/src/NodeChan.java @@ -2,9 +2,9 @@ package com.squidtech.nodechan; import java.util.Scanner; import java.util.ArrayList; +import java.util.InputMismatchException; import java.util.Collections; import java.util.Comparator; -import java.util.InputMismatchException; import java.io.BufferedReader; import java.io.InputStreamReader; @@ -36,9 +36,15 @@ public class NodeChan { /** Max number of times each client will propagate a single post **/ public static final int MAX_PROPS = 3; - /** The maximum number of threads to display on one page **/ + /** The maximum number of threads to display on one console page **/ public static final int PAGE_SIZE = 10; + /** The time to delay between sending "keep-alive" packets (second) **/ + public static final int KEEP_ALIVE_DELAY = 150; + + /** No new peers will be automatically added when the client has at least this many peers **/ + public static final int AUTO_ADD_PEER_LIMIT = 50; + // command-line options @@ -58,6 +64,12 @@ public class NodeChan { /** If true, no initial peer will be retrieved from the tracker. **/ public static boolean noinitpeer = false; + /** Whether to auto-refresh the thread list in GUI mode **/ + public static boolean autorefresh = true; + + /** Whether to keep ourselves alive on the network while idle **/ + public static boolean keepAlive = true; + @@ -83,7 +95,10 @@ public class NodeChan { private static ArrayList<Peer> blocked; /** URL of the peer tracker to use **/ - private static String peerTrackerURL = "http://squid-tech.com/nodes/peer.php?ip="; + public static String peerTrackerURL = "http://squid-tech.com/nodes/peer.php?ip="; + + /** Main GUI object **/ + public static GUIMain mainGui; public static void main(String[] args) { // parse command line args @@ -191,11 +206,35 @@ public class NodeChan { } } + // start sending keep-alive packets to peers + new Thread() { + public void run() { + while(true) { + try { + Thread.sleep(KEEP_ALIVE_DELAY * 1000); + } catch (InterruptedException e) { + System.err.println("keepAlive interrupted (thread stopped)"); + break; + } + + if (keepAlive) { + if (!nohello) { + for (Peer p : peers) { + sendHelloPacket(p); + } + } + + if (!local) { + getPeerFromTracker(peerTrackerURL); + } + } + } + } + }.start(); + if (nogui) { // command-line mode while(true) { - checkPeerTimeouts(); - System.out.print("> "); input = scan.nextLine(); @@ -246,14 +285,6 @@ public class NodeChan { } } - // Sort the threads based on their last post time (most recent first) - Collections.sort(threads, new Comparator<ChanThread>() { - @Override - public int compare(ChanThread thread1, ChanThread thread2) { - return thread1.compareTo(thread2); - } - }); - System.out.println("Page " + page); System.out.println("TID Subject"); for (int i = (page - 1) * PAGE_SIZE; @@ -318,24 +349,15 @@ public class NodeChan { System.out.print("Enter peer address: "); String readIP = scan.nextLine(); - Peer newPeer = new Peer(readIP); - - // check to make sure that we haven't already blocked this peer - if (checkBlocked(newPeer.getAddress())) { - System.out.println("Cannot add a blocked user as a peer."); - continue; - } - - if (!newPeer.isResolved()) { - System.err.println("Could not add that address as a peer."); - } else { - peers.add(newPeer); - sendHelloPacket(newPeer); - - System.out.println("\nPeer " + readIP + " added."); - } + addPeer(readIP); } else if (input.equals("getpeer")) { - if (local) { + if (local) { // sort our thread list by most recent activity first + Collections.sort(threads, new Comparator<ChanThread>() { + @Override + public int compare(ChanThread thread1, ChanThread thread2) { + return thread1.compareTo(thread2); + } + }); System.out.println("Cannot add external peers in LAN mode."); continue; } @@ -398,9 +420,8 @@ public class NodeChan { } } } else { - // TODO: implement gui... - System.out.println("\nGUI not implemented yet. Run with option -nogui."); - System.exit(0); + System.out.println("Starting NodeChan GUI..."); + mainGui = new GUIMain(threads, peers); } } @@ -444,7 +465,12 @@ public class NodeChan { /** * Create a new thread based on title and text, and send it. */ - public static void createThreadAndSend(String title, String text) { + public static ChanThread createThreadAndSend(String title, String text) { + if (title.equals("") || text.equals("")) { + System.out.println("Your title and text must not be blank!"); + return null; + } + ChanThread newThread = new ChanThread(""); ChanPost newPost = new ChanPost( @@ -471,12 +497,32 @@ public class NodeChan { // report success System.out.println("\n\nThread posted. Your TID is " + newThread.getTid()); + + // sort our thread list by most recent activity first + Collections.sort(threads, new Comparator<ChanThread>() { + @Override + public int compare(ChanThread thread1, ChanThread thread2) { + return thread1.compareTo(thread2); + } + }); + + // refresh thread list if GUI is active + if (!nogui) { + mainGui.refreshThreads(); + } + + return newThread; } /** * Create a new reply to a thread and send it. */ public static void createReplyAndSend(ChanThread thread, String reply) { + if (reply.equals("")) { + System.out.println("Your reply must contain text!"); + return; + } + ChanPost newPost = new ChanPost( thread.getTid(), "", @@ -496,6 +542,43 @@ public class NodeChan { for (Peer p : peers) { new OutgoingThread(p.getAddress(), NC_PORT, outbytes).start(); } + + // sort our thread list by most recent activity first + Collections.sort(threads, new Comparator<ChanThread>() { + @Override + public int compare(ChanThread thread1, ChanThread thread2) { + return thread1.compareTo(thread2); + } + }); + + // refresh thread list if GUI is active + if (!nogui) { + mainGui.refreshThreads(); + } + } + + /** + * Add a Peer from a specific IP address + */ + public static boolean addPeer(String readIP) { + Peer newPeer = new Peer(readIP); + + // check to make sure that we haven't already blocked this peer + if (checkBlocked(newPeer.getAddress())) { + System.out.println("Cannot add a blocked user as a peer."); + return false; + } + + if (!newPeer.isResolved()) { + System.err.println("Could not add that address as a peer."); + return false; + } + + peers.add(newPeer); + sendHelloPacket(newPeer); + + System.out.println("\nPeer " + readIP + " added."); + return true; } /** @@ -611,8 +694,10 @@ public class NodeChan { InetAddress blockAddress = blockPost.getSender_addr(); // check to make sure the user isn't blocking themselves - if (blockAddress.getHostAddress().equals(node_ip.getHostAddress())) + if (blockAddress.getHostAddress().equals(node_ip.getHostAddress())) { + System.out.println("You can't block yourself!"); return false; + } // check all threads for posts by this user for (int t = 0; t < threads.size();) { @@ -647,6 +732,10 @@ public class NodeChan { } } + if (!nogui) { + mainGui.refreshThreads(); + } + return true; }