mirror of
https://github.com/chatmail/core.git
synced 2026-07-03 04:55:14 +03:00
Compare commits
2 Commits
jsonrpc-ap
...
json-rpc-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af25c8bead | ||
|
|
8160a3c60a |
@@ -4442,40 +4442,28 @@ pub unsafe extern "C" fn dc_accounts_get_next_event(
|
||||
#[cfg(feature = "jsonrpc")]
|
||||
mod jsonrpc {
|
||||
use super::*;
|
||||
use deltachat_jsonrpc::api::DeltaChatApiV0;
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
|
||||
pub struct dc_jsonrpc_instance_t {
|
||||
receiver: OutReceiver,
|
||||
handle: RpcSession<DeltaChatApiV0>,
|
||||
handle: RpcSession<CommandApi>,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_init(
|
||||
account_manager: *mut dc_accounts_t,
|
||||
api_version: *const libc::c_char,
|
||||
) -> *mut dc_jsonrpc_instance_t {
|
||||
if account_manager.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_init()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let api_version = to_string_lossy(api_version);
|
||||
|
||||
let rpc_api = match api_version.as_str() {
|
||||
"v0" => {
|
||||
deltachat_jsonrpc::api::DeltaChatApiV0::from_arc((*account_manager).inner.clone())
|
||||
}
|
||||
version => {
|
||||
eprintln!(
|
||||
"Error initializing JSON-RPC API: API version {} is not supported.",
|
||||
version
|
||||
);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let cmd_api =
|
||||
deltachat_jsonrpc::api::CommandApi::from_arc((*account_manager).inner.clone());
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
let handle = RpcSession::new(request_handle, rpc_api);
|
||||
let handle = RpcSession::new(request_handle, cmd_api);
|
||||
|
||||
let instance = dc_jsonrpc_instance_t { receiver, handle };
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use deltachat::{
|
||||
context::get_info,
|
||||
message::{Message, MsgId, Viewtype},
|
||||
provider::get_provider_info,
|
||||
qr,
|
||||
qr::{self, Qr},
|
||||
webxdc::StatusUpdateSerial,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
@@ -22,6 +22,7 @@ pub mod events;
|
||||
pub mod types;
|
||||
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::QrObject;
|
||||
|
||||
use types::account::Account;
|
||||
use types::chat::FullChat;
|
||||
@@ -34,20 +35,20 @@ use types::webxdc::WebxdcMessageInfo;
|
||||
use self::types::message::MessageViewtype;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DeltaChatApiV0 {
|
||||
pub struct CommandApi {
|
||||
pub(crate) accounts: Arc<RwLock<Accounts>>,
|
||||
}
|
||||
|
||||
impl DeltaChatApiV0 {
|
||||
impl CommandApi {
|
||||
pub fn new(accounts: Accounts) -> Self {
|
||||
DeltaChatApiV0 {
|
||||
CommandApi {
|
||||
accounts: Arc::new(RwLock::new(accounts)),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
||||
DeltaChatApiV0 { accounts }
|
||||
CommandApi { accounts }
|
||||
}
|
||||
|
||||
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
|
||||
@@ -63,7 +64,7 @@ impl DeltaChatApiV0 {
|
||||
}
|
||||
|
||||
#[rpc(all_positional, ts_outdir = "typescript/generated")]
|
||||
impl DeltaChatApiV0 {
|
||||
impl CommandApi {
|
||||
// ---------------------------------------------
|
||||
// Misc top level functions
|
||||
// ---------------------------------------------
|
||||
@@ -202,6 +203,13 @@ impl DeltaChatApiV0 {
|
||||
qr::set_config_from_qr(&ctx, &qr_content).await
|
||||
}
|
||||
|
||||
async fn check_qr(&self, account_id: u32, qr_content: String) -> Result<QrObject> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let qr = qr::check_qr(&ctx, &qr_content).await?;
|
||||
let qr_object = QrObject::from(qr);
|
||||
Ok(qr_object)
|
||||
}
|
||||
|
||||
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
get_config(&ctx, &key).await
|
||||
|
||||
@@ -19,7 +19,9 @@ pub enum Account {
|
||||
color: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Unconfigured { id: u32 },
|
||||
Unconfigured {
|
||||
id: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl Account {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::download::DownloadState;
|
||||
use deltachat::message::Message;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::message::Viewtype;
|
||||
@@ -53,6 +54,8 @@ pub struct MessageObject {
|
||||
file_mime: Option<String>,
|
||||
file_bytes: u64,
|
||||
file_name: Option<String>,
|
||||
|
||||
download_state: MessageDownloadState
|
||||
}
|
||||
|
||||
impl MessageObject {
|
||||
@@ -121,6 +124,8 @@ impl MessageObject {
|
||||
file_mime: message.get_filemime(),
|
||||
file_bytes,
|
||||
file_name: message.get_filename(),
|
||||
|
||||
download_state: message.download_state().try_into().unwrap()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -165,6 +170,7 @@ pub enum MessageViewtype {
|
||||
Webxdc,
|
||||
}
|
||||
|
||||
|
||||
impl From<Viewtype> for MessageViewtype {
|
||||
fn from(viewtype: Viewtype) -> Self {
|
||||
match viewtype {
|
||||
@@ -200,3 +206,36 @@ impl From<MessageViewtype> for Viewtype {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, TypeDef)]
|
||||
#[serde(rename = "DownloadState")]
|
||||
pub enum MessageDownloadState {
|
||||
Done,
|
||||
Available,
|
||||
Failure,
|
||||
InProgress
|
||||
}
|
||||
|
||||
|
||||
impl From<DownloadState> for MessageDownloadState{
|
||||
fn from(download_state: DownloadState) -> Self {
|
||||
match download_state {
|
||||
DownloadState::Done => MessageDownloadState::Done,
|
||||
DownloadState::Available => MessageDownloadState::Available,
|
||||
DownloadState::Failure => MessageDownloadState::Failure,
|
||||
DownloadState::InProgress => MessageDownloadState::InProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MessageDownloadState> for DownloadState {
|
||||
fn from(message_download_state: MessageDownloadState) -> Self {
|
||||
match message_download_state {
|
||||
MessageDownloadState::Done => DownloadState::Done,
|
||||
MessageDownloadState::Available => DownloadState::Available,
|
||||
MessageDownloadState::Failure => DownloadState::Failure,
|
||||
MessageDownloadState::InProgress => DownloadState::InProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
use deltachat::qr::Qr;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
pub mod account;
|
||||
pub mod chat;
|
||||
pub mod chat_list;
|
||||
@@ -17,3 +21,98 @@ fn maybe_empty_string_to_option(string: String) -> Option<String> {
|
||||
Some(string)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||
pub enum QrObject {
|
||||
AskVerifyContact {
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
AskVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
FprOk {
|
||||
contact_id: u32,
|
||||
},
|
||||
FprMismatch {
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
FprWithoutAddr {
|
||||
fingerprint: String,
|
||||
},
|
||||
Account {
|
||||
domain: String,
|
||||
},
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
},
|
||||
Addr {
|
||||
contact_id: u32,
|
||||
},
|
||||
Url {
|
||||
url: String,
|
||||
},
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
WithdrawVerifyContact {
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
WithdrawVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
ReviveVerifyContact {
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
ReviveVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Qr> for QrObject {
|
||||
fn from(qr: Qr) -> Self {
|
||||
match qr {
|
||||
Qr::Account { domain } => {
|
||||
QrObject::Account { domain }
|
||||
}
|
||||
Qr::FprOk {
|
||||
contact_id,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::FprOk { contact_id }
|
||||
}
|
||||
Qr::FprWithoutAddr {
|
||||
fingerprint,
|
||||
} => {
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::FprWithoutAddr { fingerprint }
|
||||
}
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
pub mod api;
|
||||
pub use api::events;
|
||||
pub use api::{Accounts, DeltaChatApiV0};
|
||||
pub use yerpc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::api::{Accounts, DeltaChatApiV0};
|
||||
use super::api::{Accounts, CommandApi};
|
||||
use async_channel::unbounded;
|
||||
use futures::StreamExt;
|
||||
use tempfile::TempDir;
|
||||
@@ -15,7 +14,7 @@ mod tests {
|
||||
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||
let accounts = Accounts::new(tmp_dir).await?;
|
||||
let api = DeltaChatApiV0::new(accounts);
|
||||
let api = CommandApi::new(accounts);
|
||||
|
||||
let (sender, mut receiver) = unbounded::<String>();
|
||||
|
||||
@@ -56,7 +55,7 @@ mod tests {
|
||||
async fn test_batch_set_config() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||
let accounts = Accounts::new(tmp_dir).await?;
|
||||
let api = DeltaChatApiV0::new(accounts);
|
||||
let api = CommandApi::new(accounts);
|
||||
|
||||
let (sender, mut receiver) = unbounded::<String>();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
mod api;
|
||||
use api::events::event_to_json_rpc_notification;
|
||||
use api::{Accounts, DeltaChatApiV0};
|
||||
use api::{Accounts, CommandApi};
|
||||
|
||||
const DEFAULT_PORT: u16 = 20808;
|
||||
|
||||
@@ -20,10 +20,10 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
.unwrap_or(DEFAULT_PORT);
|
||||
log::info!("Starting with accounts directory `{path}`.");
|
||||
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
|
||||
let state = DeltaChatApiV0::new(accounts);
|
||||
let state = CommandApi::new(accounts);
|
||||
|
||||
let app = Router::new()
|
||||
.route("/rpc/v0", get(handler))
|
||||
.route("/ws", get(handler))
|
||||
.layer(Extension(state.clone()));
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -40,7 +40,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<DeltaChatApiV0>) -> Response {
|
||||
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
|
||||
let (client, out_receiver) = RpcClient::new();
|
||||
let session = RpcSession::new(client.clone(), api.clone());
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -123,6 +123,11 @@ export class RawClient {
|
||||
}
|
||||
|
||||
|
||||
public checkQr(accountId: T.U32, qrContent: string): Promise<T.Qr> {
|
||||
return (this._transport.request('check_qr', [accountId, qrContent] as RPC.Params)) as Promise<T.Qr>;
|
||||
}
|
||||
|
||||
|
||||
public getConfig(accountId: T.U32, key: string): Promise<(string|null)> {
|
||||
return (this._transport.request('get_config', [accountId, key] as RPC.Params)) as Promise<(string|null)>;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export type U32=number;
|
||||
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
|
||||
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
|
||||
export type Qr=({"askVerifyContact":{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;};}|{"askVerifyGroup":{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;};}|{"fprOk":{"contact_id":U32;};}|{"fprMismatch":{"contact_id":(U32|null);};}|{"fprWithoutAddr":{"fingerprint":string;};}|{"account":{"domain":string;};}|{"webrtcInstance":{"domain":string;"instance_pattern":string;};}|{"addr":{"contact_id":U32;};}|{"url":{"url":string;};}|{"text":{"text":string;};}|{"withdrawVerifyContact":{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;};}|{"withdrawVerifyGroup":{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;};}|{"reviveVerifyContact":{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;};}|{"reviveVerifyGroup":{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;};});
|
||||
export type Usize=number;
|
||||
export type ChatListEntry=[U32,U32];
|
||||
export type I64=number;
|
||||
@@ -60,7 +61,8 @@ export type Viewtype=("Unknown"|
|
||||
"Webxdc");
|
||||
export type I32=number;
|
||||
export type U64=number;
|
||||
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quotedText":(string|null);"quotedMessageId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);};
|
||||
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
|
||||
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quotedText":(string|null);"quotedMessageId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"downloadState":DownloadState;};
|
||||
export type WebxdcMessageInfo={
|
||||
/**
|
||||
* The name of the app.
|
||||
@@ -94,4 +96,4 @@ export type WebxdcMessageInfo={
|
||||
* Implementations may offer an menu or a button to open this URL.
|
||||
*/
|
||||
"sourceCodeUrl":(string|null);};
|
||||
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,U32,null,U32,null,U32,(U32)[],U32,U32,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,null,U32,U32,null,U32,string,string,U32,U32,U32,U32,(U32)[],U32,U32,Message,U32,(U32)[],Record<U32,Message>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,string,U32,U32];
|
||||
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,U32,null,U32,null,U32,(U32)[],U32,U32,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,null,U32,U32,null,U32,string,string,U32,U32,U32,U32,(U32)[],U32,U32,Message,U32,(U32)[],Record<U32,Message>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,string,U32,U32];
|
||||
|
||||
@@ -115,7 +115,7 @@ export class AccountManager extends EventEmitter {
|
||||
debug('Started event handler')
|
||||
}
|
||||
|
||||
startJsonRpcHandler(callback: ((response: string) => void) | null, apiVersion: string = "v0") {
|
||||
startJsonRpcHandler(callback: ((response: string) => void) | null) {
|
||||
if (this.dcn_accounts === null) {
|
||||
throw new Error('dcn_account is null')
|
||||
}
|
||||
@@ -126,7 +126,7 @@ export class AccountManager extends EventEmitter {
|
||||
throw new Error('jsonrpc was started already')
|
||||
}
|
||||
|
||||
binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, apiVersion, callback.bind(this))
|
||||
binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, callback.bind(this))
|
||||
debug('Started JSON-RPC handler')
|
||||
this.jsonRpcStarted = true
|
||||
}
|
||||
|
||||
@@ -3313,10 +3313,9 @@ static void call_accounts_js_jsonrpc_handler(napi_env env, napi_value js_callbac
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_accounts_start_jsonrpc) {
|
||||
NAPI_ARGV(3);
|
||||
NAPI_ARGV(2);
|
||||
NAPI_DCN_ACCOUNTS();
|
||||
NAPI_ARGV_UTF8_MALLOC(api_version, 1);
|
||||
napi_value callback = argv[2];
|
||||
napi_value callback = argv[1];
|
||||
|
||||
TRACE("calling..");
|
||||
napi_value async_resource_name;
|
||||
@@ -3339,7 +3338,7 @@ NAPI_METHOD(dcn_accounts_start_jsonrpc) {
|
||||
TRACE("done");
|
||||
|
||||
dcn_accounts->gc = 0;
|
||||
dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts, api_version);
|
||||
dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts);
|
||||
|
||||
TRACE("creating uv thread..");
|
||||
uv_thread_create(&dcn_accounts->jsonrpc_thread, accounts_jsonrpc_thread_func, dcn_accounts);
|
||||
|
||||
Reference in New Issue
Block a user