601 lines
22 KiB
Go
601 lines
22 KiB
Go
// Package cchat is a set of stabilized interfaces for cchat implementations,
|
|
// joining the backend and frontend together.
|
|
//
|
|
// 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 (
|
|
"context"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/diamondburned/cchat/text"
|
|
)
|
|
|
|
// 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
|
|
// Authenticate begins the authentication process. It's put into a method so
|
|
// backends can easily restart the entire process.
|
|
Authenticate() Authenticator
|
|
}
|
|
|
|
// SessionRestorer extends Service and is called by the frontend to restore a
|
|
// saved session. The frontend may call this at any time, but it's usually on
|
|
// startup.
|
|
//
|
|
// To save a session, refer to SessionSaver which extends Session.
|
|
type SessionRestorer interface {
|
|
RestoreSession(map[string]string) (Session, error)
|
|
}
|
|
|
|
// Configurator is what the backend can implement for an arbitrary configuration
|
|
// API.
|
|
type Configurator interface {
|
|
Configuration() (map[string]string, error)
|
|
SetConfiguration(map[string]string) error
|
|
}
|
|
|
|
// ErrInvalidConfigAtField is the structure for an error at a specific
|
|
// configuration field. Frontends can use this and highlight fields if the
|
|
// backends support it.
|
|
type ErrInvalidConfigAtField struct {
|
|
Key string
|
|
Err error
|
|
}
|
|
|
|
func (err *ErrInvalidConfigAtField) Error() string {
|
|
return "Error at " + err.Key + ": " + err.Err.Error()
|
|
}
|
|
|
|
func (err *ErrInvalidConfigAtField) Unwrap() error {
|
|
return err.Err
|
|
}
|
|
|
|
// 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())
|
|
//
|
|
// 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.
|
|
AuthenticateForm() []AuthenticateEntry
|
|
// Authenticate will be called with a list of values with indices
|
|
// correspond to the returned slice of AuthenticateEntry.
|
|
Authenticate([]string) (Session, error)
|
|
}
|
|
|
|
// AuthenticateEntry represents a single authentication entry, usually an email
|
|
// or password prompt. Passwords or similar entries should have Secrets set to
|
|
// true, which should imply to frontends that the fields be masked.
|
|
type AuthenticateEntry struct {
|
|
Name string
|
|
Secret bool
|
|
Multiline bool
|
|
}
|
|
|
|
// Identifier requires ID() to return a uniquely identifiable string for
|
|
// 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
|
|
}
|
|
|
|
// Namer requires Name() to return the name of the object. Typically, this
|
|
// implies usernames for sessions or service names for services.
|
|
type Namer interface {
|
|
Name() text.Rich
|
|
}
|
|
|
|
// 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
|
|
// Namer gives the name of the session, which is typically the username.
|
|
Namer
|
|
|
|
// Disconnect asks the service to disconnect. It does not necessarily mean
|
|
// removing the service.
|
|
//
|
|
// The frontend must cancel the active ServerMessage before disconnecting.
|
|
// The backend can rely on this behavior.
|
|
//
|
|
// The frontend will reuse the stored session data from SessionSaver to
|
|
// reconnect.
|
|
//
|
|
// When this function fails, the frontend may display the error upfront.
|
|
// However, it will treat the session as actually disconnected. If needed,
|
|
// the backend must implement reconnection by itself.
|
|
Disconnect() error
|
|
|
|
ServerList
|
|
}
|
|
|
|
// SessionSaver extends Session and is called by the frontend to save the
|
|
// current session. This is typically called right after authentication, but a
|
|
// frontend may call this any time, including when it's closing.
|
|
//
|
|
// The frontend can ask to restore a session using SessionRestorer, which
|
|
// extends Service.
|
|
type SessionSaver interface {
|
|
Save() (map[string]string, error)
|
|
}
|
|
|
|
// 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
|
|
// stream, in which the frontend must display it live and close it on EOF.
|
|
//
|
|
// The function must not do any IO; if it does, then they have to be in a
|
|
// goroutine and stream their results to the ReadCloser.
|
|
//
|
|
// The client should make guarantees that an empty string (and thus a
|
|
// zero-length string slice) should be ignored. The backend should be able
|
|
// to assume that the argument slice is always length 1 or more.
|
|
RunCommand([]string) (io.ReadCloser, error)
|
|
}
|
|
|
|
// CommandCompleter is an optional interface that a session could implement for
|
|
// completion support. This also depends on whether or not the frontend supports
|
|
// it.
|
|
type CommandCompleter interface {
|
|
// CompleteCommand is called with the line and current word, which the
|
|
// backend should return with a list of new words.
|
|
CompleteCommand(words []string, wordIndex int) []string
|
|
}
|
|
|
|
// 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
|
|
|
|
// Implement ServerList and/or ServerMessage.
|
|
}
|
|
|
|
// ServerNickname extends Server to add a specific user nickname into a server.
|
|
// The frontend should not traverse up the server tree, and thus the backend
|
|
// must handle nickname inheritance. This also means that servers that don't
|
|
// implement ServerMessage also don't need to implement ServerNickname. By
|
|
// default, the session name should be used.
|
|
type ServerNickname interface {
|
|
Nickname(context.Context, LabelContainer) (stop func(), err error)
|
|
}
|
|
|
|
// Icon is an extra interface that an interface could implement for an icon.
|
|
// Typically, Service would return the service logo, Session would return the
|
|
// user's avatar, and Server would return the server icon.
|
|
//
|
|
// For session, the avatar should be the same as the one returned by messages
|
|
// sent by the current user.
|
|
type Icon interface {
|
|
Icon(context.Context, IconContainer) (stop func(), err error)
|
|
}
|
|
|
|
// ServerList is for servers that contain children servers. This is similar to
|
|
// guilds containing channels in Discord, or IRC servers containing channels.
|
|
//
|
|
// There isn't a similar stop callback API unlike other interfaces because all
|
|
// servers are expected to be listed. However, they could be hidden, such as
|
|
// collapsing a tree.
|
|
//
|
|
// The backend should call both the container and other icon and label
|
|
// containers, if any.
|
|
type ServerList interface {
|
|
// Servers should call SetServers() on the given ServersContainer to render
|
|
// all servers. This function can do IO, and the frontend should run this in
|
|
// a goroutine.
|
|
Servers(ServersContainer) error
|
|
}
|
|
|
|
// 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 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)
|
|
}
|
|
|
|
// ServerMessageUnreadIndicator is for servers that can contain messages and
|
|
// know from the state if that message makes the server unread and if it
|
|
// contains a message that mentions the user.
|
|
type ServerMessageUnreadIndicator interface {
|
|
// UnreadIndicate subscribes the given unread indicator for unread and
|
|
// mention events. Examples include when a new message is arrived and the
|
|
// backend needs to indicate that it's unread.
|
|
//
|
|
// This function must provide a way to remove callbacks, as clients must
|
|
// call this when the old server is destroyed, such as when Servers is
|
|
// called.
|
|
UnreadIndicate(UnreadIndicator) (stop func(), err error)
|
|
}
|
|
|
|
// ServerMessageSender optionally extends ServerMessage to add message sending
|
|
// capability to the server.
|
|
type ServerMessageSender interface {
|
|
// SendMessage is called by the frontend to send a message to this channel.
|
|
SendMessage(SendableMessage) error
|
|
}
|
|
|
|
// ServerMessageAttachmentSender optionally extends ServerMessageSender to
|
|
// indicate that the backend can accept attachments in its messages. The
|
|
// attachments will still be sent through SendMessage, though this interface
|
|
// will mostly be used to indicate the capability.
|
|
type ServerMessageAttachmentSender interface {
|
|
ServerMessageSender
|
|
// SendAttachments sends only message attachments. The frontend would
|
|
// most of the time use SendableMessage that implements
|
|
// SendableMessageAttachments, but this method is useful for detecting
|
|
// capabilities.
|
|
SendAttachments([]MessageAttachment) error
|
|
}
|
|
|
|
// ServerMessageEditor optionally extends ServerMessage to add message editing
|
|
// capability to the server. Only EditMessage can have IO.
|
|
type ServerMessageEditor interface {
|
|
// MessageEditable returns whether or not a message can be edited by the
|
|
// client.
|
|
MessageEditable(id string) bool
|
|
// RawMessageContent gets the original message text for editing. Backends
|
|
// must not do IO.
|
|
RawMessageContent(id string) (string, error)
|
|
// EditMessage edits the message with the given ID to the given content,
|
|
// which is the edited string from RawMessageContent. This method can do IO.
|
|
EditMessage(id, content string) error
|
|
}
|
|
|
|
// ServerMessageActioner optionally extends ServerMessage to add custom message
|
|
// action capabilities to the server. Similarly to ServerMessageEditor, these
|
|
// functions can have IO.
|
|
type ServerMessageActioner interface {
|
|
// MessageActions returns a list of possible actions in pretty strings that
|
|
// the frontend will use to directly display. This function must not do any
|
|
// IO.
|
|
//
|
|
// The string slice returned can be nil or empty.
|
|
MessageActions(messageID string) []string
|
|
// DoMessageAction executes a message action on the given messageID, which
|
|
// would be taken from MessageHeader.ID(). This function is allowed to do
|
|
// IO; the frontend should take care of running this asynchronously.
|
|
DoMessageAction(action, messageID string) error
|
|
}
|
|
|
|
// ServerMessageTypingIndicator optionally extends ServerMessage to provide
|
|
// bidirectional typing indicating capabilities. This is similar to typing
|
|
// events on Discord and typing client tags on IRCv3.
|
|
//
|
|
// The client should remove a typer when a message is received with the same
|
|
// user ID, when RemoveTyper() is called by the backend or when the timeout
|
|
// returned from TypingTimeout() has been reached.
|
|
type ServerMessageTypingIndicator interface {
|
|
// Typing is called by the client to indicate that the user is typing. This
|
|
// function can do IO calls, and the client will take care of calling it in
|
|
// a goroutine (or an asynchronous queue) as well as throttling it to
|
|
// TypingTimeout.
|
|
Typing() error
|
|
// TypingTimeout returns the interval between typing events sent by the
|
|
// client as well as the timeout before the client should remove the typer.
|
|
// Typically, a constant should be returned.
|
|
TypingTimeout() time.Duration
|
|
// TypingSubscribe subscribes the given indicator to typing events sent by
|
|
// the backend. The added event handlers have to be removed by the backend
|
|
// when the stop() callback is called.
|
|
//
|
|
// This method does not take in a context, as it's supposed to only use
|
|
// event handlers and not do any IO calls. Nonetheless, the client must
|
|
// treat it like it does and call it asynchronously.
|
|
TypingSubscribe(TypingIndicator) (stop func(), err error)
|
|
}
|
|
|
|
// Typer is an individual user that's typing. This interface is used
|
|
// interchangably in TypingIndicator and thus ServerMessageTypingIndicator as
|
|
// well.
|
|
type Typer interface {
|
|
MessageAuthor
|
|
Time() time.Time
|
|
}
|
|
|
|
// ServerMessageSendCompleter optionally extends ServerMessageSender to add
|
|
// autocompletion into the message composer. IO is not allowed and the backend
|
|
// should do that only in goroutines and update its state for future calls.
|
|
//
|
|
// Frontends could utilize the split package inside utils for splitting words
|
|
// and index.
|
|
type ServerMessageSendCompleter interface {
|
|
// CompleteMessage returns the list of possible completion entries for the
|
|
// given word list and the current word index. It takes in a list of
|
|
// whitespace-split slice of string as well as the position of the cursor
|
|
// relative to the given string slice.
|
|
CompleteMessage(words []string, current int) []CompletionEntry
|
|
}
|
|
|
|
// CompletionEntry is a single completion entry returned by CompleteMessage. The
|
|
// icon URL field is optional.
|
|
type CompletionEntry struct {
|
|
// Raw is the text to be replaced in the input box.
|
|
Raw string
|
|
// Text is the label to be displayed.
|
|
Text text.Rich
|
|
// Secondary is the label to be displayed on the second line, on the right
|
|
// of Text, or not displayed at all. This should be optional. This text may
|
|
// be dimmed out as styling.
|
|
Secondary text.Rich
|
|
// IconURL is the URL to the icon that will be displayed on the left of the
|
|
// text. This field is optional.
|
|
IconURL string
|
|
// Image returns whether or not the icon URL is actually an image, which
|
|
// indicates that the frontend should not do rounded corners.
|
|
Image bool
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// MessageCreate is the interface for an incoming message.
|
|
type MessageCreate interface {
|
|
MessageHeader
|
|
Author() MessageAuthor
|
|
Content() text.Rich
|
|
}
|
|
|
|
// MessageUpdate is the interface for a message update (or edit) event. If the
|
|
// returned text.Rich returns true for Empty(), then the element shouldn't be
|
|
// changed.
|
|
type MessageUpdate interface {
|
|
MessageHeader
|
|
Author() MessageAuthor // optional (nilable)
|
|
Content() text.Rich // optional (rich.Content == "")
|
|
}
|
|
|
|
// MessageAuthor is the interface for an identifiable message author. The
|
|
// returned ID may or may not be used by the frontend, but clients must
|
|
// guarantee uniqueness for intended behaviors.
|
|
//
|
|
// The frontend may also use this to squash messages with the same author
|
|
// together.
|
|
type MessageAuthor interface {
|
|
Identifier
|
|
Namer
|
|
}
|
|
|
|
// MessageAuthorAvatar is an optional interface that MessageAuthor could
|
|
// implement. A frontend may optionally support this. A backend may return an
|
|
// empty string, in which the frontend must handle, perhaps by using a
|
|
// placeholder.
|
|
type MessageAuthorAvatar interface {
|
|
Avatar() (url string)
|
|
}
|
|
|
|
// MessageDelete is the interface for a message delete event.
|
|
type MessageDelete interface {
|
|
MessageHeader
|
|
}
|
|
|
|
// MessageNonce extends SendableMessage and MessageCreate to add nonce support.
|
|
// This is known in IRC as labeled responses. Clients could use this for
|
|
// various purposes, including knowing when a message has been sent
|
|
// successfully.
|
|
//
|
|
// Both the backend and frontend must implement this for the feature to work
|
|
// properly. The backend must check if SendableMessage implements MessageNonce,
|
|
// and the frontend must check if MessageCreate implements MessageNonce.
|
|
type MessageNonce interface {
|
|
Nonce() string
|
|
}
|
|
|
|
// MessageMentioned extends MessageCreate to add mentioning support. The
|
|
// frontend may or may not implement this. If it does, the frontend will
|
|
// typically format the message into a notification and play a sound.
|
|
type MessageMentioned interface {
|
|
// Mentioned returns whether or not the message mentions the current user.
|
|
Mentioned() bool
|
|
}
|