Added member list support; moved documentation off README and into GoDoc

This commit adds member list support for servers capable of showing
messages. This includes both backend and frontend interfaces.

A UserStatus type was added with the appropriate constants for this purpose,
but it could be used in the future for other purposes.

All cchat documentation has been moved off of the README and into
GoDoc's documentation sections. This is done to free up the README for
other useful information about the project that doesn't have to do with
the code itself.
This commit is contained in:
diamondburned 2020-07-19 10:37:51 -07:00
parent 8827df937d
commit c45d874a80
3 changed files with 306 additions and 315 deletions

304
README.md
View File

@ -1,301 +1,27 @@
# [cchat](https://godoc.org/github.com/diamondburned/cchat)
# [cchat][godoc]
A set of stabilized interfaces for cchat implementations, joining the backend
and frontend together.
Refer to the [GoDoc][godoc] for interfaces and documentations.
[godoc]: https://godoc.org/github.com/diamondburned/cchat
## Backend
## Known implementations
Methods implemented by the backend that have frontend containers as arguments
can do IO. Frontends must NOT rely on individual backend caches and should always
assume that they will block.
The following sections contain known cchat implementations. PRs are welcomed for
more implementations to be added here.
Methods that do not return an error must NOT do any IO to prevent blocking the
main thread. Methods that do return an error may do IO, but they should be
documented per method. `ID()` and `Name()` must never do any IO.
### Backend
Backend implementations have certain conditions that should be adhered to:
- [diamondburned/cchat-mock](https://github.com/diamondburned/cchat-mock)
- A small subset of the cchat backend implementation mocked with fake data
for testing.
- [diamondburned/cchat-discord](https://github.com/diamondburned/cchat-discord)
- A Discord backend implementing cchat interfaces.
- Storing MessagesContainer and ServersContainer are advised against; however,
they should be done if need be.
- Other containers such as LabelContainer and IconContainer should also not be
stored; however, the same rule as above applies.
- For the server list, icon updates and such that happen after their calls
should use `SetServers()`.
- For the nickname of the current server, the backend can store the state of
the label container. It must, however, remove the container when the stop
callback from `JoinServer()` is called.
- Some methods that take in a container may take in a context as well.
Although implementations don't have to use this context, it should try to.
### Frontend
**Note:** IO in most cases usually refer to networking, but they should files and
anything that could block, such as mutexes or semaphores.
- [diamondburned/cchat-gtk](https://github.com/diamondburned/cchat-gtk)
- A GTK+3 implementation of a cchat frontend.
**Note:** As mentioned above, contexts are optional for both the frontend and
backend. The frontend may use it for cancellation, and the backend may ignore
it.
### Service
A service is a complete service that's capable of multiple sessions. It has to
implement the `Authenticate()` method, which returns an implementation of
Authenticator.
A service can implement `SessionRestorer`, which would indicate the frontend
that it can restore past sessions. Sessions are saved using the `SessionSaver`
interface that `Session` can implement.
A service can also implement `Configurator` if it has additional configurations.
The current API is a flat key-value map, which can be parsed by the backend
itself into more meaningful data structures. All configurations must be
optional, as frontends may not implement a configurator UI.
#### Interfaces
- Namer
- SessionRestorer (optional)
- Configurator (optional)
- Icon (optional)
### Authenticator
The authenticator interface allows for a multistage initial authentication API
that the backend could use. Multistage is done by calling `AuthenticateForm`
then `Authenticate` again forever until no errors are returned.
#### Reference Implementation
```go
var s *cchat.Session
var err error
for {
// Pseudo-function to render the form and return the results of those forms
// when the user confirms it.
outputs := renderAuthForm(svc.AuthenticateForm())
s, err = svc.Authenticate(outputs)
if err != nil {
renderError(errors.Wrap(err, "Error while authenticating"))
continue // retry
}
break // success
}
```
### Session
A session is returned after authentication on the service. Session implements
`Name()`, which should return the username most of the time. It also implements
`UserID()`, which might be used by frontends to check against
`MessageAuthor.ID()` and other things.
A session can implement `SessionSaver`, which would allow the frontend to save
the session into its keyring at any time. Whether the keyring is completely
secure or not is up to the frontend. For cchat-gtk, that would be using the
Gnome Keyring daemon.
#### Interfaces
- Identifier
- Namer
- ServerList
- Icon (optional)
- Commander (optional)
- SessionSaver (optional)
### Commander
The commander interface allows the backend to implement custom commands to
easily extend the API.
#### Interfaces
- CommandCompleter (optional)
### Identifier
The identifier interface forces whatever interface that embeds this one to be
uniquely identifiable.
### Namer
The namer interface forces whatever interface that embeds it to have an ideally
human-friendly name. This is typically a username or a service name.
### Configurator
The configurator interface is a way for the frontend to display configuration
options that the backend has.
### Server
A server is any entity that is usually a channel or a guild.
#### Interfaces
- Identifier
- Namer
- ServerList and/or ServerMessage
- ServerNickname (optional)
- Icon (optional)
### ServerMessage
A server message is an entity that contains messages to be displayed. An example
would be channels in Discord and IRC.
#### Interfaces
- ServerMessageSender (optional): adds message sending capability.
- ServerMessageSendCompleter (optional): adds message input completion capability.
- ServerMessageAttachmentSender (optional): adds attachment sending capability
- ServerMessageEditor (optional): adds message editing capability.
- ServerMessageActioner (optional): adds custom actions capability.
- ServerMessageUnreadIndicator (optional): adds unread indication capability.
- ServerMessageTypingIndicator (optional): adds typing indication capability.
### Messages
#### Interfaces
- MessageHeader: the minimum for a proper message.
- MessageCreate or MessageUpdate or MessageDelete
- MessageNonce (optional)
- MessageMentionable (optional)
### MessageAuthor
MessageAuthor is the interface that a message author would implement. ID would
typically return the user ID, or the username if that's the unique identifier.
#### Interfaces
- MessageAuthorAvatar (optional)
## Frontend
Frontend contains all interfaces that a frontend can or must implement. The
backend may call these methods any time from any goroutine. Thus, they should
be thread-safe. They should also not block the call by doing so, as backends
may call these methods in its own main thread.
It is worth pointing out that frontend container interfaces will not have an
error handling API, as frontends can do that themselves. Errors returned by
backend methods will be errors from the backend itself and never the frontend
errors.
### LabelContainer
A label container is a generic abstraction for any container that can hold
texts. It's typically used for labels that can update itself, such as usernames.
### IconContainer
The icon container is similar to the label container. Refer to above.
### RoundIconContainer
Similar to IconContainer, but contains images with rounded corners.
### ServersContainer
A servers container is any type of view that displays the list of servers. It
should implement a `SetServers([]Server)` that the backend could use to call
anytime the server list changes (at all).
Typically, most frontend should implement this interface onto a tree node
instead of a tree view, as servers can be infinitely nested.
This interface expects the frontend to handle its own errors.
### MessagesContainer
A messages container is a view implementation that displays a list of messages
live. This implements the 3 most common message events: `CreateMessage`,
`UpdateMessage` and `DeleteMessage`. The frontend must handle all 3.
Since this container interface extends a single Server, the frontend is allowed
to have multiple views. This is usually done with tabs or splits, but the
backend should update them all nonetheless.
### SendableMessage
The frontend can make its own send message data implementation to indicate extra
capabilities that the backend may want.
An example of this is `MessageNonce`, which is similar to IRCv3's [labeled
response extension](https://ircv3.net/specs/extensions/labeled-response).
The frontend could implement this interface and check if incoming
`MessageCreate` events implement the same interface.
#### Interfaces (only known)
- MessageNonce (optional)
- SendableMessageAttachments (optional): adds attachments into the message
### UnreadIndicator
A single server container (such as a button or a tree node) can implement this
interface if it's capable of indicating the read and mentioned status for that
channel.
Server containers that implement this has to implement both `SetUnread` and
`SetMentioned`, and they should also represent those statuses differently. For
example, a mentioned channel could have a red outline, while an unread channel
could appear brighter.
Server containers are expected to represent this information in their parent
nodes as well. For example, if a server is unread, then its parent servers as
well as the session node should indicate the same status. Highlighting the
session and service nodes are, however, implementation details, meaning that
this decision is up to the frontend to decide.
### TypingIndicator
The frontend can arbitrarily implement this on any of their containers, which
would add typing indicator capability. This is similar to Discord's and IRCv3's.
For more information, refer to the documentation for TypingIndicator and
ServerMessageTypingIndicator in GoDoc.

219
cchat.go
View File

@ -1,7 +1,49 @@
// Package cchat is a set of stabilized interfaces for cchat implementations,
// joining the backend and frontend together.
//
// For detailed explanations, refer to the README.
// Backend
//
// Methods implemented by the backend that have frontend containers as arguments
// can do IO. Frontends must NOT rely on individual backend caches and should
// always assume that they will block.
//
// Methods that do not return an error must NOT do any IO to prevent blocking
// the main thread. Methods that do return an error may do IO, but they should
// be documented per method. ID() and Name() must never do any IO.
//
// Backend implementations have certain conditions that should be adhered to:
//
// - Storing MessagesContainer and ServersContainer are advised against;
// however, they should be done if need be.
// - Other containers such as LabelContainer and IconContainer should also
// not be stored; however, the same rule as above applies.
// - For the server list, icon updates and such that happen after their calls
// should use SetServers().
// - For the nickname of the current server, the backend can store the state
// of the label container. It must, however, remove the container when the
// stop callback from JoinServer() is called.
// - Some methods that take in a container may take in a context as well.
// Although implementations don't have to use this context, it should try to.
//
// Note: IO in most cases usually refer to networking, but they should files and
// anything that could block, such as mutexes or semaphores.
//
// Note: As mentioned above, contexts are optional for both the frontend and
// backend. The frontend may use it for cancellation, and the backend may ignore
// it.
//
// Frontend
//
// Frontend contains all interfaces that a frontend can or must implement. The
// backend may call these methods any time from any goroutine. Thus, they should
// be thread-safe. They should also not block the call by doing so, as backends
// may call these methods in its own main thread.
//
// It is worth pointing out that frontend container interfaces will not have an
// error handling API, as frontends can do that themselves. Errors returned by
// backend methods will be errors from the backend itself and never the frontend
// errors.
//
package cchat
import (
@ -12,8 +54,27 @@ import (
"github.com/diamondburned/cchat/text"
)
// Service contains the bare minimum set of interface that a backend has to
// implement. Core can also implement Authenticator.
// A service is a complete service that's capable of multiple sessions. It has
// to implement the Authenticate() method, which returns an implementation of
// Authenticator.
//
// A service can implement SessionRestorer, which would indicate the frontend
// that it can restore past sessions. Sessions are saved using the SessionSaver
// interface that Session can implement.
//
// A service can also implement Configurator if it has additional
// configurations. The current API is a flat key-value map, which can be parsed
// by the backend itself into more meaningful data structures. All
// configurations must be optional, as frontends may not implement a
// configurator UI.
//
// Service can implement the following interfaces:
//
// - Namer
// - SessionRestorer (optional)
// - Configurator (optional)
// - Icon (optional)
//
type Service interface {
// Namer returns the name of the service.
Namer
@ -54,17 +115,28 @@ func (err *ErrInvalidConfigAtField) Unwrap() error {
return err.Err
}
// Authenticator is what the backend can implement for authentication. A typical
// authentication frontend implementation would look like this:
// The authenticator interface allows for a multistage initial authentication
// API that the backend could use. Multistage is done by calling
// AuthenticateForm then Authenticate again forever until no errors are
// returned.
//
// var s *cchat.Session
// var err error
//
// for {
// // Pseudo-function to render the form and return the results of those forms
// // when the user confirms it.
// outputs := renderAuthForm(svc.AuthenticateForm())
// if err := svc.Authenticate(outputs); err != nil {
// log.Println("Error while authenticating:", err)
//
// s, err = svc.Authenticate(outputs)
// if err != nil {
// renderError(errors.Wrap(err, "Error while authenticating"))
// continue // retry
// }
//
// break // success
// }
//
type Authenticator interface {
// AuthenticateForm should return a list of authentication entries for
// the frontend to render.
@ -84,7 +156,9 @@ type AuthenticateEntry struct {
}
// Identifier requires ID() to return a uniquely identifiable string for
// whatever this is embedded into. Typically, servers and messages have IDs.
// whatever this is embedded into. Typically, servers and messages have IDs. It
// is worth mentioning that IDs should be consistent throughout the lifespan of
// the program or maybe even forever.
type Identifier interface {
ID() string
}
@ -95,8 +169,25 @@ type Namer interface {
Name() text.Rich
}
// Service contains the bare minimum set of interface that a backend has to
// implement. Core can also implement Authenticator.
// A session is returned after authentication on the service. Session implements
// Name(), which should return the username most of the time. It also implements
// ID(), which might be used by frontends to check against MessageAuthor.ID()
// and other things.
//
// A session can implement SessionSaver, which would allow the frontend to save
// the session into its keyring at any time. Whether the keyring is completely
// secure or not is up to the frontend. For a Gtk client, that would be using
// the GNOME Keyring daemon.
//
// Session can implement the following interfaces:
//
// - Identifier
// - Namer
// - ServerList
// - Icon (optional)
// - Commander (optional)
// - SessionSaver (optional)
//
type Session interface {
// Identifier should typically return the user ID.
Identifier
@ -133,6 +224,11 @@ type SessionSaver interface {
// Commander is an optional interface that a session could implement for command
// support. This is different from just intercepting the SendMessage() API, as
// this extends globally to the entire session.
//
// Commander can implement the following interfaces:
//
// - CommandCompleter (optional)
//
type Commander interface {
// RunCommand executes the given command, with the slice being already split
// arguments, similar to os.Args. The function could return an output
@ -159,6 +255,15 @@ type CommandCompleter interface {
// Server is a single server-like entity that could translate to a guild, a
// channel, a chat-room, and such. A server must implement at least ServerList
// or ServerMessage, else the frontend must treat it as a no-op.
//
// Server can implement the following interfaces:
//
// - Identifier
// - Namer
// - ServerList and/or ServerMessage (and its interfaces)
// - ServerNickname (optional)
// - Icon (optional)
//
type Server interface {
Identifier
Namer
@ -203,10 +308,25 @@ type ServerList interface {
// ServerMessage is for servers that contain messages. This is similar to
// Discord or IRC channels.
//
// ServerMessage can implement the following interfaces:
//
// - ServerMessageSender (optional): adds message sending capability.
// - ServerMessageSendCompleter (optional): adds message input completion
// capability.
// - ServerMessageAttachmentSender (optional): adds attachment sending
// capability.
// - ServerMessageEditor (optional): adds message editing capability.
// - ServerMessageActioner (optional): adds custom actions capability.
// - ServerMessageUnreadIndicator (optional): adds unread indication
// capability.
// - ServerMessageTypingIndicator (optional): adds typing indication
// capability.
// - ServerMessageMemberLister (optional): adds member listing capability.
//
type ServerMessage interface {
// JoinServer should be called if Servers() returns nil, in which the
// backend should connect to the server and start calling methods in the
// container.
// JoinServer joins a server that's capable of receiving messages. The
// server may not necessarily support sending messages.
JoinServer(context.Context, MessagesContainer) (stop func(), err error)
}
@ -342,7 +462,78 @@ type CompletionEntry struct {
Image bool
}
// MessageHeader implements the interface for any message event.
// ServerMessageMemberLister optionally extends ServerMessage to add a member
// list into each channel. This function works similarly to ServerMessage's
// JoinServer.
type ServerMessageMemberLister interface {
// ListMembers assigns the given container to the channel's member list.
// The given context may be used to provide HTTP request cancellations, but
// frontends must not rely solely on this, as the general context rules
// applies.
ListMembers(context.Context, MemberListContainer) (stop func(), err error)
}
// UserStatus represents a user's status. This might be used by the frontend to
// visually display the status.
type UserStatus uint8
const (
UnknownStatus UserStatus = iota
OnlineStatus
IdleStatus
BusyStatus // also known as Do Not Disturb
AwayStatus
OfflineStatus
InvisibleStatus // reserved; currently unused
)
// String formats a user status as a title string, such as "Online" or
// "Unknown". It treats unknown constants as UnknownStatus.
func (s UserStatus) String() string {
switch s {
case OnlineStatus:
return "Online"
case IdleStatus:
return "Idle"
case BusyStatus:
return "Busy"
case AwayStatus:
return "Away"
case OfflineStatus:
return "Offline"
case InvisibleStatus:
return "Invisible"
case UnknownStatus:
fallthrough
default:
return "Unknown"
}
}
// ListMember represents a single member in the member list. This is a base
// interface that may implement more interfaces, such as Iconer for the user's
// avatar. The frontend may give everyone an avatar regardless, or it may not
// show any avatars at all.
//
// This interface works similarly to a slightly extended MessageAuthor
// interface.
type ListMember interface {
// Identifier identifies the individual member. This works similarly to
// MessageAuthor.
Identifier
// Namer returns the name of the member. This works similarly to a
// MessageAuthor.
Namer
// Status returns the status of the member. The backend does not have to
// show offline members with the offline status if it doesn't want to show
// offline menbers at all.
Status() UserStatus
// Secondary returns the subtext of this member. This could be anything,
// such as a user's custom status or away reason.
Secondary() text.Rich
}
// MessageHeader implements the minimum interface for any message event.
type MessageHeader interface {
Identifier
Time() time.Time

View File

@ -6,10 +6,13 @@ import (
"github.com/diamondburned/cchat/text"
)
// ServersContainer is a frontend implementation for a server view, with
// synchronous callbacks to render those events. The frontend is typically
// expected to reset the entire list, but it can do so with or without deleting
// everything and starting all over again.
// ServersContainer is any type of view that displays the list of servers. It
// should implement a SetServers([]Server) that the backend could use to call
// anytime the server list changes (at all).
//
// Typically, most frontends should implement this interface onto a tree node,
// as servers can be infinitely nested. Frontends should also reset the entire
// node and its children when SetServers is called again.
type ServersContainer interface {
// SetServer is called by the backend service to request a reset of the
// server list. The frontend can choose to call Servers() on each of the
@ -18,8 +21,13 @@ type ServersContainer interface {
SetServers([]Server)
}
// MessagesContainer is a frontend implementation for a message view, with
// thread-safe callbacks to render those events.
// MessagesContainer is a view implementation that displays a list of messages
// live. This implements the 3 most common message events: CreateMessage,
// UpdateMessage and DeleteMessage. The frontend must handle all 3.
//
// Since this container interface extends a single Server, the frontend is
// allowed to have multiple views. This is usually done with tabs or splits, but
// the backend should update them all nonetheless.
type MessagesContainer interface {
CreateMessage(MessageCreate)
UpdateMessage(MessageUpdate)
@ -53,13 +61,20 @@ type ImageContainer interface {
SetImage(url string)
}
// UnreadIndicator is a generic interface for any container that can have
// different styles to indicate an unread and/or mentioned server.
// UnreadIndicator is an interface that a single server container (such as a
// button or a tree node) can implement if it's capable of indicating the read
// and mentioned status for that channel.
//
// Servers that have this highlighted must traverse up the tree and highlight
// their parent servers too, if needed.
// Server containers that implement this has to implement both SetUnread and
// SetMentioned, and they should also represent those statuses differently. For
// example, a mentioned channel could have a red outline, while an unread
// channel could appear brighter.
//
// Methods that have this interface as its arguments can do IO.
// Server containers are expected to represent this information in their parent
// nodes as well. For example, if a server is unread, then its parent servers as
// well as the session node should indicate the same status. Highlighting the
// session and service nodes are, however, implementation details, meaning that
// this decision is up to the frontend to decide.
type UnreadIndicator interface {
// Unread sets the container's unread state to the given boolean. The
// frontend may choose how to represent this.
@ -83,8 +98,67 @@ type TypingIndicator interface {
RemoveTyper(id string)
}
// MemberListContainer is a generic interface for any container that can display
// a member list. This is similar to Discord's right-side member list or IRC's
// users list. Below is a visual representation of a typical member list
// container:
//
// +-MemberList-----------\
// | +-Section------------|
// | | |
// | | Header - Total |
// | | |
// | | +-Member-----------|
// | | | Name |
// | | | Secondary |
// | | \__________________|
// | | |
// | | +-Member-----------|
// | | | Name |
// | | | Secondary |
// | | \__________________|
// \_\____________________/
//
type MemberListContainer interface {
// SetSections (re)sets the list of sections to be the given slice. Members
// from the old section list should be transferred over to the new section
// entry if the section name's content is the same. Old sections that don't
// appear in the new slice should be removed.
SetSections(sections []MemberListSection)
// SetMember adds or updates (or upsert) a member into a section. This
// operation must not change the section's member count. As such, changes
// should be done separately in SetSection. If the section does not exist,
// then the client should ignore this member. As such, backends must call
// SetSections first before SetMember on a new section.
SetMember(sectionContent string, member ListMember)
// RemoveMember removes a member from a section. If neither the member nor
// the section exists, then the client should ignore it.
RemoveMember(sectionContent string, id string)
}
// MemberListSection represents a member list section. The section name's
// content must be unique among other sections from the same list regardless of
// the rich segments.
type MemberListSection interface {
// Name returns the section name.
Name() text.Rich
// Total returns the total member count.
Total() int
}
// SendableMessage is the bare minimum interface of a sendable message, that is,
// a message that can be sent with SendMessage().
// a message that can be sent with SendMessage(). This allows the frontend to
// implement its own message data implementation.
//
// An example of extending this interface is MessageNonce, which is similar to
// IRCv3's labeled response extension or Discord's nonces. The frontend could
// implement this interface and check if incoming MessageCreate events implement
// the same interface.
//
// SendableMessage can implement the following interfaces:
//
// - MessageNonce (optional)
// - SendableMessageAttachments (optional): refer to ServerMessageAttachmentSender
type SendableMessage interface {
Content() string
}