diff --git a/docs/API.md b/docs/API.md index b0872978..d4d85878 100644 --- a/docs/API.md +++ b/docs/API.md @@ -55,6 +55,31 @@ The response is a JSON object with fields: * `success`: whether the credentials provided were valid * `accountName`: canonical, case-unfolded version of the account name +`/v1/list` +---------- + +This endpoint returns a list of channels that exist on the network. The request body is ignored and can be empty. + +The response is a JSON object with fields: + +* `success`: whether the request was successful +* `channels`: a list of channel objects, as described below + +Each channel object has fields: + +* `name`: canonical name of the channel without case-normalization +* `hasKey`: boolean, whether the channel has a key set with the `+k` mode +* `inviteOnly`: boolean, whether the channel has the `+i` invite-only mode set +* `secret`: boolean, whether the channel has the `+s` secret mode set (and would be hidden from an unprivileged `LIST` command) +* `userCount`: int, number of users in the channel +* `topic`: string, channel topic +* `topicSetAt`: string, time the topic was last updated (in ISO8601 format) +* `createdAt`: string, time the channel was created (in ISO8601 format) +* `registered`: boolean, whether the channel is registered +* `owner`: string, account name of the registered owner if the channel is registered +* `registeredAt`: string, registration date/time of the channel (in ISO8601 format) if it is registered + + `/v1/ns/info` ------------- diff --git a/irc/api.go b/irc/api.go index 84063620..baf489cc 100644 --- a/irc/api.go +++ b/irc/api.go @@ -8,6 +8,7 @@ import ( "runtime" "strings" + "github.com/ergochat/ergo/irc/modes" "github.com/ergochat/ergo/irc/utils" ) @@ -20,6 +21,7 @@ func newAPIHandler(server *Server) http.Handler { // server-level functionality: api.mux.HandleFunc("POST /v1/rehash", api.handleRehash) api.mux.HandleFunc("POST /v1/status", api.handleStatus) + api.mux.HandleFunc("POST /v1/list", api.handleList) // use Ergo as a source of truth for authentication in other services: api.mux.HandleFunc("POST /v1/check_auth", api.handleCheckAuth) @@ -347,3 +349,57 @@ func (a *ergoAPI) handleStatus(w http.ResponseWriter, r *http.Request) { a.writeJSONResponse(response, w, r) } + +type apiChannelData struct { + Name string `json:"name"` + HasKey bool `json:"hasKey"` + InviteOnly bool `json:"inviteOnly"` + Secret bool `json:"secret"` + UserCount int `json:"userCount"` + Topic string `json:"topic"` + TopicSetAt string `json:"topicSetAt,omitempty"` + CreatedAt string `json:"createdAt"` + Registered bool `json:"registered"` + Owner string `json:"owner,omitempty"` + RegisteredAt string `json:"registeredAt,omitempty"` +} + +func (channel *Channel) apiData() (result apiChannelData) { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + result.Name = channel.name + result.HasKey = channel.key != "" + result.InviteOnly = channel.flags.HasMode(modes.InviteOnly) + result.Secret = channel.flags.HasMode(modes.Secret) + result.UserCount = len(channel.members) + result.Topic = channel.topic + if !channel.topicSetTime.IsZero() { + result.TopicSetAt = channel.topicSetTime.UTC().Format(utils.IRCv3TimestampFormat) + } + result.CreatedAt = channel.createdTime.UTC().Format(utils.IRCv3TimestampFormat) + result.Registered = channel.registeredFounder != "" + if result.Registered { + result.Owner = channel.registeredFounder + if !channel.registeredTime.IsZero() { + result.RegisteredAt = channel.registeredTime.UTC().Format(utils.IRCv3TimestampFormat) + } + } + return +} + +type apiListResponse struct { + apiGenericResponse + Channels []apiChannelData `json:"channels"` +} + +func (a *ergoAPI) handleList(w http.ResponseWriter, r *http.Request) { + channels := a.server.channels.ListableChannels() + response := apiListResponse{ + apiGenericResponse: apiGenericResponse{Success: true}, + Channels: make([]apiChannelData, 0, len(channels)), + } + for _, channel := range channels { + response.Channels = append(response.Channels, channel.apiData()) + } + a.writeJSONResponse(response, w, r) +}