1
0
mirror of https://github.com/anope/anope.git synced 2026-06-12 15:44:46 +02:00

Add the rpc_user module.

This commit is contained in:
Sadie Powell
2025-06-01 09:50:29 +01:00
parent b724617a8d
commit d326f869a3
5 changed files with 629 additions and 0 deletions
+23
View File
@@ -906,6 +906,29 @@ module
}
}
/*
* rpc_user
*
* Adds support for the following RPC methods:
*
* anope.checkCredentials anope.identify
* anope.listCommands anope.command
*
* Requires either the jsonrpc or xmlrpc module.
*
* See docs/RPC/rpc_user.md for API documentation.
*/
#module
{
name = "rpc_user"
/*
* Some commands can only be executed by a real IRC user. You can work around
* this executing them as an IRC user logged into the account if one exists.
*/
pretenduser = no
}
/*
* rpc_data
*
+52
View File
@@ -206,6 +206,58 @@ class AnopeRPC {
messageUser(source, target, ...messages) {
return this.run("anope.messageUser", source, target, ...messages);
}
/**
* Checks whether the specified credentials are valid.
*
* Requires the rpc_user module to be loaded.
*
* @param {string} account A nickname belonging to the account to check.
* @param {string} password The password for the specified account.
* @returns {object} An object containing basic information about the account.
*/
checkCredentials(account, password) {
return this.run("anope.checkCredentials", account, password);
}
/**
* Identifies an IRC user to the specified account.
*
* Requires the rpc_user module to be loaded.
*
* @param {string} account Either an account identifier or nickname belonging to the account to
* identify to.
* @param {string} password The nickname of the IRC user to identify to the account.
*/
identify(account, user) {
return this.run("anope.identify", account, user);
}
/**
* Lists all commands that exist on the network.
*
* Requires the rpc_user module to be loaded.
*
* @param {...*} services The nicknames of the services to list commands for.
* @returns {object} An object containing information about the available commands.
*/
listCommands(...services) {
return this.run("anope.listCommands", ...services);
}
/**
* Lists all commands that exist on the network.
*
* Requires the rpc_user module to be loaded.
*
* @param {string} account If non-empty then the account to execute the command as.
* @param {string} service The service which the command exists on.
* @param {...*} command The the command to execute and any parameters to pass to it.
* @returns {object} An object containing information about the available commands.
*/
command(account, service, ...command) {
return this.run("anope.command", account, service, ...command);
}
}
/*
+44
View File
@@ -186,6 +186,50 @@ class AnopeRPC
def message_user(source, target, *messages)
self.run("anope.messageUser", source, target, *messages)
end
# Checks whether the specified credentials are valid.
#
# Requires the rpc_user module to be loaded.
#
# @param account [String] A nickname belonging to the account to check.
# @param password [String] The password for the specified account.
# @return [Hash] A hash containing basic information about the account.
def check_credentials(account, password)
self.run("anope.checkCredentials", account, password)
end
# Identifies an IRC user to the specified account.
#
# Requires the rpc_user module to be loaded.
#
# @param account [String] Either an account identifier or nickname belonging to the account to
# identify to.
# @param password [String] The nickname of the IRC user to identify to the account.
def identify(account, user)
self.run("anope.identify", account, user)
end
# Lists all commands that exist on the network.
#
# Requires the rpc_user module to be loaded.
#
# @param services [Array<String>] The nicknames of the services to list commands for.
# @return [Hash] A hash containing information about the available commands.
def list_commands(*services)
self.run("anope.listCommands", *services)
end
# Lists all commands that exist on the network.
#
# Requires the rpc_user module to be loaded.
#
# @param account [String] If non-empty then the account to execute the command as.
# @param service [String] The service which the command exists on.
# @param command [Array<String>] The the command to execute and any parameters to pass to it.
# @return [Hash] A hash containing information about the available commands.
def command(account, service, *command)
self.run("anope.command", account, service, *command)
end
end
=begin
+175
View File
@@ -0,0 +1,175 @@
# Anope `rpc_user` RPC interface
## `anope.checkCredentials`
Checks whether the specified credentials are valid.
### Parameters
Index | Description
----- | -----------
0 | A nickname belonging to the account to check.
1 | The password for the specified account.
### Errors
Code | Description
------ | -----------
-32099 | The specified account does not exist.
-32098 | The specified password is not correct.
-32097 | The specified account is suspended.
### Result
Returns a map containing basic information about the account. More information about the account can be found by calling [the `anope.account` event using the value from the `uniqueid` field (requires the rpc_data module)](./rpc_user.md).
Key | Type | Description
--- | ---- | -----------
account | string | The display nickname of the account.
confirmed | boolean | Whether the account has been confirmed.
uniqueid | uint | The unique immutable identifier of the account.
#### Example
```json
{
"account": "foo",
"confirmed": true,
"uniqueid": 11085415958920757000,
}
```
## `anope.identify`
Identifies an IRC user to the specified account.
### Parameters
Index | Description
----- | -----------
0 | Either an account identifier or nickname belonging to the account to identify to.
1 | The nickname of the IRC user to identify to the account.
### Errors
Code | Description
------ | -----------
-32099 | The specified account does not exist.
-32098 | The specified IRC user does not exist.
### Result
This procedure returns no result.
## `anope.listCommands`
Lists all commands that exist on the network.
### Parameters
Index | Description
----- | -----------
0...n | The nicknames of the services to list commands for. If none are specified then all commands are returned.
### Errors
Code | Description
------ | -----------
-32098 | The specified service does not exist.
### Result
Returns a map containing information about the available commands.
Key | Type | Description
--- | ---- | -----------
\* | map | A key-value map of services to the commands that exist on them.
\*.\* | string | A key-value map of commands to information about the commands.
\*.\*.group | string or null | The group that the command belongs to or null if the command is not grouped.
\*.\*.hidden | boolean | Whether the command is visible in the help output.
\*.\*.maxparams | uint or null | The maximum number of parameters that the command accepts or null if there is no limit.
\*.\*.minparams | uint | The minimum number of parameters that the command accepts.
\*.\*.permission | string or null | The services operator permission required to execute the command or null if no permissions are required.
\*.\*.requiresaccount | boolean | Whether a caller must be logged into an account to execute the command.
\*.\*.requiresuser | boolean | Whether an IRC user is required to execute the command.
#### Example
```json
{
"Global": {
"GLOBAL": {
"group": null,
"hidden": false,
"maxparams": 1,
"minparams": 0,
"permission": "global/global",
"requiresaccount": true,
"requiresuser": false
},
"HELP": {
"group": null,
"hidden": false,
"maxparams": null,
"minparams": 0,
"permission": null,
"requireaccount": false,
"requireuser": false
},
"QUEUE": {
"group": null,
"hidden": false,
"maxparams": 2,
"minparams": 1,
"permission": "global/queue",
"requireaccount": true,
"requireuser": false
},
"SERVER": {
"group": null,
"hidden": false,
"maxparams": 2,
"minparams": 1,
"permission": "global/server",
"requireaccount": true,
"requireuser": false
}
}
}
```
## `anope.commands`
Executes the specified command.
### Parameters
Index | Description
----- | -----------
0 | If non-empty then the account to execute the command as.
1 | The service which the command exists on.
2...n | The the command to execute and any parameters to pass to it.
### Errors
Code | Description
------ | -----------
-32099 | The specified account does not exist.
-32098 | The specified service does not exist.
-32097 | The specified command does not exist.
### Result
Returns an array of messages returned by the command.
#### Example
```json
[
"Global commands:",
" GLOBAL Send a message to all users",
" HELP Displays this list and give information about commands",
" QUEUE Manages your pending message queue.",
" SERVER Send a message to all users on a server"
]
```
+335
View File
@@ -0,0 +1,335 @@
/*
*
* (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.checkCredentials, anope.identify, and anope.command.
ERR_INVALID_ACCOUNT = RPC::ERR_CUSTOM_START,
// Used by anope.checkCredentials
ERR_INVALID_PASSWORD = RPC::ERR_CUSTOM_START + 1,
ERR_ACCOUNT_SUSPENDED = RPC::ERR_CUSTOM_START + 2,
// Used by anope.identify
ERR_INVALID_USER = RPC::ERR_CUSTOM_START + 1,
// Used by anope.listCommands, and anope.command
ERR_INVALID_SERVICE = RPC::ERR_CUSTOM_START + 1,
// Used by anope.command
ERR_INVALID_COMMAND = RPC::ERR_CUSTOM_START + 2,
};
class AnopeCheckCredentialsRPCEvent final
: public RPC::Event
{
private:
class RPCIdentifyRequest final
: public IdentifyRequest
{
private:
RPC::Request request;
Reference<HTTP::Client> client;
Reference<RPC::ServiceInterface> rpcinterface;
public:
RPCIdentifyRequest(Module *m, RPC::Request &r, HTTP::Client *c, RPC::ServiceInterface *i, const Anope::string &a, const Anope::string &p)
: IdentifyRequest(m, a, p, c->GetIP())
, request(r)
, client(c)
, rpcinterface(i)
{
}
void OnSuccess() override
{
if (!rpcinterface || !client)
return;
auto *na = NickAlias::Find(GetAccount());
if (!na)
return; // Should never happen.
if (na->nc->HasExt("NS_SUSPENDED"))
{
request.Error(ERR_ACCOUNT_SUSPENDED, "Account suspended");
rpcinterface->Reply(request);
client->SendReply(&request.reply);
return;
}
auto &root = request.Root();
root.Reply("account", na->nc->display)
.Reply("confirmed", !na->nc->HasExt("UNCONFIRMED"))
.Reply("uniqueid", na->nc->GetId());
rpcinterface->Reply(request);
client->SendReply(&request.reply);
}
void OnFail() override
{
if (!rpcinterface || !client)
return;
if (NickAlias::Find(GetAccount()))
request.Error(ERR_INVALID_ACCOUNT, "Invalid account");
else
request.Error(ERR_INVALID_PASSWORD, "Invalid password");
rpcinterface->Reply(request);
client->SendReply(&request.reply);
}
};
public:
AnopeCheckCredentialsRPCEvent(Module *o)
: RPC::Event(o, "anope.checkCredentials", 2)
{
}
bool Run(RPC::ServiceInterface *iface, HTTP::Client *client, RPC::Request &request) override
{
const auto &username = request.data[0];
const auto &password = request.data[1];
if (username.empty() || password.empty())
{
request.Error(RPC::ERR_INVALID_PARAMS, "Not enough parameters");
return true;
}
auto *req = new RPCIdentifyRequest(this->owner, request, client, iface, username, password);
FOREACH_MOD(OnCheckAuthentication, (nullptr, req));
req->Dispatch();
return false;
}
};
class AnopeIdentifyRPCEvent final
: public RPC::Event
{
public:
AnopeIdentifyRPCEvent(Module *o)
: RPC::Event(o, "anope.identify", 2)
{
}
bool Run(RPC::ServiceInterface *iface, HTTP::Client *client, RPC::Request &request) override
{
auto *na = request.data[0].is_pos_number_only()
? NickAlias::FindId(Anope::Convert(request.data[0], 0))
: NickAlias::Find(request.data[0]);
if (!na)
{
request.Error(ERR_INVALID_ACCOUNT, "No such account");
return true;
}
auto *u = User::Find(request.data[1]);
if (!u)
{
request.Error(ERR_INVALID_USER, "No such user");
return true;
}
u->Identify(na);
return true;
}
};
class AnopeListCommandsRPCEvent final
: public RPC::Event
{
public:
AnopeListCommandsRPCEvent(Module *o)
: RPC::Event(o, "anope.listCommands")
{
}
bool Run(RPC::ServiceInterface *iface, HTTP::Client *client, RPC::Request &request) override
{
std::vector<BotInfo *> bots;
if (request.data.empty())
{
for (const auto &[_, bi] : *BotListByNick)
bots.push_back(bi);
}
else
{
for (const auto &bot : request.data)
{
auto *bi = BotInfo::Find(bot);
if (!bi)
{
request.Error(ERR_INVALID_SERVICE, "No such service");
return true;
}
bots.push_back(bi);
}
}
auto &root = request.Root();
for (const auto *bi : bots)
{
if (bi->commands.empty())
continue;
auto &commands = root.ReplyMap(bi->nick);
for (const auto &[command, info] : bi->commands)
{
ServiceReference<Command> cmdref("Command", info.name);
if (!cmdref)
continue;
auto &cmdinfo = commands.ReplyMap(command);
cmdinfo.Reply("hidden", info.hide)
.Reply("minparams", cmdref->min_params)
.Reply("requiresaccount", !cmdref->AllowUnregistered())
.Reply("requiresuser", cmdref->RequireUser());
if (info.group.empty())
cmdinfo.Reply("group", nullptr);
else
cmdinfo.Reply("group", info.group);
if (cmdref->max_params)
cmdinfo.Reply("maxparams", cmdref->max_params);
else
cmdinfo.Reply("maxparams", nullptr);
if (info.permission.empty())
cmdinfo.Reply("permission", nullptr);
else
cmdinfo.Reply("permission", info.permission);
}
}
return true;
}
};
class AnopeCommandRPCEvent final
: public RPC::Event
{
private:
class RPCCommandReply final
: public CommandReply
{
private:
RPC::Array &root;
public:
RPCCommandReply(RPC::Array &r)
: root(r)
{
}
void SendMessage(BotInfo *source, const Anope::string &msg) override
{
root.Reply(NormalizeBuffer(msg.replace_all_cs("\x1A", "\x20")));
};
};
public:
static bool pretenduser;
AnopeCommandRPCEvent(Module *o)
: RPC::Event(o, "anope.command", 3)
{
}
bool Run(RPC::ServiceInterface *iface, HTTP::Client *client, RPC::Request &request) override
{
NickAlias *na = nullptr;
if (!request.data[0].empty())
{
na = request.data[0].is_pos_number_only()
? NickAlias::FindId(Anope::Convert(request.data[0], 0))
: NickAlias::Find(request.data[0]);
if (!na)
{
request.Error(ERR_INVALID_ACCOUNT, "No such account");
return true;
}
}
auto *bi = BotInfo::Find(request.data[1], true);
if (!bi)
{
request.Error(ERR_INVALID_SERVICE, "No such service");
return true;
}
Anope::string command;
for (size_t i = 2; i < request.data.size(); ++i)
{
if (!command.empty())
command.push_back(' ');
command.append(request.data[i]);
}
User *u = nullptr;
if (pretenduser && na && !na->nc->users.empty())
{
// Try and find the nick user first.
for (auto *user : na->nc->users)
{
if (user->nick.equals_ci(na->nick))
{
u = user;
break;
}
}
// No nick user, fallback to the first.
if (!u)
u = na->nc->users.front();
}
RPCCommandReply reply(request.Root<RPC::Array>());
CommandSource source(na ? na->nick : "RPC", u, na ? *na->nc : nullptr, &reply, bi, request.id);
if (!Command::Run(source, command))
request.Error(ERR_INVALID_COMMAND, "No such command");
return true;
}
};
bool AnopeCommandRPCEvent::pretenduser = false;
class ModuleRPCAccount final
: public Module
{
private:
AnopeCheckCredentialsRPCEvent anopecheckcredentialsrpcevent;
AnopeIdentifyRPCEvent anopeidentifyrpcevent;
AnopeListCommandsRPCEvent anopelistcommandsrpcevent;
AnopeCommandRPCEvent anopecommandrpcevent;
public:
ModuleRPCAccount(const Anope::string &modname, const Anope::string &creator)
: Module(modname, creator, EXTRA | VENDOR)
, anopecheckcredentialsrpcevent(this)
, anopeidentifyrpcevent(this)
, anopelistcommandsrpcevent(this)
, anopecommandrpcevent(this)
{
}
void OnReload(Configuration::Conf &conf) override
{
AnopeCommandRPCEvent::pretenduser = conf.GetModule(this).Get<bool>("pretenduser");
}
};
MODULE_INIT(ModuleRPCAccount)