mirror of
https://github.com/ergochat/ergo.git
synced 2025-06-03 05:17:49 +02:00
API enhancements (#2261)
Fixes #2257 and #2260 * add `/v1/status` endpoint * add `/v1/account_list` endpoint * add fields to `/v1/account_details` response
This commit is contained in:
parent
4da6511674
commit
e4aac56bda
36
docs/API.md
36
docs/API.md
@ -51,6 +51,8 @@ The response is a JSON object with fields:
|
|||||||
* `success`: whether the account exists or not
|
* `success`: whether the account exists or not
|
||||||
* `accountName`: canonical, case-unfolded version of the account name
|
* `accountName`: canonical, case-unfolded version of the account name
|
||||||
* `email`: email address of the account provided
|
* `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`
|
`/v1/check_auth`
|
||||||
----------------
|
----------------
|
||||||
@ -86,3 +88,37 @@ The response is a JSON object with fields:
|
|||||||
* `success`: whether the account creation succeeded
|
* `success`: whether the account creation succeeded
|
||||||
* `errorCode`: string, optional, machine-readable description of the error. Possible values include: `ACCOUNT_EXISTS`, `INVALID_PASSPHRASE`, `UNKNOWN_ERROR`.
|
* `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.
|
* `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
|
||||||
|
103
irc/api.go
103
irc/api.go
@ -5,7 +5,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newAPIHandler(server *Server) http.Handler {
|
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/check_auth", api.handleCheckAuth)
|
||||||
api.mux.HandleFunc("POST /v1/saregister", api.handleSaregister)
|
api.mux.HandleFunc("POST /v1/saregister", api.handleSaregister)
|
||||||
api.mux.HandleFunc("POST /v1/account_details", api.handleAccountDetails)
|
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
|
return api
|
||||||
}
|
}
|
||||||
@ -29,7 +34,6 @@ type ergoAPI struct {
|
|||||||
|
|
||||||
func (a *ergoAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (a *ergoAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
defer a.server.HandlePanic(nil)
|
defer a.server.HandlePanic(nil)
|
||||||
|
|
||||||
defer a.server.logger.Debug("api", r.URL.Path)
|
defer a.server.logger.Debug("api", r.URL.Path)
|
||||||
|
|
||||||
if a.checkBearerAuth(r.Header.Get("Authorization")) {
|
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
|
// try passphrase if present
|
||||||
if request.AccountName != "" && request.Passphrase != "" {
|
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)
|
account, err := a.server.accounts.checkPassphrase(request.AccountName, request.Passphrase)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
@ -133,7 +135,6 @@ func (a *ergoAPI) handleCheckAuth(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.Error = err.Error()
|
response.Error = err.Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// try certfp if present
|
// try certfp if present
|
||||||
if !response.Success && request.Certfp != "" {
|
if !response.Success && request.Certfp != "" {
|
||||||
// TODO support cerftp
|
// TODO support cerftp
|
||||||
@ -175,8 +176,10 @@ func (a *ergoAPI) handleSaregister(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
type apiAccountDetailsResponse struct {
|
type apiAccountDetailsResponse struct {
|
||||||
apiGenericResponse
|
apiGenericResponse
|
||||||
AccountName string `json:"accountName,omitempty"`
|
AccountName string `json:"accountName,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
|
RegisteredAt string `json:"registeredAt,omitempty"`
|
||||||
|
Channels []string `json:"channels,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountDetailsRequest struct {
|
type apiAccountDetailsRequest struct {
|
||||||
@ -191,8 +194,6 @@ func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var response apiAccountDetailsResponse
|
var response apiAccountDetailsResponse
|
||||||
|
|
||||||
// TODO could probably use better error handling and more details
|
|
||||||
|
|
||||||
if request.AccountName != "" {
|
if request.AccountName != "" {
|
||||||
accountData, err := a.server.accounts.LoadAccount(request.AccountName)
|
accountData, err := a.server.accounts.LoadAccount(request.AccountName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -207,6 +208,12 @@ func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) {
|
|||||||
case nil:
|
case nil:
|
||||||
response.AccountName = accountData.Name
|
response.AccountName = accountData.Name
|
||||||
response.Email = accountData.Settings.Email
|
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
|
response.Success = true
|
||||||
case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended:
|
case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended:
|
||||||
response.Success = false
|
response.Success = false
|
||||||
@ -222,3 +229,83 @@ func (a *ergoAPI) handleAccountDetails(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
a.writeJSONResponse(response, w, r)
|
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)
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user