Homework 7: PennPals
Computing’s core challenge is how not to make a mess of it.
— Edsger W. Dijkstra
Assignment Overview
In this assignment, you will be using Java to build an internet chat server. In particular, your task will be to design and model the internal state of the server and handle communication with users of the chat system.
How to Approach this Project
This project, like the OCaml Paint project, is designed to get you comfortable working with larger software projects with a lot of moving parts.
This project will be significantly easier if you approach it methodically and follow the program design process!
When you encounter errors or confusion, you may feel the need to refer back to the instructions and specifications. This is perfectly normal—we don’t expect you to memorize every tiny detail of the server specification at once! Programming with documentation can be overwhelming at times, but it is good practice for larger software engineering work. Understand the big picture first; work through the details of implementation at each part by referring back to the instructions. Use the instructions to clarify any confusion or edge cases you encounter.
Pay careful attention to your coding style as you complete this assignment. Things like modularity of design will come naturally if you follow the layout of the tasks listed below and plan ahead. Other stylistic concerns, such as naming conventions and formatting, will require more of an ongoing effort. This assignment has a lot of related components, and you will probably find it rewarding to maintain a fair amount of stylistic discipline. A poorly named datastructure, when it is one of many, can quickly add significant cognitive overhead to tracing through your code, should you need to debug it!
Setting Up
The codio project should be set up with all the requisite files. Make sure to keep the
PLAN.txt
file handy.
Task 1: Understanding the Problem
Client-Server Architecture
A client-server architecture is a pattern for building a distributed system that requires the coordination of a (potentially large) number of different computers. Email and the World Wide Web are examples of such systems; they allow multiple clients, usually the end users of the service, to exchange data with servers. A server is a computer system designed to provide a service to many connected clients at once, often facilitating communication between them.
Clients communicate with the server by means of a protocol, which is a standardized set of instructions for requesting and transmitting information over the internet. For example, you might have heard of HTTP (HyperText Transfer Protocol), which allows browsers to request web pages from a server. Our server and clients will use the PennPals protocol (explained in greater detail below) to communicate with each other.
User IDs and Nicknames
The ServerBackend
class is the base layer of the server, which
handles network connections with various clients. When a client connects to the server
for the first time, the ServerBackend
generates a unique integer ID
to represent the open connection with the client. (There are no guarantees about
these integers’ values beyond uniqueness.) Because chatting with other clients
based on their ID number is confusing, we will allow clients to have
nicknames which are used within the application. Unlike user IDs, which
cannot change as long as the client is connected to the server, clients can
change their nicknames at any time.
Channels and Owners
The server does not serve as one giant chat room for all connected users. Instead, this protocol is modeled around the idea of channels, or groupings of users on the server. The server has multiple channels, and a client may be in any number of channels (or in none). Any user who opts to join the discussion in a channel will receive all messages and commands that are directed to that channel after they join. At any point a user can also leave a channel, and will then stop receiving messages and commands.
The user that created a channel is designated as that channel’s
owner. The owner of a channel may kick other users from the channel. If
the channel is invite-only, the owner of the channel must add other users
by sending an InviteCommand
. For the sake of simplicity, the owner of a
channel cannot be changed; a channel is removed if its owner leaves.
PennPals Chat Protocol
The PennPals protocol is modeled around a set of commands which can be sent to the server from a client, who is known as the sender of the command. Some commands also include a target user, who is the object of an action that the sender wishes the server to perform.
The table below is a general summary of each of the commands that can be issued by a client:
Command | Effect |
NicknameCommand |
Changes the sender’s nickname. |
CreateCommand |
Creates a new channel with the sender as its owner. |
JoinCommand |
Adds the sender to a public channel. |
InviteCommand |
Invites the target user to a private channel owned by the sender. |
MessageCommand |
Delivers a message to all users in a channel. |
LeaveCommand |
Removes the sender from a channel. |
KickCommand |
Removes the target user from a private channel. |
How Commands are Represented
The server and client communicate using a text-based protocol, where commands
are encoded as strings and transmitted as a stream of characters. When the
server recieves a command string from a client, it uses the
CommandParser
class to convert the string into a corresponding
Command
object.
For example, consider the following command string:
:camel MESG java :CIS 120 is the best!
This string is a command issued by the client whose nickname is camel, instructing the server to deliver the message CIS 120 is the best! to every user in the java channel.
You don’t need to be concerned with the specifics of converting command strings to objects (since we provide you with the parser), though you will likely see them printed to the console when you run your server.
Subtyping and Dynamic Dispatch
All of the possible protocol commands are subtypes of the abstract
class Command
. Since Command
is abstract, it cannot be directly
instantiated. Unlike an interface, however, the abstract class defines some
fields and methods which are inherited by its concrete subclasses.
Command
is designed to be extended by classes
representing each of the different commands defined by the protocol. Every
instance of Command
has implementations for at least the following
methods:
int getSenderId()
String getSender()
Broadcast updateServerModel(ServerModel model)
The updateServerModel
method is marked abstract in the
definition of Command
, meaning that each subclass must have its own
implementation of that method. This allows the server backend, regardless of the
command’s dynamic class, to call updateServerModel
and rely on
dynamic dispatch to achieve the appropriate behavior.
Your job will be to implement the updateServerModel
method for
each concrete subclass of Command
, allowing the server to process
commands sent by its clients. Before that, however, you will need to think about
how the server state should be modeled.
Task 2: Designing the ServerModel
Testing with the GUI client is not sufficient to ensure that your code works as expected. Make sure to write JUnit tests in addition to any manual testing you perform. Use the tests in
ServerModelTest.java
as a guide.
The
ServerModel
is the component responsible for processing each
Command
that is generated by the parser, and for keeping track of
the server state. Your model will need to keep track of all the users connected
to the server (see task 3) and the channels they are in (task 4 and task 5).
ServerModel
documentation to see
what methods the
ServerModel
class must provide. The
structure of the server’s state should make it
convenient to implement those methods!Selecting the appropriate data structures to model your application state is one of the most important design considerations in software engineering. An improper choice can lead to numerous headaches down the road, and can be difficult to change later on because the model’s implementaton relies so heavily on the data structures it uses.
For this reason, we encourage you to take some time to think through the requirements for the server model and plan your choice of collections before beginning your implementation. Grab some coffee, go for a walk, listen to some music—but don’t write any code until you’ve put some thought into your design!
Skim through the remainder of these instructions to get a sense of what is
expected from the ServerModel
class. Decide what information the
model will be responsible for storing, and how it should be grouped and
associated. There isn’t one right answer, but certain implementations are
preferable because they limit the complexity associated with manipulating and
using the data.
The next part of the instructions for this task will provide general advice
about model design. You should take it into account—a significant
portion of your grade for this assignment is based on the clarity and efficiency
of the models you design! At the end of this task, we will ask you to
implement some model query functions using your design, and answer the questions
in the PLAN
file about your design decisions.
Choosing Collections
It’s likely you will need to store groups of objects so you can organize
and access them at a later date. Java has a wide range of built-in data
structures available
in java.util.Collections
, but for
simplicity we restrict the
ones you may use to those we have discussed at length in this course. In
particular, these are:
-
TreeSet
: Implementation of theSet
interface using binary search trees, like theBSTSet
you created in OCaml for Homework 3. Elements placed into this collection must implement theComparable
interface (see next section). -
TreeMap
: Implementation of theMap
interface using BSTs. Keys must implement theComparable
interface. Similar in structure and efficiency toTreeSet
, but can be used to assocate values with keys. -
LinkedList
: Implementation of theList
andDeque
interfaces using a doubly-linked list. This is equivalent to the deques you implemented as part of Homework 4.
You may not use any other types of
Collection
to complete this assignment.
Consider the relative merits of each type of data structure and the uses for
which they are appropriate. You should use collections whose properties
correspond to the properties of the data they will be storing. For example, if
your data has a meaningful ordering and permits duplicates, a sorted list might
be a good way of representing that order. You should write similar
justifications for the data structures you use in your PLAN
.
List<T>
, Map<K,
V>
, etc., rather
than LinkedList<T>
or TreeMap<K, V>
. Doing so is
good practice as it makes your code easier to
maintain and update—if you choose to change
the implementing class you can simply replace the
constructor call without having to make any other
changes.
Creating New Classes
You may find it useful to create new classes to represent certain components of the server state. Classes are a useful organizational tool because they associate state and functionality in small, discrete units. The design principle of separation of concerns is important to keep in mind; the internal details of a particular operation can be encapsulated within a class to hide details other classes don’t need to know about. Feel free to create as many or as few new classes (and/or interfaces) as you deem necessary, and to extend others as you wish.
Here is some useful advice to keep in mind if you end up creating new classes.
-
Designate your class fields as private. Controlling access to the internal fields of your class prevents other classes and users from relying on access to its internal state or breaking invariants. You should use getter and setter methods to query and update the internal state of your class, and ensure that those methods maintain the invaraints. This allows you to encapsulate private state and mantain separation between components of your program.
-
Override the
equals
method to define equality for instances of your class. By default, new objects inherit theequals
method fromObject
, which is simple referential equality. If you would like a more meaningful structural comparison, you are responsible for defining it, usually in terms of the data stored within the fields of the class. In addition to theequals
method, it is also good practice to override thehashCode
method to be consistent withequals
. -
Consider having your class implement the
Comparable
interface. TheTreeSet
andTreeMap
data structures rely on their elements having a natural ordering, which can be used to achieve efficient lookup of elements. Although most built-in Java types already implementComparable
, if you would like to store a custom class in one of these data structures, you must impementComparable<T>
for your classT
. Implementing this interface requires you to define the methodcompareTo
according to the specification in the Javadocs forComparable
.
Implementing User Models
Now it’s time to implement some model query functions based on your design.
If you have determined you will need to make additional classes, you should have
at least a skeletal implementation before continuing. You should add whatever
collections you plan on using as private fields in your
ServerModel
class, and ensure you are initializing them in your
constructor.
You should now implement the getRegisteredUsers
method
in the ServerModel
class, using the collections you’ve chosen
for storing information about the clients currently connected to the server.
/** ... */
comments before a class or method definition, which are
often referred to as Javadoc comments. Here’s a link to the
Javadocs for
getRegisteredUsers
.
You should also implement the getUserId
and
getNickname
methods. If you find your logic for any of these
methods to be rather convoluted, you should consider whether an alternative
design for your ServerModel
state is more appropriate. If you need
to change your design, doing so now rather than later will save you
many headaches!
Implementing Channel Models
Once you have the ability to model the users connected to the server, you
will also need to store the channels on the server, and the users they contain.
After adding the necessary collections, you should implement the
getChannels
, getUsersInChannel
, and getOwner
methods in ServerModel
. You may need to make some additional
modifications to your model at this point—remember to document them in
your PLAN
.
PLAN
file corresponding to task
2, which requires you to document your design process and justify the
decisions you made. Though you cannot yet test the query functions in
ServerModel
, you should complete them before moving on.
Task 3: Connections and Nicknames
The next ServerModel
features you will implement are
acknowledging user connections and allowing users to set their own nicknames.
We’ll first introduce the (provided) Broadcast
class, which is used
by the server to coordinate responses to clients. Then it will be your turn to
implement some of those responses.
Generating Broadcast
Objects
While the server receives commands from one client at a time, it is often the case that multiple clients should be informed about the effects of a command. For instance, when a client changes his or her nickname, everybody who can see that client should be informed of the name change, not just the user whose nickname changed.
To facilitate the sending of multiple responses at once, the
ServerModel
and ServerBackend
use the
Broadcast
class to queue a set of responses to be dispatched to
potentially many clients. Given a Broadcast
, the
ServerBackend
will take care of sending the appropriate protocol
responses to the clients. (Note the separation of concerns here—the
ServerModel
does not need to know the intricacies of the protocol,
only the public interface for the Broadcast
class.)
As you will see in the Javadocs
for Broadcast
, you cannot create a Broadcast
simply by calling the constructor. Instead, the class provides a set of static
factory methods, which can be called to create the appropriate type
of Broadcast
. Here are the conditions under which you should
use each factory method:
-
Use
Broadcast.connected
when a new client connects. This will be used to inform the client of their initially assigned nickname. -
Use
Broadcast.disconnected
when a client disconnects from the server. This will be used to inform other users in that client’s channels that they have quit the server. Note that because the connection has closed, the original client cannot be sent thisBroadcast
. -
Use
Broadcast.names
when adding a client to a channel. ThisBroadcast
should only be sent as a result of handling aJoinCommand
or anInviteCommand
. It will inform all clients that a new user has been added to the channel in question, and also inform the new user of the names of everybody already in the channel. -
Use
Broadcast.error
when processing a command results in an error. The sender who issued an invalid command (and no other clients) should be informed that their command resulted in an error. There is a specified set of error conditions for each command type, which we’ll introduce as they arise. -
Use
Broadcast.okay
in all other cases where the command is handled successfully. This method can should be used to instruct all clients to perform whatever command was issued by the sender of the command.
Handling Client Registration
The registerUser
method will be called by the server backend
when a new client connects to the server, passing the ID of the new client as an
argument. Because the client has just connected to the server, they have not yet
had the chance to set their nickname, and so you should use the provided
generateUniqueNickname
method to assign them a default nickname.
You should store the association between user ID and nickname in your model’s
data structures.
This function will return a Broadcast
object to the server
backend. You should use the connected
static method to construct a
broadcast containing the client’s initially assigned nickname. Once this is
done, your implementation should pass some of the simpler tests in
ConnectionNicknamesTest.java
.
The only recipient for this broadcast will be the user who just registered.
Handling Client Disconnection
When a client disconnects from the server, the server backend will call the
deregisterUser
method, instructing the
client to remove all state associated with the user who
has quit (if they later rejoin, they should do so as an
entirely new user). There might also be other clients
who were in the same channels as this user; they should
be notified that the user has quit. (The user who quit
cannot be sent any such notification, since they are no
longer in contact with the server.)
You will want to construct a broadcast using the disconnected
static method. This method takes the nickname of the user who just quit the
server, as well as a set of nicknames of users who were in the same channels as
the disconnected user. (This set will be empty for now, since you have not
yet implemented channel creation.)
The recipients of this broadcast should be all the users who were in a channel with the disconnecting user.
Handling Nickname Changes
The next step is allowing users to change their nicknames. As will be the
case when implementing most commands, you will need to distribute work between
the updateServerModel
method of the
corresponding Command
and the ServerModel
itself. In this case, you will need to modify the
updateServerModel
method of
the NicknameCommand
class.
updateServerModel
methods should never
directly modify the data structures and fields of ServerModel
(this should not even be possible!).
The table below contains a specification of the errors (defined in the
ServerError
enum) that might arise as a result of handling an invalid
NicknameCommand
. If such an error occurs, you should immediately
return a Broadcast
object created using the error
static method. It is important to detect such errors as early as possible, so
that the state of the server is not corrupted as a result of partially
performing invalid commands.
Error conditions for
NicknameCommand | |
NAME_ALREADY_IN_USE |
There is already a user with the desired nickname. |
INVALID_NAME |
The desired nickname contains illegal characters. |
You can use the isValidName
method in ServerModel
to validate a nickname.
If the command can be handled successfully by the model, then you should
relay the command to any clients in the same channels as the sender by using the
okay
static method in Broadcast
, including the user
who changed their name. Note that this method takes a Command
as an
argument. Since we are creating a Broadcast
as a result of handling
the current command, you can pass this
(a reference to the current
object instance) as the command.
ConnectionNicknamesTest.java
. User registration, deregistration, and
nickname changes should all be functioning correctly. If you made any
changes to your design while completing this task, you should document
this in your PLAN
.
Task 4: Channels and Messages
In this task, you will implement some more commands related to channel creation, entry and exit, and messaging. By the end, you will have a working implementation of most of the server functionality!
Handling Channel Creation
In this step, you will add support for clients creating new channels on the
server. As in earlier tasks, you will have to coordinate channel creation between the
updateServerModel
method of the CreateCommand
and the ServerModel
itself.
Recall that every channel has a name, an owner, and a set of users who are
in the channel. (For now, you can ignore the inviteOnly
parameter;
it will be used later.) When handling a CreateCommand
, you should make
sure to store this information in your ServerModel
. If the channel
was successfully created, you should inform only the channel’s creator using the
okay
factory method in Broadcast
. Only the creator
of the channel should be in the set of users receiving the response.
If an error creating the channel arises, you should return a
Broadcast
containing the appropriate error. Use the same
isValidName
method to validate a channel name.
Error conditions for
CreateCommand | |
CHANNEL_ALREADY_EXISTS |
There is already a channel with the desired name. |
INVALID_NAME |
The desired channel name contains illegal characters. |
Adding Users to Channels
If a channel already exists, a client should be able to join it by issuing
a JoinCommand
. If no errors arise, then the client should be
added to the channel, and the users already present in the channel should
be notified that a new user has joined.
How does the new joiner know the names of everyone already in the channel?
This is handled by the names
method in Broadcast
,
which generates an additional protocol message to the client with the
nicknames of the channel’s users. You should consult the
Javadocs for names
for more information about this
method.
In task 5, you will implement invite-only channels, which require users to be
invited by the channel’s owner instead of joining freely. This added feature
means that the code you write now will need to be extended later on. Writing a
clear and well-factored implementation now will make this much
easier. For now, you should ignore the JOIN_PRIVATE_CHANNEL
error.
The recipients for this Broadcast should be all the people in the channel that the user just joined.
Error conditions for JoinCommand | |
NO_SUCH_CHANNEL |
There is no channel with the specified name. |
JOIN_PRIVATE_CHANNEL |
The channel is private, and the user has not been invited. |
What if the client tries to join a channel they are already a part of? The resulting model state should be no different than the original, which means we do not need to protect the server state from this sort of error. In cases like this, it is safe to “ignore” the error and process the command as usual.
Sending Messages to Channels
Finally, we are ready to implement the actual messaging part of the server!
When a user sends a message to a channel, it should be relayed (via an
okay
Broadcast
) to all clients in that channel. You
should keep in mind that no part of the server model needs to change for message
delivery; you should rely on it only for error condition checking.
Error conditions for MessageCommand | |
NO_SUCH_CHANNEL |
There is no channel with the specified name. |
USER_NOT_IN_CHANNEL |
The user is not in the specified channel. |
Users Leaving Channels
At any time, a user may decide to leave a channel by issuing a
LeaveCommand
. This will prevent them from receiving any further
messages from that channel. If the command is okay
, all users in
the channel should be added to the Broadcast
you create. This
includes the user who has left the channel.
Because every channel has exactly one owner (and the owner cannot change), we
specify that if the owner of a channel leaves, every user is to be removed from
the channel, and the channel itself should be destroyed. You should issue a
Broadcast
the same way as before; your handling of the command on
in the ServerModel
is the only place where you will need to account
for this case.
The recipients for this Broadcast should be all the people in the channel that the user just joined.
Error conditions for LeaveCommand | |
NO_SUCH_CHANNEL |
There is no channel with the specified name. |
USER_NOT_IN_CHANNEL |
The user is not in the specified channel. |
ChannelsMessagesTest.java
. The chat client should be mostly
functioning, and you should be able to run multiple instances to test their
interactions.
Testing your Server Using the Client
Running the Client
At this point, your server supports enough functionality to be used as a real
chat server! We’ve provided you with a client application in the file
hw07-client.jar
(which is already included in the project files in Codio and can be downloaded separately if you're using Eclipse).
Running the Client in Codio
You can launch the client using the menu options in Codio. Before launching the client, fire up your server from the menu. You should see a small window pop up which indicates the server is running when you open up the viewer through Codio (similar to the previous homework). Now, launch an instance of the client through the menu option. You can create as many instance of the client as you want by clicking on the menu option multiple times. All clients will appear in the same viewer tab. You will need to repeat this process to test your server with multiple instances of the client application.
Running the Client in Eclipse
You can launch the client by double-clicking the
hw07-client.jar
file. Before launching the client, fire up your server by running theServerMain
class in Eclipse. You should see a small window pop up which indicates the server is running. Now, launch an instance of the client. You can create as many instance of the client as you want by clicking on the menu option multiple times. You will need to repeat this process to test your server with multiple instances of the client application.
Once the client is running, you will be prompted to enter the IP address of the
server. Since the client and the server are running on the same computer (either yours or Codio's virtual computer), the address you
should enter is localhost
. Note that the client will not display
all of the channels on the server—to populate the list on the left-hand side,
you will either have to create or join a channel.
Advice about Debugging
One caveat: although testing different interactions in the client is a good way to test a range of your server’s behaviors, it is not a replacement for writing JUnit tests. There may be (unintentional) bugs in the client application, and it is not possible to exhaustively test all cases using the client UI. If you encounter a bug while using the client, it is best to translate the sequence of steps you followed into a JUnit test. The TAs are not responsible for helping you debug behavior in the client, unless there is a corresponding JUnit test case.
Task 5: Invite-Only Channels
In this task, you will extend the basic functionality of your chat server
with additional features for private, invite-only channels. If your model design
in ServerModel
does not yet include information about whether
a channel is invite-only, you should add that before moving on.
Once your model is updated to take this information into account, you should
go back to your implementation of JoinCommand
and add error
detection for the case of JOIN_PRIVATE_CHANNEL
. As you proceed
through the remaining steps, if you find duplicated functionality or other
suboptimality in your models, you should consider refactoring your
implementation.
Inviting Users to Channels
The InviteCommand
is the equivalent of JoinCommand
for invite-only channels. Users are added directly by the channel’s owner. You
should use the names
method to inform the joinee of the names of
the other users in the channel.
You will notice that there are multiple error messages that might result from
an improper command (e.g., inviting a non-existent user to a public
channel). In such cases, it is acceptable to return a Broadcast
with any one of the appropriate errors.
Error conditions for InviteCommand | |
NO_SUCH_USER |
There is no target user with the specified name. |
INVITE_TO_PUBLIC_CHANNEL |
The specified channel is public. |
NO_SUCH_CHANNEL |
There is no channel with the specified name. |
USER_NOT_OWNER |
The sender is not the owner of the specified channel. |
Kicking Users from Channels
Our final extension will be supporting the KickCommand
, with
which a channel’s owner can remove a user from the channel. This command should
be supported for both public and private channels. Like with the
LeaveCommand
, if the owner kicks themself out of a channel, you
should also remove the channel from your server state entirely.
The recipients for this Broadcast should be all the people in the channel that the user just joined.
Error conditions for KickCommand | |
NO_SUCH_USER |
There is no target user with the specified name. |
USER_NOT_OWNER |
The sender is not the owner of the specified channel |
NO_SUCH_CHANNEL |
There is no channel with the specified name. |
USER_NOT_IN_CHANNEL |
The user is not in the specified channel. |
InviteOnlyTest.java
. Your server should be fully functional, so fire
up two instances of the client application and spend some time playing
around with it. (Make sure to fix any bugs you find!)
Task 6: Refactoring
Now that you have implemented all the required features, it is important to take
some time to look for any redundant data, convoluted functions, or any other
issues that can be refactored into more elegant code. It is very likely that
some aspect of your initial design is not completely optimal, so don’t be afraid
to adjust some data structures, split functions into multiple helper functions,
and so on. Make sure to document any changes you make in your
PLAN
file.
Broadcast toString
It's important to understand the output of the toString method of Broadcast - it returns a list of responses. Each response will have a command attached to it in caps. View ClientCommand.java
for a list of the commands. To help understand this, we've provided a few examples below.
{User0=[:User0 CONNECT]}
means that User0 connected. The key, "User0", is the user who sent the broadcast.
{cis120=[:User0 NICK cis120]}
means User0 changed his nickname to CIS120
{User0=[:User0 ERROR 500]}
indicates an error. Specifically, a NAME_ALREADY_IN_USE
error (refer to ServerError code 500).
Submission and Grading
Before submitting your assignment, you should take one last look through your
PLAN
file to see if there are any questions you haven’t yet
answered, or update your answers if necessary.
Submission Instructions
As with previous assignments, you will be uploading hw07-submit.zip
,
a compressed archive containing only the following files:
PLAN.txt
src/Command.java
src/ServerModel.java
test/ServerModelTest.java
- Any additional classes you made
If you are using Codio
These files should be organized similar to above (with a src and test directory). The easiest option is to use the Zip menu item in Codio. Do not include any of the other provided files, since doing so may cause your submission to fail to compile.
If you are using Eclipse
- Alternative 1 - Zip your files from Eclipse using the instructions below.
- Right click on project, select Export...
- Expand General, select Archive File
- Only select the files listed above (click on arrow in window on left and select files in the window on the right)
- Browse... -> Save as "hw07-submit" in Desktop/Documents/Downloads
- Select "Save in zip format"
- Finish
- Go to submission site, find file in Desktop/Documents/Downloads and then upload
- Alternative 2 - Copy-Paste your code in Codio and zip from there.
Follow these instructions to create and upload hw07-submit.zip:
You have three free submissions for this assignment. Each additional submission will cost you five points from your final score.
Grading Breakdown
- Automated testing (80%)
- Task 3: Connections and Nicknames (30%)
- Task 4: Channels and Messages (25%)
- Task 5: Invite-Only Channels (20%)
- Model state encapsulation (5%)
- Manual grading (20%)
PLAN.txt
(5%)- Style and design (10%)
- Quality of testing (5%)