Update MikrotikAPI to support more options.

New connectivity methods.
This commit is contained in:
Tech Garage 2024-01-25 19:30:59 +03:30
parent 3bea9768ba
commit 8d9b340aeb
9 changed files with 507 additions and 233 deletions

View file

@ -43,6 +43,33 @@ namespace MikrotikAPI
return json.ToModel<List<ServerTraffic>>(); return json.ToModel<List<ServerTraffic>>();
} }
public async Task<string> GetIPAddresses()
{
var json = await SendGetRequestAsync(Endpoints.IPAddress);
return json;
}
public async Task<CreationStatus> CreateIPAddress(IPAddressCreateModel ipAddress)
{
return await CreateItem<IPAddress>(Endpoints.IPAddress, ipAddress);
}
public async Task<CreationStatus> UpdateIPAddress(IPAddressUpdateModel ipAddress)
{
var itemJson = JObject.FromObject(ipAddress, new JsonSerializer
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
});
return await UpdateItem(Endpoints.IPAddress, itemJson, ipAddress.Id);
}
public async Task<List<IPAddress>> GetServerIPAddress(string Interface)
{
var json = await SendGetRequestAsync(Endpoints.IPAddress + "?interface=" + Interface);
return json.ToModel<List<IPAddress>>();
}
public async Task<List<WGPeer>> GetUsersAsync() public async Task<List<WGPeer>> GetUsersAsync()
{ {
string json = await SendGetRequestAsync(Endpoints.WireguardPeers); string json = await SendGetRequestAsync(Endpoints.WireguardPeers);
@ -55,6 +82,12 @@ namespace MikrotikAPI
return users.Find(u => u.Id == id); return users.Find(u => u.Id == id);
} }
public async Task<WGPeerLastHandshake> GetUserHandshake(string id)
{
var json = await SendRequestBase(RequestMethod.GET, Endpoints.WireguardPeers + $"/{id}?.proplist=last-handshake");
return json.ToModel<WGPeerLastHandshake>();
}
public async Task<MTInfo> GetInfo() public async Task<MTInfo> GetInfo()
{ {
var json = await SendGetRequestAsync(Endpoints.SystemResource); var json = await SendGetRequestAsync(Endpoints.SystemResource);
@ -67,6 +100,21 @@ namespace MikrotikAPI
return json.ToModel<MTIdentity>(); return json.ToModel<MTIdentity>();
} }
public async Task<CreationStatus> SetName(MTIdentityUpdateModel identity) // Create Model
{
var itemJson = JObject.FromObject(identity, new JsonSerializer
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
}).ToString();
var json = await SendPostRequestAsync(Endpoints.SystemIdentity + "/set", itemJson);
return json == "[]" ? new()
{
Success = true,
Item = await GetName()
} : json.ToModel<CreationStatus>();
}
public async Task<LoginStatus> TryConnectAsync() public async Task<LoginStatus> TryConnectAsync()
{ {
var connection = await SendGetRequestAsync(Endpoints.Empty, true); var connection = await SendGetRequestAsync(Endpoints.Empty, true);
@ -85,6 +133,27 @@ namespace MikrotikAPI
return json.ToModel<List<Job>>(); return json.ToModel<List<Job>>();
} }
public async Task<DNS> GetDNS()
{
var json = await SendGetRequestAsync(Endpoints.DNS);
return json.ToModel<DNS>();
}
public async Task<CreationStatus> SetDNS(MTDNSUpdateModel dns) // Create Model
{
var itemJson = JObject.FromObject(dns, new JsonSerializer
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
}).ToString();
var json = await SendPostRequestAsync(Endpoints.DNS + "/set", itemJson);
return json == "[]" ? new()
{
Success = true,
Item = await GetDNS()
} : json.ToModel<CreationStatus>();
}
public async Task<string> KillJob(string JobID) public async Task<string> KillJob(string JobID)
{ {
return await SendDeleteRequestAsync($"{Endpoints.Jobs}/" + JobID); return await SendDeleteRequestAsync($"{Endpoints.Jobs}/" + JobID);
@ -92,271 +161,52 @@ namespace MikrotikAPI
public async Task<CreationStatus> CreateServer(WGServerCreateModel server) public async Task<CreationStatus> CreateServer(WGServerCreateModel server)
{ {
var json = await SendPutRequestAsync(Endpoints.Wireguard, server); return await CreateItem<WGServer>(Endpoints.Wireguard, server);
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success
};
} }
public async Task<CreationStatus> CreateUser(WGPeerCreateModel user) public async Task<CreationStatus> CreateUser(WGPeerCreateModel user)
{ {
var jsonData = JObject.Parse(JsonConvert.SerializeObject(user, new JsonSerializerSettings return await CreateItem<WGPeer>(Endpoints.WireguardPeers, user);
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
}));
var json = await SendPutRequestAsync(Endpoints.WireguardPeers, jsonData);
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
WGPeer peer = null;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
peer = JsonConvert.DeserializeObject<WGPeer>(json);
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success,
Item = peer ?? null
};
} }
public async Task<CreationStatus> UpdateServer(WGServerUpdateModel server) public async Task<CreationStatus> UpdateServer(WGServerUpdateModel server)
{ {
var serverJson = JObject.Parse(JsonConvert.SerializeObject(server, new JsonSerializerSettings var itemJson = JObject.FromObject(server, new JsonSerializer
{ {
NullValueHandling = NullValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore DefaultValueHandling = DefaultValueHandling.Ignore
})); });
var json = await SendPatchRequestAsync($"{Endpoints.Wireguard}/{server.Id}", serverJson); return await UpdateItem(Endpoints.Wireguard, itemJson, server.Id);
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
WGServer srv = null;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
srv = JsonConvert.DeserializeObject<WGServer>(json);
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success,
Item = srv ?? null
};
} }
public async Task<CreationStatus> UpdateUser(WGPeerUpdateModel user) public async Task<CreationStatus> UpdateUser(WGPeerUpdateModel user)
{ {
var userJson = JObject.Parse(JsonConvert.SerializeObject(user, new JsonSerializerSettings var itemJson = JObject.FromObject(user, new JsonSerializer
{ {
NullValueHandling = NullValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore DefaultValueHandling = DefaultValueHandling.Ignore
})); });
var json = await SendPatchRequestAsync($"{Endpoints.WireguardPeers}/{user.Id}", userJson); return await UpdateItem(Endpoints.WireguardPeers, itemJson, user.Id);
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
WGPeer peer = null;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
peer = JsonConvert.DeserializeObject<WGPeer>(json);
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success,
Item = peer ?? null
};
} }
public async Task<CreationStatus> SetServerEnabled(WGEnability enability) public async Task<CreationStatus> SetServerEnabled(WGEnability enability)
{ {
var json = await SendPatchRequestAsync($"{Endpoints.Wireguard}/{enability.ID}", new { disabled = enability.Disabled }); return await UpdateItem(Endpoints.Wireguard, new { disabled = enability.Disabled }, enability.ID);
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
WGPeer peer = null;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
peer = JsonConvert.DeserializeObject<WGPeer>(json);
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success,
Item = peer ?? null
};
} }
public async Task<CreationStatus> SetUserEnabled(WGEnability enability) public async Task<CreationStatus> SetUserEnabled(WGEnability enability)
{ {
var json = await SendPatchRequestAsync($"{Endpoints.WireguardPeers}/{enability.ID}", new { disabled = enability.Disabled }); return await UpdateItem(Endpoints.WireguardPeers, new { disabled = enability.Disabled }, enability.ID);
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
WGPeer peer = null;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
peer = JsonConvert.DeserializeObject<WGPeer>(json);
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success,
Item = peer ?? null
};
} }
public async Task<CreationStatus> DeleteServer(string id) public async Task<CreationStatus> DeleteServer(string id)
{ {
var json = await SendDeleteRequestAsync($"{Endpoints.Wireguard}/" + id); return await DeleteItem(Endpoints.Wireguard, id);
if (string.IsNullOrWhiteSpace(json))
{
return new()
{
Success = true
};
}
else
{
return new()
{
Success = false,
Item = json
};
}
} }
public async Task<CreationStatus> DeleteUser(string id) public async Task<CreationStatus> DeleteUser(string id)
{ {
var json = await SendDeleteRequestAsync($"{Endpoints.WireguardPeers}/" + id); return await DeleteItem(Endpoints.WireguardPeers, id);
if (string.IsNullOrWhiteSpace(json))
{
return new()
{
Success = true
};
}
else
{
return new()
{
Success = false,
Item = json
};
}
} }
public async Task<string> GetTrafficSpeed() public async Task<string> GetTrafficSpeed()
@ -364,7 +214,167 @@ namespace MikrotikAPI
return await SendPostRequestAsync(Endpoints.MonitorTraffic, "{\"interface\":\"ether1\",\"duration\":\"3s\"}"); return await SendPostRequestAsync(Endpoints.MonitorTraffic, "{\"interface\":\"ether1\",\"duration\":\"3s\"}");
} }
private async Task<string> SendRequestBase(RequestMethod Method, string Endpoint, object Data = null, bool IsTest = false) public async Task<List<Script>> GetScripts()
{
var json = await SendGetRequestAsync(Endpoints.Scripts);
return json.ToModel<List<Script>>();
}
public async Task<CreationStatus> CreateScript(ScriptCreateModel script)
{
return await CreateItem<Script>(Endpoints.Scripts, script);
}
public async Task<CreationStatus> DeleteScript(string id)
{
return await DeleteItem(Endpoints.Scripts, id);
}
public async Task<CreationStatus> UpdateScript(ScriptUpdateModel script)
{
return await UpdateItem(Endpoints.Scripts, script, script.Id);
}
public async Task<string> RunScript(string name)
{
return await SendPostRequestAsync(Endpoints.Execute, "{\"script\":\"" + name + "\"}");
}
public async Task<List<Scheduler>> GetSchedulers()
{
var json = await SendGetRequestAsync(Endpoints.Scheduler);
return json.ToModel<List<Scheduler>>();
}
public async Task<CreationStatus> CreateScheduler(SchedulerCreateModel scheduler)
{
return await CreateItem<Scheduler>(Endpoints.Scheduler, scheduler);
}
public async Task<List<IPPool>> GetIPPools()
{
var json = await SendGetRequestAsync(Endpoints.IPPool);
return json.ToModel<List<IPPool>>();
}
public async Task<CreationStatus> CreateIPPool(IPPoolCreateModel ipPool)
{
return await CreateItem<IPPool>(Endpoints.IPPool, ipPool);
}
public async Task<CreationStatus> UpdateIPPool(IPPoolUpdateModel ipPool)
{
var itemJson = JObject.FromObject(ipPool, new JsonSerializer
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
});
return await UpdateItem(Endpoints.IPPool, itemJson, ipPool.Id);
}
public async Task<CreationStatus> DeleteIPPool(string id)
{
return await DeleteItem(Endpoints.IPPool, id);
}
private async Task<CreationStatus> CreateItem<T>(string Endpoint, object ItemCreateModel)
{
var jsonData = JObject.Parse(JsonConvert.SerializeObject(ItemCreateModel, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
}));
var json = await SendPutRequestAsync(Endpoint, jsonData);
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
T item = default;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
item = JsonConvert.DeserializeObject<T>(json);
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success,
Item = item ?? default
};
}
private async Task<CreationStatus> DeleteItem(string Endpoint, string ItemID)
{
var json = await SendDeleteRequestAsync($"{Endpoint}/{ItemID}");
if (string.IsNullOrWhiteSpace(json))
{
return new()
{
Success = true
};
}
else
{
return new()
{
Success = false,
Item = json
};
}
}
private async Task<CreationStatus> UpdateItem<T>(string Endpoint, T item, string itemId)
{
var json = await SendPatchRequestAsync($"{Endpoint}/{itemId}", item);
var obj = JObject.Parse(json);
bool success = false;
string code = string.Empty, message = string.Empty, detail = string.Empty;
T itemType = default;
if (obj.TryGetValue(".id", out var Id))
{
success = true;
itemType = JsonConvert.DeserializeObject<T>(json);
}
else if (obj.TryGetValue("error", out var Error))
{
var error = JsonConvert.DeserializeObject<CreationStatus>(json);
success = false;
code = Error.Value<string>();
message = error.Message;
detail = error.Detail;
}
else
{
success = false;
message = "Failed";
detail = json;
};
return new()
{
Code = code,
Message = message,
Detail = detail,
Success = success,
Item = itemType
};
}
private async Task<string> SendRequestBase(RequestMethod Method, string Endpoint, object? Data = null, bool IsTest = false)
{ {
HttpClientHandler handler = new() HttpClientHandler handler = new()
{ {

View file

@ -11,6 +11,12 @@
public const string ActiveUsers = "user/active"; public const string ActiveUsers = "user/active";
public const string Jobs = "system/script/job"; public const string Jobs = "system/script/job";
public const string MonitorTraffic = "interface/monitor-traffic"; public const string MonitorTraffic = "interface/monitor-traffic";
public const string Scripts = "system/script";
public const string Scheduler = "system/scheduler";
public const string Execute = "execute";
public const string DNS = "ip/dns";
public const string IPPool = "ip/pool";
public const string IPAddress = "ip/address";
public static string Empty => string.Empty; public static string Empty => string.Empty;
} }

66
MikrotikAPI/Models/DNS.cs Normal file
View file

@ -0,0 +1,66 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MikrotikAPI.Models
{
public class DNS
{
[JsonProperty("allow-remote-requests")]
public bool AllowRemoteRequests { get; set; }
[JsonProperty("cache-max-ttl")]
public string CacheMaxTtl { get; set; }
[JsonProperty("cache-size")]
public int CacheSize { get; set; }
[JsonProperty("cache-used")]
public int CacheUsed { get; set; }
[JsonProperty("doh-max-concurrent-queries")]
public int DohMaxConcurrentQueries { get; set; }
[JsonProperty("doh-max-server-connections")]
public int DohMaxServerConnections { get; set; }
[JsonProperty("doh-timeout")]
public string DohTimeout { get; set; }
[JsonProperty("dynamic-servers")]
public string DynamicServers { get; set; }
[JsonProperty("max-concurrent-queries")]
public int MaxConcurrentQueries { get; set; }
[JsonProperty("max-concurrent-tcp-sessions")]
public int MaxConcurrentTcpSessions { get; set; }
[JsonProperty("max-udp-packet-size")]
public int MaxUdpPacketSize { get; set; }
[JsonProperty("query-server-timeout")]
public string QueryServerTimeout { get; set; }
[JsonProperty("query-total-timeout")]
public string QueryTotalTimeout { get; set; }
[JsonProperty("servers")]
public string Servers { get; set; }
[JsonProperty("use-doh-server")]
public string UseDohServer { get; set; }
[JsonProperty("verify-doh-cert")]
public bool VerifyDohCert { get; set; }
}
public class MTDNSUpdateModel
{
[JsonProperty("servers")]
public string Servers { get; set; }
}
}

View file

@ -0,0 +1,39 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MikrotikAPI.Models
{
public class IPAddress
{
[JsonProperty(".id")]
public string Id { get; set; }
[JsonProperty("actual-interface")]
public string ActualInterface { get; set; }
public string Address { get; set; }
public bool Disabled { get; set; }
public bool Dynamic { get; set; }
public string Interface { get; set; }
public bool Invalid { get; set; }
public string Network { get; set; }
}
public class IPAddressCreateModel
{
[JsonProperty("address")]
public string Address { get; set; }
[JsonProperty("interface")]
public string Interface { get; set; }
}
public class IPAddressUpdateModel
{
[JsonProperty(".id")]
public string Id { set; get; }
[JsonProperty("address")]
public string Address { get; set; }
}
}

View file

@ -0,0 +1,40 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MikrotikAPI.Models
{
public class IPPool
{
[JsonProperty(".id")]
public string Id { get; set; }
public string Name { get; set; }
public string Ranges { get; set; }
[JsonProperty("next-pool")]
public string NextPool { get; set; }
}
public class IPPoolCreateModel
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("ranges")]
public string Ranges { get; set; }
[JsonProperty("next-pool")]
public string? NextPool { get; set; }
}
public class IPPoolUpdateModel
{
[JsonProperty(".id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("ranges")]
public string Ranges { get; set; }
[JsonProperty("next-pool")]
public string NextPool { get; set; }
}
}

View file

@ -1,7 +1,15 @@
namespace MikrotikAPI.Models using Newtonsoft.Json;
namespace MikrotikAPI.Models
{ {
public class MTIdentity public class MTIdentity
{ {
public string Name { get; set; } public string Name { get; set; }
} }
public class MTIdentityUpdateModel
{
[JsonProperty("name")]
public string Name { get; set; }
}
} }

View file

@ -0,0 +1,45 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MikrotikAPI.Models
{
public class Scheduler
{
[JsonProperty(".id")]
public string Id { get; set; }
public string Name { get; set; }
public string Owner { get; set; }
[JsonProperty("start-date")]
public string StartDate { get; set; }
[JsonProperty("start-time")]
public string StartTime { get; set; }
public string Interval { get; set; }
public string Policy { get; set; }
[JsonProperty("run-count")]
public int RunCount { get; set; }
[JsonProperty("next-run")]
public string NextRun { get; set; }
[JsonProperty("on-event")]
public string OnEvent { get; set; }
public bool Disabled { get; set; }
}
public class SchedulerCreateModel
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("start-date")]
public string StartDate { get; set; }
[JsonProperty("start-time")]
public string StartTime { get; set; }
[JsonProperty("interval")]
public string Interval { get; set; }
[JsonProperty("policy")]
public string Policy { get; set; }
[JsonProperty("on-event")]
public string OnEvent { get; set; }
}
}

View file

@ -0,0 +1,52 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MikrotikAPI.Models
{
public class Script
{
[JsonProperty(".id")]
public string Id { get; set; }
[JsonProperty("dont-require-permissions")]
public bool DontRequiredPermissions { get; set; }
public bool Invalid { get; set; }
[JsonProperty("last-started")]
public string LastStarted { get; set; }
public string Name { get; set; }
public string Owner { get; set; }
public string Policy { get; set; }
[JsonProperty("run-count")]
public int RunCount { get; set; }
public string Source { get; set; }
}
public class ScriptCreateModel
{
[JsonProperty("dont-require-permissions")]
public bool DontRequiredPermissions { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("policy")]
public string Policy { get; set; }
[JsonProperty("source")]
public string Source { get; set; }
}
public class ScriptUpdateModel
{
[JsonProperty(".id")]
public string Id { get; set; }
[JsonProperty("dont-require-permissions")]
public bool DontRequiredPermissions { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("policy")]
public string Policy { get; set; }
[JsonProperty("source")]
public string Source { get; set; }
}
}

View file

@ -34,6 +34,14 @@ namespace MikrotikAPI.Models
public string TX { get; set; } public string TX { get; set; }
} }
public class WGPeerLastHandshake
{
[JsonProperty(".id")]
public string Id { get; set; }
[JsonProperty("last-handshake")]
public string? LastHandshake { get; set; }
}
public class WGPeerCreateModel public class WGPeerCreateModel
{ {
[JsonProperty("allowed-address")] [JsonProperty("allowed-address")]