3
0
mirror of https://github.com/ergochat/ergo.git synced 2026-04-26 10:38:23 +02:00

add /v1/whois to API (#2387)

This commit is contained in:
Shivaram Lingamneni 2026-04-19 21:58:29 -07:00 committed by GitHub
parent 7c8d81ac79
commit 49192e6fa5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 103 additions and 2 deletions

View File

@ -179,3 +179,27 @@ The response is a JSON object with fields:
* `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
`/v1/whois`
-----------
This endpoint returns data about the current status of a nickname on the server. The request is a JSON object with fields:
* `nickname`: string, nickname to query
The response is a JSON object with fields:
* `success`: whether the request succeeded (a successful execution returns `true` here even if the nickname is not present)
* `present`: whether the nickname is present on the server; if false, the remaining fields are undefined
* `nickname`: actual nickname (without case normalization) of the nickname as present on the server
* `username`: IRC protocol username field of the user (not to be confused with account name)
* `hostname`: hostname of the user
* `realname`: realname/gecos of the user
* `account`: account name of the user (without case normalization)
* `modes`: string of all set user modes
* `away`: user's away message if set (omitted if they are not away)
* `channels`: list of channels the user is present in. Each channel is an object with fields:
* `name`: name of the channel
* `mode`: string, highest mode the user has in the channel (omitted if they have no mode)
* `join_time`: string, time the user joined the channel (in ISO8601 format)
* `session_count`: integer, number of active sessions

View File

@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"runtime"
"slices"
"strings"
"github.com/ergochat/ergo/irc/modes"
@ -27,6 +28,7 @@ func newAPIHandler(server *Server) http.Handler {
// use Ergo as a source of truth for authentication in other services:
api.mux.HandleFunc("POST /v1/check_auth", api.handleCheckAuth)
api.mux.HandleFunc("POST /v1/whois", api.handleWhois)
// legacy names for /v1/ns endpoints:
api.mux.HandleFunc("POST /v1/saregister", api.handleSaregister)
@ -422,6 +424,81 @@ type apiListResponse struct {
Channels []apiChannelData `json:"channels"`
}
type apiWhoisRequest struct {
Nickname string `json:"nickname"`
}
type apiWhoisChannelData struct {
Name string `json:"name"`
Mode string `json:"mode,omitempty"`
JoinTime string `json:"join_time"`
}
type apiWhoisResponse struct {
apiGenericResponse
Present bool `json:"present"`
Nickname string `json:"nickname,omitempty"`
Username string `json:"username,omitempty"`
Hostname string `json:"hostname,omitempty"`
Realname string `json:"realname,omitempty"`
Account string `json:"account"`
Modes string `json:"modes,omitempty"`
Away string `json:"away,omitempty"`
Channels []apiWhoisChannelData `json:"channels"`
SessionCount int `json:"session_count"`
}
func (a *ergoAPI) handleWhois(w http.ResponseWriter, r *http.Request) {
var request apiWhoisRequest
if err := a.decodeJSONRequest(&request, w, r); err != nil {
return
}
response := apiWhoisResponse{
apiGenericResponse: apiGenericResponse{Success: true},
}
client := a.server.clients.Get(request.Nickname)
if client != nil {
response.Present = true
details := client.Details()
response.Nickname = details.nick
response.Username = details.username
response.Hostname = details.hostname
response.Realname = details.realname
if details.account != "" {
response.Account = details.accountName
}
response.Modes = client.ModeString()
if away, awayMsg := client.Away(); away {
response.Away = awayMsg
}
response.SessionCount = len(client.Sessions())
channels := client.Channels()
response.Channels = make([]apiWhoisChannelData, 0, len(channels))
for _, channel := range channels {
present, joinTime, cModes := channel.ClientStatus(client)
if !present {
continue
}
chData := apiWhoisChannelData{
Name: channel.Name(),
JoinTime: joinTime.Format(utils.IRCv3TimestampFormat),
}
for _, m := range modes.ChannelUserModes {
if slices.Contains(cModes, m) {
chData.Mode = string(rune(m))
break
}
}
response.Channels = append(response.Channels, chData)
}
}
a.writeJSONResponse(response, w, r)
}
func (a *ergoAPI) handleList(w http.ResponseWriter, r *http.Request) {
channels := a.server.channels.ListableChannels()
response := apiListResponse{

View File

@ -547,12 +547,12 @@ func (channel *Channel) ClientPrefixes(client *Client, isMultiPrefix bool) strin
}
}
func (channel *Channel) ClientStatus(client *Client) (present bool, joinTimeSecs int64, cModes modes.Modes) {
func (channel *Channel) ClientStatus(client *Client) (present bool, joinTime time.Time, cModes modes.Modes) {
channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock()
memberData, present := channel.members[client]
if present {
return present, time.Unix(0, memberData.joinTime).Unix(), memberData.modes.AllModes()
return present, time.Unix(0, memberData.joinTime), memberData.modes.AllModes()
} else {
return
}