1
0
mirror of https://github.com/anope/anope.git synced 2026-06-30 09:16:38 +02:00

Add the rpc_data module.

- Add rewritten and namespaced versions of the channel, oper, user
  events.

- Add the following new events:
  * anope.listChannels
  * anope.listOpers
  * anope.listServers
  * anope.listUsers
  * anope.server
This commit is contained in:
Sadie Powell
2025-02-24 05:46:43 +00:00
parent 801a748e25
commit e500258ce4
6 changed files with 773 additions and 148 deletions
+16
View File
@@ -817,6 +817,22 @@ module { name = "sasl" }
server = "httpd/main"
}
/*
* rpc_data
*
* Adds support for the following RPC methods:
*
* anope.listChannels anope.channel
* anope.listOpers anope.oper
* anope.listServers anope.server
* anope.listUsers anope.user
*
* Requires either the jsonrpc or xmlrpc module.
*
* See docs/RPC/rpc_data.md for API documentation.
*/
#module { name = "rpc_data" }
/*
* rpc_main
*
-7
View File
@@ -13,13 +13,6 @@ command - Takes three parameters, a service name (BotServ, ChanServ, NickServ),
stats - Takes no parameters, returns miscellaneous stats that can be found in the /operserv stats command.
channel - Takes one parameter, a channel name, and returns real time information regarding that channel, such as users, modes
(ban lists and such), topic etc.
user - Takes one parameter, a user name, and returns real time information regarding that user.
opers - Takes no parameters, returns opertypes, their privileges and commands.
notice - Takes three parameters, source user, target user, and message. Sends a message to the user.
RPC was designed to be used with db_sql, and will not return any information that can be pulled from the SQL
+2 -2
View File
@@ -117,7 +117,7 @@ class AnopeRPC
*/
public function channel($channel)
{
return $this->run("channel", [$channel]);
return $this->run("anope.channel", [$channel]);
}
/**
@@ -143,7 +143,7 @@ class AnopeRPC
*/
public function user($user)
{
return $this->run("user", [$user]);
return $this->run("anope.user", [$user]);
}
}
+309
View File
@@ -0,0 +1,309 @@
# Anope `rpc_data` RPC interface
## `anope.listChannels`
Lists all channels that exist on the network.
### Parameters
*None*
### Errors
*Only standard RPC errors*
### Result
Returns an array of channel names.
#### Example
```json
["#chan1", "#chan2", "#chan3"]
```
## `anope.channel`
Retrieves information about the specified channel.
### Parameters
Index | Description
----- | -----------
0 | The name of the channel.
### Errors
Code | Description
------ | -----------
-32099 | The specified channel does not exist.
### Result
Returns a map containing information about the channel.
Key | Type | Description
--- | ---- | -----------
created | uint | The UNIX time at which the channel was originally created.
listmodes | map | List modes which are set on the channel keyed by the mode character.
modes | array[string] | Flag and parameter modes which are set on the channel.
name | string | The name of the channel.
registered | boolean | Whether the channel is registered.
topic | map or null | The channel topic or null if no topic is set.
topic.setat | uint | The time at which the topic was set.
topic.setby | string | The nick or nuh of the user who set the topic.
topic.value | string | The text of the topic.
users | array[string] | The users that are current in the channel prefixed by their status mode prefixes.
#### Example
```json
{
"created": 1740402691,
"listmodes": {
"b": ["foo!bar@baz", "account:bax"],
},
"modes": ["+knrt", "secret"],
"name": "#chan1",
"registered": true,
"topic": {
"setat": 1740404706,
"setby": "nick1",
"value": "Example channel topic"
},
"users": ["@nick1", "nick2"]
}
```
## `anope.listOpers`
Lists all services operators that exist on the network.
### Parameters
*None*
### Errors
*Only standard RPC errors*
### Result
Returns an array of services operator names.
#### Example
```json
["nick1", "nick2", "nick3"]
```
## `anope.oper`
Retrieves information about the specified services operator.
### Parameters
Index | Description
----- | -----------
0 | The name of the services operator.
### Errors
Code | Description
------ | -----------
-32099 | The specified services operator does not exist.
### Result
Returns a map containing information about the services operator.
Key | Type | Description
--- | ---- | -----------
fingerprints | array[string] or null | The client certificate fingerprints that a user must be using to log in as this services operator or null if there are no client certificate restrictions.
hosts | array[string] or null | The user@ip and user@ip masks that a user must be connecting from to log in as this services operator or null if there are no host restrictions.
name | string | The name of the services operator.
operonly | boolean | Whether a user has to be a server operator to log in as this services operator.
opertype | map | The oper type associated with the services operator opertype.
opertype.commands | array[string] | The commands that the services operator type can use.
opertype.name | string | The name of the services operator type.
opertype.privileges | array[string] | The privileges that the services operator type has.
password | boolean | Whether a user has to specify a password to log in as the services operator.
vhost | string or null | The vhost of the services operator or null if there is no vhost.
#### Example
```json
{
"fingerprints": null,
"hosts": ["*@*.example.com"],
"name": "stest",
"operonly": true,
"opertype": {
"commands": ["hostserv/*", "operserv/session"],
"name": "Helper",
"privileges": ["chanserv/no-register-limit"]
},
"password": false,
"vhost": null
}
```
## `anope.listServers`
Lists all servers that exist on the network.
### Parameters
*None*
### Errors
*Only standard RPC errors*
### Result
Returns an array of server names.
#### Example
```json
["irc1.example.com", "irc2.example.com", "services.example.com"]
```
## `anope.server`
Retrieves information about the specified server.
### Parameters
Index | Description
----- | -----------
0 | The name of the server.
### Errors
Code | Description
------ | -----------
-32099 | The specified server does not exist.
### Result
Returns a map containing information about the server.
Key | Type | Description
--- | ---- | -----------
description | string | The description of the server.
downlinks | array[string] | The servers which are behind this server
juped | boolean | Whether the server has been juped.
name | string | The name of the server.
sid | string or null | The unique immutable identifier of the server or null if the IRCd does not use SIDs.
synced | boolean | Whether the server has finished syncing.
ulined | boolean | Whether the server is U-lined.
uplink | string or null | The server in front of this server or null if it is the services server.
#### Example
```json
{
"description": "Anope IRC Services",
"downlinks": ["irc.example.com"],
"juped": false,
"name": "services.example.com",
"sid": "00B",
"synced": true,
"ulined": true,
"uplink": null
}
```
## `anope.listUsers`
Lists all users that exist on the network.
### Parameters
*None*
### Errors
*Only standard RPC errors*
### Result
Returns an array of user nicknames.
#### Example
```json
["nick1", "nick2", "nick3"]
```
## `anope.user`
Retrieves information about the specified user.
### Parameters
Index | Description
----- | -----------
0 | The nickname of the user.
### Errors
Code | Description
------ | -----------
-32099 | The specified user does not exist.
### Result
Returns a map containing information about the user.
Key | Type | Description
--- | ---- | -----------
account | map or null | The user's account or null if they are not logged in to an account.
account.display | string | The display nickname of the account.
account.opertype | string or null | The account's oper type or null if the account is not a services operator.
account.uniqueid | uint | The unique immutable identifier of the account.
address | string | The IP address the user is connecting from.
channels | array[string] | The channels that the user is in prefixed by their status mode prefixes.
chost | string or null | The cloaked hostname of the user or null if they have no cloak.
fingerprint | string or null | The fingerprint of the user's client certificate or null if they are not using one.
host | string | The real hostname of the user.
ident | string | The username (ident) of the user.
modes | array[string] | Flag and parameter modes which are set on the user.
nick | string | The nickname of the user.
nickchanged | uint | The time at which the user last changed their nickname.
real | string | The real name of the user.
server | string | The server that the user is connected to.
signon | uint | The time at which the user connected to the network.
uid | string or null | The unique immutable identifier of the user or null if the IRCd does not use UIDs.
vhost | string or null | The virtual host of the user or null if they have no vhost.
vident | string or null | The virtual ident (username) of the user or null if they have no vident.
#### Example
```json
{
"account": {
"display": "nick1",
"opertype": "Services Root",
"uniqueid": "17183514657819486040"
},
"address": "127.0.0.1",
"channels": ["@#chan1", "#chan2"],
"chost": "localhost",
"fingerprint": null,
"host": "localhost",
"id": "9TSAAAAAA",
"ident": "user1",
"modes": ["+r"],
"nick": "nick1",
"nickchanged": 1740408318,
"real": "An IRC User",
"server": "irc.example.com",
"signon": 1740408296,
"vhost": "staff.example.com",
"vident": null,
}
```
+446
View File
@@ -0,0 +1,446 @@
/*
*
* (C) 2010-2025 Anope Team
* Contact us at team@anope.org
*
* Please read COPYING and README for further details.
*/
#include "module.h"
#include "modules/rpc.h"
enum
{
// Used by anope.channel, anope.oper, anope.server, and anope.user
ERR_NO_SUCH_TARGET = RPC::ERR_CUSTOM_START,
};
class AnopeListChannelsRPCEvent final
: public RPC::Event
{
public:
AnopeListChannelsRPCEvent()
: RPC::Event("anope.listChannels")
{
}
bool Run(RPC::ServiceInterface *iface, HTTPClient *client, RPC::Request &request) override
{
auto &root = request.Root<RPC::Array>();
for (auto &[_, c] : ChannelList)
root.Reply(c->name);
return true;
}
};
class AnopeChannelRPCEvent final
: public RPC::Event
{
public:
AnopeChannelRPCEvent()
: RPC::Event("anope.channel")
{
}
bool Run(RPC::ServiceInterface *iface, HTTPClient *client, RPC::Request &request) override
{
if (request.data.empty())
{
request.Error(RPC::ERR_INVALID_PARAMS, "Not enough parameters");
return true;
}
auto *c = Channel::Find(request.data[0]);
if (!c)
{
request.Error(ERR_NO_SUCH_TARGET, "No such channel");
return true;
}
auto &root = request.Root();
root.Reply("created", c->creation_time)
.Reply("name", c->name)
.Reply("registered", !!c->ci);
std::map<char, RPC::Array&> modemap;
auto &listmodes = root.ReplyMap("listmodes");
for (auto *cm : ModeManager::GetChannelModes())
{
if (cm->type != MODE_LIST)
continue;
for (auto &entry : c->GetModeList(cm->name))
{
auto *wcm = cm->Wrap(entry);
auto it = modemap.find(wcm->mchar);
if (it == modemap.end())
it = modemap.emplace(wcm->mchar, listmodes.ReplyArray(wcm->mchar)).first;
it->second.Reply(entry);
}
}
std::vector<Anope::string> modelist = { "+" };
for (const auto &[mname, mvalue] : c->GetModes())
{
auto *cm = ModeManager::FindChannelModeByName(mname);
if (!cm || cm->type == MODE_LIST)
continue;
modelist.front().push_back(cm->mchar);
if (!mvalue.empty())
modelist.push_back(mvalue);
}
auto &modes = root.ReplyArray("modes");
for (const auto &modeparam : modelist)
modes.Reply(modeparam);
if (c->topic.empty())
root.Reply("topic", nullptr);
else
{
auto &topic = root.ReplyMap("topic");
topic.Reply("setat", c->topic_ts)
.Reply("setby", c->topic_setter)
.Reply("value", c->topic);
}
auto &users = root.ReplyArray("users");
for (const auto &[_, uc] : c->users)
users.Reply(uc->status.BuildModePrefixList() + uc->user->nick);
return true;
}
};
class AnopeListOpersRPCEvent final
: public RPC::Event
{
public:
AnopeListOpersRPCEvent()
: RPC::Event("anope.listOpers")
{
}
bool Run(RPC::ServiceInterface *iface, HTTPClient *client, RPC::Request &request) override
{
auto &root = request.Root<RPC::Array>();
for (auto *oper : Oper::opers)
root.Reply(oper->name);
return true;
}
};
class AnopeOperRPCEvent final
: public RPC::Event
{
public:
AnopeOperRPCEvent()
: RPC::Event("anope.oper")
{
}
bool Run(RPC::ServiceInterface *iface, HTTPClient *client, RPC::Request &request) override
{
if (request.data.empty())
{
request.Error(RPC::ERR_INVALID_PARAMS, "Not enough parameters");
return true;
}
auto *o = Oper::Find(request.data[0]);
if (!o)
{
request.Error(ERR_NO_SUCH_TARGET, "No such oper");
return true;
}
auto &root = request.Root();
root
.Reply("name", o->name)
.Reply("operonly", o->require_oper)
.Reply("password", !o->password.empty());
if (o->certfp.empty())
root.Reply("fingerprints", nullptr);
else
{
auto &fingerprints = root.ReplyArray("fingerprints");
for (const auto &fingerprint : o->certfp)
fingerprints.Reply(fingerprint);
}
if (o->hosts.empty())
root.Reply("hosts", nullptr);
else
{
auto &hosts = root.ReplyArray("hosts");
for (const auto &host : o->hosts)
hosts.Reply(host);
}
auto &opertype = root.ReplyMap("opertype");
opertype.Reply("name", o->ot->GetName());
{
auto &commands = opertype.ReplyArray("commands");
for (const auto &command : o->ot->GetCommands())
commands.Reply(command);
auto &privileges = opertype.ReplyArray("privileges");
for (const auto &privilege : o->ot->GetPrivs())
privileges.Reply(privilege);
}
if (o->vhost.empty())
root.Reply("vhost", nullptr);
else
root.Reply("vhost", o->vhost);
return true;
}
};
class AnopeListServersRPCEvent final
: public RPC::Event
{
public:
AnopeListServersRPCEvent()
: RPC::Event("anope.listServers")
{
}
bool Run(RPC::ServiceInterface *iface, HTTPClient *client, RPC::Request &request) override
{
auto &root = request.Root<RPC::Array>();
for (auto &[_, s] : Servers::ByName)
root.Reply(s->GetName());
return true;
}
};
class AnopeServerRPCEvent final
: public RPC::Event
{
public:
AnopeServerRPCEvent()
: RPC::Event("anope.server")
{
}
bool Run(RPC::ServiceInterface *iface, HTTPClient *client, RPC::Request &request) override
{
if (request.data.empty())
{
request.Error(RPC::ERR_INVALID_PARAMS, "Not enough parameters");
return true;
}
auto *s = Server::Find(request.data[0]);
if (!s)
{
request.Error(ERR_NO_SUCH_TARGET, "No such server");
return true;
}
auto &root = request.Root();
root.Reply("description", s->GetDescription())
.Reply("juped", s->IsJuped())
.Reply("name", s->GetName())
.Reply("synced", s->IsSynced())
.Reply("ulined", s->IsULined());
auto &downlinks = root.ReplyArray("downlinks");
for (const auto *s : s->GetLinks())
downlinks.Reply(s->GetName());
if (IRCD->RequiresID)
root.Reply("sid", s->GetSID());
else
root.Reply("sid", nullptr);
if (s->GetUplink())
root.Reply("uplink", s->GetUplink()->GetName());
else
root.Reply("uplink", nullptr);
return true;
}
};
class AnopeListUsersRPCEvent final
: public RPC::Event
{
public:
AnopeListUsersRPCEvent()
: RPC::Event("anope.listUsers")
{
}
bool Run(RPC::ServiceInterface *iface, HTTPClient *client, RPC::Request &request) override
{
auto &root = request.Root<RPC::Array>();
for (auto &[_, u] : UserListByNick)
root.Reply(u->nick);
return true;
}
};
class AnopeUserRPCEvent final
: public RPC::Event
{
public:
AnopeUserRPCEvent()
: RPC::Event("anope.user")
{
}
bool Run(RPC::ServiceInterface *iface, HTTPClient *client, RPC::Request &request) override
{
if (request.data.empty())
{
request.Error(RPC::ERR_INVALID_PARAMS, "Not enough parameters");
return true;
}
auto *u = User::Find(request.data[0]);
if (!u)
{
request.Error(ERR_NO_SUCH_TARGET, "No such user");
return true;
}
auto &root = request.Root();
root.Reply("address", u->ip.addr())
.Reply("host", u->host)
.Reply("ident", u->GetIdent())
.Reply("nick", u->nick)
.Reply("nickchanged", u->timestamp)
.Reply("real", u->realname)
.Reply("server", u->server->GetName())
.Reply("signon", u->signon);
if (u->IsIdentified())
{
auto &account = root.ReplyMap("account");
account.Reply("display", u->Account()->display)
.Reply("uniqueid", u->Account()->GetId());
if (u->Account()->o)
account.Reply("opertype", u->Account()->o->ot->GetName());
else
account.Reply("opertype", nullptr);
}
else
{
root.Reply("account", nullptr);
}
auto &channels = root.ReplyArray("channels");
for (const auto &[_, cc] : u->chans)
channels.Reply(cc->status.BuildModePrefixList() + cc->chan->name);
if (u->chost.empty())
root.Reply("chost", nullptr);
else
root.Reply("chost", u->chost);
if (u->fingerprint.empty())
root.Reply("fingerprint", nullptr);
else
root.Reply("fingerprint", u->fingerprint);
std::vector<Anope::string> modelist = { "+" };
for (const auto &[mname, mvalue] : u->GetModeList())
{
auto *um = ModeManager::FindUserModeByName(mname);
if (!um || um->type == MODE_LIST)
continue;
modelist.front().push_back(um->mchar);
if (!mvalue.empty())
modelist.push_back(mvalue);
}
auto &modes = root.ReplyArray("modes");
for (const auto &modeparam : modelist)
modes.Reply(modeparam);
if (IRCD->RequiresID)
root.Reply("uid", u->GetUID());
else
root.Reply("uid", nullptr);
if (u->vhost.empty() || u->vhost.equals_cs(u->host))
root.Reply("vhost", nullptr);
else
root.Reply("vhost", u->vhost);
if (u->GetVIdent().equals_cs(u->GetIdent()))
root.Reply("vident", nullptr);
else
root.Reply("vident", u->GetIdent());
return true;
}
};
class ModuleRPCData final
: public Module
{
private:
ServiceReference<RPC::ServiceInterface> rpc;
AnopeListChannelsRPCEvent anopelistchannelsrpcevent;
AnopeChannelRPCEvent anopechannelrpcevent;
AnopeListOpersRPCEvent anopelistopersrpcevent;
AnopeOperRPCEvent anopeoperrpcevent;
AnopeListServersRPCEvent anopelistserversrpcevent;
AnopeServerRPCEvent anopeserverrpcevent;
AnopeListUsersRPCEvent anopelistusersrpcevent;
AnopeUserRPCEvent anopeuserrpcevent;
public:
ModuleRPCData(const Anope::string &modname, const Anope::string &creator)
: Module(modname, creator, EXTRA | VENDOR)
, rpc("RPCServiceInterface", "rpc")
{
if (!rpc)
throw ModuleException("Unable to find RPC interface, is jsonrpc/xmlrpc loaded?");
rpc->Register(&anopelistchannelsrpcevent);
rpc->Register(&anopechannelrpcevent);
rpc->Register(&anopelistopersrpcevent);
rpc->Register(&anopeoperrpcevent);
rpc->Register(&anopelistserversrpcevent);
rpc->Register(&anopeserverrpcevent);
rpc->Register(&anopelistusersrpcevent);
rpc->Register(&anopeuserrpcevent);
}
~ModuleRPCData() override
{
if (!rpc)
return;
rpc->Unregister(&anopelistchannelsrpcevent);
rpc->Unregister(&anopechannelrpcevent);
rpc->Unregister(&anopelistopersrpcevent);
rpc->Unregister(&anopeoperrpcevent);
rpc->Unregister(&anopelistserversrpcevent);
rpc->Unregister(&anopeserverrpcevent);
rpc->Unregister(&anopelistusersrpcevent);
rpc->Unregister(&anopeuserrpcevent);
}
};
MODULE_INIT(ModuleRPCData)
-139
View File
@@ -167,136 +167,6 @@ public:
}
};
class ChannelRPCEvent final
: public RPC::Event
{
public:
ChannelRPCEvent()
: RPC::Event("channel")
{
}
bool Run(RPC::ServiceInterface *iface, HTTPClient *client, RPC::Request &request) override
{
if (request.data.empty())
{
request.Error(RPC::ERR_INVALID_PARAMS, "Invalid parameters");
return true;
}
Channel *c = Channel::Find(request.data[0]);
auto &root = request.Root();
root.Reply("name", c ? c->name : request.data[0]);
if (c)
{
root.Reply("bancount", c->HasMode("BAN"));
auto &bans = root.ReplyArray("bans");
for (auto &ban : c->GetModeList("BAN"))
bans.Reply(ban);
root.Reply("exceptcount", c->HasMode("EXCEPT"));
auto &excepts = root.ReplyArray("excepts");
for (auto &except : c->GetModeList("EXCEPT"))
excepts.Reply(except);
root.Reply("invitecount", c->HasMode("INVITEOVERRIDE"));
auto &invites = root.ReplyArray("invites");
for (auto &invite : c->GetModeList("INVITEOVERRIDE"))
invites.Reply(invite);
auto &users = root.ReplyArray("users");
for (const auto &[_, uc] : c->users)
users.Reply(uc->status.BuildModePrefixList() + uc->user->nick);
if (!c->topic.empty())
root.Reply("topic", c->topic);
if (!c->topic_setter.empty())
root.Reply("topicsetter", c->topic_setter);
root.Reply("topictime", c->topic_time);
root.Reply("topicts", c->topic_ts);
}
return true;
}
};
class UserRPCEvent final
: public RPC::Event
{
public:
UserRPCEvent()
: RPC::Event("user")
{
}
bool Run(RPC::ServiceInterface *iface, HTTPClient *client, RPC::Request &request) override
{
if (request.data.empty())
{
request.Error(RPC::ERR_INVALID_PARAMS, "Invalid parameters");
return true;
}
User *u = User::Find(request.data[0]);
auto &root = request.Root();
root.Reply("nick", u ? u->nick : request.data[0]);
if (u)
{
root.Reply("ident", u->GetIdent());
root.Reply("vident", u->GetVIdent());
root.Reply("host", u->host);
if (!u->vhost.empty())
root.Reply("vhost", u->vhost);
if (!u->chost.empty())
root.Reply("chost", u->chost);
root.Reply("ip", u->ip.addr());
root.Reply("timestamp", u->timestamp);
root.Reply("signon", u->signon);
if (u->IsIdentified())
{
root.Reply("account", u->Account()->display);
if (u->Account()->o)
root.Reply("opertype", u->Account()->o->ot->GetName());
}
auto &channels = root.ReplyArray("channels");
for (const auto &[_, cc] : u->chans)
channels.Reply(cc->status.BuildModePrefixList() + cc->chan->name);
}
return true;
}
};
class OpersRPCEvent final
: public RPC::Event
{
public:
OpersRPCEvent()
: RPC::Event("opers")
{
}
bool Run(RPC::ServiceInterface *iface, HTTPClient *client, RPC::Request &request) override
{
auto &root = request.Root();
for (auto *ot : Config->MyOperTypes)
{
Anope::string perms;
for (const auto &priv : ot->GetPrivs())
perms += " " + priv;
for (const auto &command : ot->GetCommands())
perms += " " + command;
root.Reply(ot->GetName(), perms);
}
return true;
}
};
class NoticeRPCEvent final
: public RPC::Event
{
@@ -334,9 +204,6 @@ private:
CommandRPCEvent commandrpcevent;
CheckAuthenticationRPCEvent checkauthenticationrpcevent;
StatsRPCEvent statsrpcevent;
ChannelRPCEvent channelrpcevent;
UserRPCEvent userrpcevent;
OpersRPCEvent opersrpcevent;
NoticeRPCEvent noticerpcevent;
public:
@@ -352,9 +219,6 @@ public:
rpc->Register(&commandrpcevent);
rpc->Register(&checkauthenticationrpcevent);
rpc->Register(&statsrpcevent);
rpc->Register(&channelrpcevent);
rpc->Register(&userrpcevent);
rpc->Register(&opersrpcevent);
rpc->Register(&noticerpcevent);
}
@@ -366,9 +230,6 @@ public:
rpc->Unregister(&commandrpcevent);
rpc->Unregister(&checkauthenticationrpcevent);
rpc->Unregister(&statsrpcevent);
rpc->Unregister(&channelrpcevent);
rpc->Unregister(&userrpcevent);
rpc->Unregister(&opersrpcevent);
rpc->Unregister(&noticerpcevent);
}
};