diff --git a/README.md b/README.md index 00502ae..695e393 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cchat.go b/cchat.go index b301417..a6eb339 100644 --- a/cchat.go +++ b/cchat.go @@ -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 diff --git a/cchat_frontend.go b/cchat_frontend.go index 8f4886c..66216ca 100644 --- a/cchat_frontend.go +++ b/cchat_frontend.go @@ -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 }