diff --git a/docs/API.md b/docs/API.md index 056f88c8..524e8077 100644 --- a/docs/API.md +++ b/docs/API.md @@ -51,6 +51,8 @@ The response is a JSON object with fields: * `success`: whether the account exists or not * `accountName`: canonical, case-unfolded version of the account name * `email`: email address of the account provided +* `registeredAt`: string, registration date/time of the account (in ISO8601 format) +* `channels`: array of strings, list of channels the account is registered on or associated with `/v1/check_auth` ---------------- @@ -86,3 +88,37 @@ The response is a JSON object with fields: * `success`: whether the account creation succeeded * `errorCode`: string, optional, machine-readable description of the error. Possible values include: `ACCOUNT_EXISTS`, `INVALID_PASSPHRASE`, `UNKNOWN_ERROR`. * `error`: string, optional, human-readable description of the failure. + +`/v1/account_list` +------------------- + +This endpoint fetches a list of all accounts. The request body is ignored and can be empty. + +The response is a JSON object with fields: + +* `success`: whether the request succeeded +* `accounts`: array of objects, each with fields: + * `success`: boolean, whether this individual account query succeeded + * `accountName`: string, canonical, case-unfolded version of the account name +* `totalCount`: integer, total number of accounts returned + + +`/v1/status` +------------- + +This endpoint returns status information about the running Ergo server. The request body is ignored and can be empty. + +The response is a JSON object with fields: + +* `success`: whether the request succeeded +* `version`: string, Ergo server version string +* `go_version`: string, version of Go runtime used +* `start_time`: string, server start time in ISO8601 format +* `users`: object with fields: + * `total`: total number of users connected + * `invisible`: number of invisible users + * `operators`: number of operators connected + * `unknown`: number of users with unknown status + * `max`: maximum number of users seen connected at once +* `channels`: integer, number of channels currently active +* `servers`: integer, number of servers connected in the network diff --git a/irc/api.go b/irc/api.go index a25b6537..cb313cbc 100644 --- a/irc/api.go +++ b/irc/api.go @@ -5,7 +5,10 @@ import ( "encoding/json" "fmt" "net/http" + "runtime" "strings" + + "github.com/ergochat/ergo/irc/utils" ) func newAPIHandler(server *Server) http.Handler { @@ -18,6 +21,8 @@ func newAPIHandler(server *Server) http.Handler { api.mux.HandleFunc("POST /v1/check_auth", api.handleCheckAuth) api.mux.HandleFunc("POST /v1/saregister", api.handleSaregister) api.mux.HandleFunc("POST /v1/account_details", api.handleAccountDetails) + api.mux.HandleFunc("POST /v1/account_list", api.handleAccountList) + api.mux.HandleFunc("POST /v1/status", api.handleStatus) return api } @@ -29,7 +34,6 @@ type ergoAPI struct { func (a *ergoAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer a.server.HandlePanic(nil) - defer a.server.logger.Debug("api", r.URL.Path) if a.checkBearerAuth(r.Header.Get("Authorization")) { @@ -117,8 +121,6 @@ func (a *ergoAPI) handleCheckAuth(w http.ResponseWriter, r *http.Request) { // try passphrase if present if request.AccountName != "" && request.Passphrase != "" { - // TODO this only checks the internal database, not auth-script; - // it's a little weird to use both auth-script and the API but we should probably handle it account, err := a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase) switch err { case nil: @@ -133,7 +135,6 @@ func (a *ergoAPI) handleCheckAuth(w http.ResponseWriter, r *http.Request) { response.Error = err.Error() } } - // try certfp if present if !response.Success && request.Certfp != "" { // TODO support cerftp @@ -175,8 +176,10 @@ func (a *ergoAPI) handleSaregister(w http.ResponseWriter, r *http.Request) { type apiAccountDetailsResponse struct { apiGenericResponse - AccountName string `json:"accountName,omitempty"` - Email string `json:"email,omitempty"` + AccountName string `json:"accountName,omitempty"` + Email string `json:"email,omitempty"` + RegisteredAt string `json:"registeredAt,omitempty"` + Channels []string `json:"channels,omitempty"` } type apiAccountDetailsRequest struct { @@ -191,8 +194,6 @@ func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) { var response apiAccountDetailsResponse - // TODO could probably use better error handling and more details - if request.AccountName != "" { accountData, err := a.server.accounts.LoadAccount(request.AccountName) if err == nil { @@ -207,6 +208,12 @@ func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) { case nil: response.AccountName = accountData.Name response.Email = accountData.Settings.Email + if !accountData.RegisteredAt.IsZero() { + response.RegisteredAt = accountData.RegisteredAt.Format(utils.IRCv3TimestampFormat) + } + + // Get channels the account is in + response.Channels = a.server.channels.ChannelsForAccount(accountData.NameCasefolded) response.Success = true case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended: response.Success = false @@ -222,3 +229,83 @@ func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) { a.writeJSONResponse(response, w, r) } + +type apiAccountListResponse struct { + apiGenericResponse + Accounts []apiAccountDetailsResponse `json:"accounts"` + TotalCount int `json:"totalCount"` +} + +func (a *ergoAPI) handleAccountList(w http.ResponseWriter, r *http.Request) { + var response apiAccountListResponse + + // Get all account names + accounts := a.server.accounts.AllNicks() + response.TotalCount = len(accounts) + + // Load account details + response.Accounts = make([]apiAccountDetailsResponse, len(accounts)) + for i, account := range accounts { + accountData, err := a.server.accounts.LoadAccount(account) + if err != nil { + response.Accounts[i] = apiAccountDetailsResponse{ + apiGenericResponse: apiGenericResponse{ + Success: false, + Error: err.Error(), + }, + } + continue + } + + response.Accounts[i] = apiAccountDetailsResponse{ + apiGenericResponse: apiGenericResponse{ + Success: true, + }, + AccountName: accountData.Name, + Email: accountData.Settings.Email, + } + } + + response.Success = true + a.writeJSONResponse(response, w, r) +} + +type apiStatusResponse struct { + apiGenericResponse + Version string `json:"version"` + GoVersion string `json:"go_version"` + Commit string `json:"commit,omitempty"` + StartTime string `json:"start_time"` + Users struct { + Total int `json:"total"` + Invisible int `json:"invisible"` + Operators int `json:"operators"` + Unknown int `json:"unknown"` + Max int `json:"max"` + } `json:"users"` + Channels int `json:"channels"` + Servers int `json:"servers"` +} + +func (a *ergoAPI) handleStatus(w http.ResponseWriter, r *http.Request) { + server := a.server + stats := server.stats.GetValues() + + response := apiStatusResponse{ + apiGenericResponse: apiGenericResponse{Success: true}, + Version: SemVer, + GoVersion: runtime.Version(), + Commit: Commit, + StartTime: server.ctime.Format(utils.IRCv3TimestampFormat), + } + + response.Users.Total = stats.Total + response.Users.Invisible = stats.Invisible + response.Users.Operators = stats.Operators + response.Users.Unknown = stats.Unknown + response.Users.Max = stats.Max + response.Channels = server.channels.Len() + response.Servers = 1 + + a.writeJSONResponse(response, w, r) +}