You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

DiscordRestApiClient.cs 68 KiB

8 years ago
8 years ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280
  1. #pragma warning disable CS1591
  2. using Discord.API.Rest;
  3. using Discord.Net;
  4. using Discord.Net.Converters;
  5. using Discord.Net.Queue;
  6. using Discord.Net.Rest;
  7. using Newtonsoft.Json;
  8. using System;
  9. using System.Collections.Concurrent;
  10. using System.Collections.Generic;
  11. using System.Diagnostics;
  12. using System.Globalization;
  13. using System.IO;
  14. using System.Linq;
  15. using System.Linq.Expressions;
  16. using System.Net;
  17. using System.Runtime.CompilerServices;
  18. using System.Text;
  19. using System.Threading;
  20. using System.Threading.Tasks;
  21. namespace Discord.API
  22. {
  23. internal class DiscordRestApiClient : IDisposable
  24. {
  25. private static readonly ConcurrentDictionary<string, Func<BucketIds, string>> _bucketIdGenerators = new ConcurrentDictionary<string, Func<BucketIds, string>>();
  26. public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } }
  27. private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>();
  28. protected readonly JsonSerializer _serializer;
  29. protected readonly SemaphoreSlim _stateLock;
  30. private readonly RestClientProvider RestClientProvider;
  31. protected bool _isDisposed;
  32. private CancellationTokenSource _loginCancelToken;
  33. public RetryMode DefaultRetryMode { get; }
  34. public string UserAgent { get; }
  35. internal RequestQueue RequestQueue { get; }
  36. public LoginState LoginState { get; private set; }
  37. public TokenType AuthTokenType { get; private set; }
  38. internal string AuthToken { get; private set; }
  39. internal IRestClient RestClient { get; private set; }
  40. internal ulong? CurrentUserId { get; set;}
  41. public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry,
  42. JsonSerializer serializer = null)
  43. {
  44. RestClientProvider = restClientProvider;
  45. UserAgent = userAgent;
  46. DefaultRetryMode = defaultRetryMode;
  47. _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() };
  48. RequestQueue = new RequestQueue();
  49. _stateLock = new SemaphoreSlim(1, 1);
  50. SetBaseUrl(DiscordConfig.APIUrl);
  51. }
  52. internal void SetBaseUrl(string baseUrl)
  53. {
  54. RestClient = RestClientProvider(baseUrl);
  55. RestClient.SetHeader("accept", "*/*");
  56. RestClient.SetHeader("user-agent", UserAgent);
  57. RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken));
  58. }
  59. internal static string GetPrefixedToken(TokenType tokenType, string token)
  60. {
  61. switch (tokenType)
  62. {
  63. case TokenType.Bot:
  64. return $"Bot {token}";
  65. case TokenType.Bearer:
  66. return $"Bearer {token}";
  67. case TokenType.User:
  68. return token;
  69. default:
  70. throw new ArgumentException("Unknown OAuth token type", nameof(tokenType));
  71. }
  72. }
  73. internal virtual void Dispose(bool disposing)
  74. {
  75. if (!_isDisposed)
  76. {
  77. if (disposing)
  78. {
  79. _loginCancelToken?.Dispose();
  80. (RestClient as IDisposable)?.Dispose();
  81. }
  82. _isDisposed = true;
  83. }
  84. }
  85. public void Dispose() => Dispose(true);
  86. public async Task LoginAsync(TokenType tokenType, string token, RequestOptions options = null)
  87. {
  88. await _stateLock.WaitAsync().ConfigureAwait(false);
  89. try
  90. {
  91. await LoginInternalAsync(tokenType, token, options).ConfigureAwait(false);
  92. }
  93. finally { _stateLock.Release(); }
  94. }
  95. private async Task LoginInternalAsync(TokenType tokenType, string token, RequestOptions options = null)
  96. {
  97. if (LoginState != LoginState.LoggedOut)
  98. await LogoutInternalAsync().ConfigureAwait(false);
  99. LoginState = LoginState.LoggingIn;
  100. try
  101. {
  102. _loginCancelToken = new CancellationTokenSource();
  103. AuthTokenType = TokenType.User;
  104. AuthToken = null;
  105. await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false);
  106. RestClient.SetCancelToken(_loginCancelToken.Token);
  107. AuthTokenType = tokenType;
  108. AuthToken = token;
  109. if (tokenType != TokenType.Webhook)
  110. RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken));
  111. LoginState = LoginState.LoggedIn;
  112. }
  113. catch (Exception)
  114. {
  115. await LogoutInternalAsync().ConfigureAwait(false);
  116. throw;
  117. }
  118. }
  119. public async Task LogoutAsync()
  120. {
  121. await _stateLock.WaitAsync().ConfigureAwait(false);
  122. try
  123. {
  124. await LogoutInternalAsync().ConfigureAwait(false);
  125. }
  126. finally { _stateLock.Release(); }
  127. }
  128. private async Task LogoutInternalAsync()
  129. {
  130. //An exception here will lock the client into the unusable LoggingOut state, but that's probably fine since our client is in an undefined state too.
  131. if (LoginState == LoginState.LoggedOut) return;
  132. LoginState = LoginState.LoggingOut;
  133. try { _loginCancelToken?.Cancel(false); }
  134. catch { }
  135. await DisconnectInternalAsync().ConfigureAwait(false);
  136. await RequestQueue.ClearAsync().ConfigureAwait(false);
  137. await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false);
  138. RestClient.SetCancelToken(CancellationToken.None);
  139. CurrentUserId = null;
  140. LoginState = LoginState.LoggedOut;
  141. }
  142. internal virtual Task ConnectInternalAsync() => Task.Delay(0);
  143. internal virtual Task DisconnectInternalAsync() => Task.Delay(0);
  144. //Core
  145. internal Task SendAsync(string method, Expression<Func<string>> endpointExpr, BucketIds ids,
  146. ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
  147. => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options);
  148. public async Task SendAsync(string method, string endpoint,
  149. string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
  150. {
  151. options = options ?? new RequestOptions();
  152. options.HeaderOnly = true;
  153. options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId;
  154. options.IsClientBucket = AuthTokenType == TokenType.User;
  155. var request = new RestRequest(RestClient, method, endpoint, options);
  156. await SendInternalAsync(method, endpoint, request).ConfigureAwait(false);
  157. }
  158. internal Task SendJsonAsync(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids,
  159. ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
  160. => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options);
  161. public async Task SendJsonAsync(string method, string endpoint, object payload,
  162. string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
  163. {
  164. options = options ?? new RequestOptions();
  165. options.HeaderOnly = true;
  166. options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId;
  167. options.IsClientBucket = AuthTokenType == TokenType.User;
  168. var json = payload != null ? SerializeJson(payload) : null;
  169. var request = new JsonRestRequest(RestClient, method, endpoint, json, options);
  170. await SendInternalAsync(method, endpoint, request).ConfigureAwait(false);
  171. }
  172. internal Task SendMultipartAsync(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids,
  173. ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
  174. => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options);
  175. public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs,
  176. string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
  177. {
  178. options = options ?? new RequestOptions();
  179. options.HeaderOnly = true;
  180. options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId;
  181. options.IsClientBucket = AuthTokenType == TokenType.User;
  182. var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options);
  183. await SendInternalAsync(method, endpoint, request).ConfigureAwait(false);
  184. }
  185. internal Task<TResponse> SendAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, BucketIds ids,
  186. ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class
  187. => SendAsync<TResponse>(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options);
  188. public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint,
  189. string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class
  190. {
  191. options = options ?? new RequestOptions();
  192. options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId;
  193. options.IsClientBucket = AuthTokenType == TokenType.User;
  194. var request = new RestRequest(RestClient, method, endpoint, options);
  195. return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false));
  196. }
  197. internal Task<TResponse> SendJsonAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids,
  198. ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class
  199. => SendJsonAsync<TResponse>(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options);
  200. public async Task<TResponse> SendJsonAsync<TResponse>(string method, string endpoint, object payload,
  201. string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class
  202. {
  203. options = options ?? new RequestOptions();
  204. options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId;
  205. options.IsClientBucket = AuthTokenType == TokenType.User;
  206. var json = payload != null ? SerializeJson(payload) : null;
  207. var request = new JsonRestRequest(RestClient, method, endpoint, json, options);
  208. return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false));
  209. }
  210. internal Task<TResponse> SendMultipartAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids,
  211. ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
  212. => SendMultipartAsync<TResponse>(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options);
  213. public async Task<TResponse> SendMultipartAsync<TResponse>(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs,
  214. string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
  215. {
  216. options = options ?? new RequestOptions();
  217. options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId;
  218. options.IsClientBucket = AuthTokenType == TokenType.User;
  219. var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options);
  220. return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false));
  221. }
  222. private async Task<Stream> SendInternalAsync(string method, string endpoint, RestRequest request)
  223. {
  224. if (!request.Options.IgnoreState)
  225. CheckState();
  226. if (request.Options.RetryMode == null)
  227. request.Options.RetryMode = DefaultRetryMode;
  228. var stopwatch = Stopwatch.StartNew();
  229. var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false);
  230. stopwatch.Stop();
  231. double milliseconds = ToMilliseconds(stopwatch);
  232. await _sentRequestEvent.InvokeAsync(method, endpoint, milliseconds).ConfigureAwait(false);
  233. return responseStream;
  234. }
  235. //Auth
  236. public async Task ValidateTokenAsync(RequestOptions options = null)
  237. {
  238. options = RequestOptions.CreateOrClone(options);
  239. await SendAsync("GET", () => "auth/login", new BucketIds(), options: options).ConfigureAwait(false);
  240. }
  241. //Channels
  242. public async Task<Channel> GetChannelAsync(ulong channelId, RequestOptions options = null)
  243. {
  244. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  245. options = RequestOptions.CreateOrClone(options);
  246. try
  247. {
  248. var ids = new BucketIds(channelId: channelId);
  249. return await SendAsync<Channel>("GET", () => $"channels/{channelId}", ids, options: options).ConfigureAwait(false);
  250. }
  251. catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
  252. }
  253. public async Task<Channel> GetChannelAsync(ulong guildId, ulong channelId, RequestOptions options = null)
  254. {
  255. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  256. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  257. options = RequestOptions.CreateOrClone(options);
  258. try
  259. {
  260. var ids = new BucketIds(channelId: channelId);
  261. var model = await SendAsync<Channel>("GET", () => $"channels/{channelId}", ids, options: options).ConfigureAwait(false);
  262. if (!model.GuildId.IsSpecified || model.GuildId.Value != guildId)
  263. return null;
  264. return model;
  265. }
  266. catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
  267. }
  268. public async Task<IReadOnlyCollection<Channel>> GetGuildChannelsAsync(ulong guildId, RequestOptions options = null)
  269. {
  270. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  271. options = RequestOptions.CreateOrClone(options);
  272. var ids = new BucketIds(guildId: guildId);
  273. return await SendAsync<IReadOnlyCollection<Channel>>("GET", () => $"guilds/{guildId}/channels", ids, options: options).ConfigureAwait(false);
  274. }
  275. public async Task<Channel> CreateGuildChannelAsync(ulong guildId, CreateGuildChannelParams args, RequestOptions options = null)
  276. {
  277. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  278. Preconditions.NotNull(args, nameof(args));
  279. Preconditions.GreaterThan(args.Bitrate, 0, nameof(args.Bitrate));
  280. Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name));
  281. options = RequestOptions.CreateOrClone(options);
  282. var ids = new BucketIds(guildId: guildId);
  283. return await SendJsonAsync<Channel>("POST", () => $"guilds/{guildId}/channels", args, ids, options: options).ConfigureAwait(false);
  284. }
  285. public async Task<Channel> DeleteChannelAsync(ulong channelId, RequestOptions options = null)
  286. {
  287. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  288. options = RequestOptions.CreateOrClone(options);
  289. var ids = new BucketIds(channelId: channelId);
  290. return await SendAsync<Channel>("DELETE", () => $"channels/{channelId}", ids, options: options).ConfigureAwait(false);
  291. }
  292. public async Task<Channel> ModifyGuildChannelAsync(ulong channelId, Rest.ModifyGuildChannelParams args, RequestOptions options = null)
  293. {
  294. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  295. Preconditions.NotNull(args, nameof(args));
  296. Preconditions.AtLeast(args.Position, 0, nameof(args.Position));
  297. Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name));
  298. options = RequestOptions.CreateOrClone(options);
  299. var ids = new BucketIds(channelId: channelId);
  300. return await SendJsonAsync<Channel>("PATCH", () => $"channels/{channelId}", args, ids, options: options).ConfigureAwait(false);
  301. }
  302. public async Task<Channel> ModifyGuildChannelAsync(ulong channelId, Rest.ModifyTextChannelParams args, RequestOptions options = null)
  303. {
  304. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  305. Preconditions.NotNull(args, nameof(args));
  306. Preconditions.AtLeast(args.Position, 0, nameof(args.Position));
  307. Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name));
  308. options = RequestOptions.CreateOrClone(options);
  309. var ids = new BucketIds(channelId: channelId);
  310. return await SendJsonAsync<Channel>("PATCH", () => $"channels/{channelId}", args, ids, options: options).ConfigureAwait(false);
  311. }
  312. public async Task<Channel> ModifyGuildChannelAsync(ulong channelId, Rest.ModifyVoiceChannelParams args, RequestOptions options = null)
  313. {
  314. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  315. Preconditions.NotNull(args, nameof(args));
  316. Preconditions.AtLeast(args.Bitrate, 8000, nameof(args.Bitrate));
  317. Preconditions.AtLeast(args.UserLimit, 0, nameof(args.UserLimit));
  318. Preconditions.AtLeast(args.Position, 0, nameof(args.Position));
  319. Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name));
  320. options = RequestOptions.CreateOrClone(options);
  321. var ids = new BucketIds(channelId: channelId);
  322. return await SendJsonAsync<Channel>("PATCH", () => $"channels/{channelId}", args, ids, options: options).ConfigureAwait(false);
  323. }
  324. public async Task ModifyGuildChannelsAsync(ulong guildId, IEnumerable<Rest.ModifyGuildChannelsParams> args, RequestOptions options = null)
  325. {
  326. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  327. Preconditions.NotNull(args, nameof(args));
  328. options = RequestOptions.CreateOrClone(options);
  329. var channels = args.ToArray();
  330. switch (channels.Length)
  331. {
  332. case 0:
  333. return;
  334. case 1:
  335. await ModifyGuildChannelAsync(channels[0].Id, new Rest.ModifyGuildChannelParams { Position = channels[0].Position }).ConfigureAwait(false);
  336. break;
  337. default:
  338. var ids = new BucketIds(guildId: guildId);
  339. await SendJsonAsync("PATCH", () => $"guilds/{guildId}/channels", channels, ids, options: options).ConfigureAwait(false);
  340. break;
  341. }
  342. }
  343. public async Task AddRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null)
  344. {
  345. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  346. Preconditions.NotEqual(userId, 0, nameof(userId));
  347. Preconditions.NotEqual(roleId, 0, nameof(roleId));
  348. options = RequestOptions.CreateOrClone(options);
  349. var ids = new BucketIds(guildId: guildId);
  350. await SendAsync("PUT", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options);
  351. }
  352. public async Task RemoveRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null)
  353. {
  354. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  355. Preconditions.NotEqual(userId, 0, nameof(userId));
  356. Preconditions.NotEqual(roleId, 0, nameof(roleId));
  357. options = RequestOptions.CreateOrClone(options);
  358. var ids = new BucketIds(guildId: guildId);
  359. await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options);
  360. }
  361. //Channel Messages
  362. public async Task<Message> GetChannelMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null)
  363. {
  364. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  365. Preconditions.NotEqual(messageId, 0, nameof(messageId));
  366. options = RequestOptions.CreateOrClone(options);
  367. try
  368. {
  369. var ids = new BucketIds(channelId: channelId);
  370. return await SendAsync<Message>("GET", () => $"channels/{channelId}/messages/{messageId}", ids, options: options).ConfigureAwait(false);
  371. }
  372. catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
  373. }
  374. public async Task<IReadOnlyCollection<Message>> GetChannelMessagesAsync(ulong channelId, GetChannelMessagesParams args, RequestOptions options = null)
  375. {
  376. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  377. Preconditions.NotNull(args, nameof(args));
  378. Preconditions.AtLeast(args.Limit, 0, nameof(args.Limit));
  379. Preconditions.AtMost(args.Limit, DiscordConfig.MaxMessagesPerBatch, nameof(args.Limit));
  380. options = RequestOptions.CreateOrClone(options);
  381. int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxMessagesPerBatch);
  382. ulong? relativeId = args.RelativeMessageId.IsSpecified ? args.RelativeMessageId.Value : (ulong?)null;
  383. string relativeDir;
  384. switch (args.RelativeDirection.GetValueOrDefault(Direction.Before))
  385. {
  386. case Direction.Before:
  387. default:
  388. relativeDir = "before";
  389. break;
  390. case Direction.After:
  391. relativeDir = "after";
  392. break;
  393. case Direction.Around:
  394. relativeDir = "around";
  395. break;
  396. }
  397. var ids = new BucketIds(channelId: channelId);
  398. Expression<Func<string>> endpoint;
  399. if (relativeId != null)
  400. endpoint = () => $"channels/{channelId}/messages?limit={limit}&{relativeDir}={relativeId}";
  401. else
  402. endpoint = () => $"channels/{channelId}/messages?limit={limit}";
  403. return await SendAsync<IReadOnlyCollection<Message>>("GET", endpoint, ids, options: options).ConfigureAwait(false);
  404. }
  405. public async Task<Message> CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null)
  406. {
  407. Preconditions.NotNull(args, nameof(args));
  408. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  409. if (!args.Embed.IsSpecified || args.Embed.Value == null)
  410. Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content));
  411. if (args.Content.Length > DiscordConfig.MaxMessageSize)
  412. throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
  413. options = RequestOptions.CreateOrClone(options);
  414. var ids = new BucketIds(channelId: channelId);
  415. return await SendJsonAsync<Message>("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
  416. }
  417. public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null)
  418. {
  419. if (AuthTokenType != TokenType.Webhook)
  420. throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token.");
  421. Preconditions.NotNull(args, nameof(args));
  422. Preconditions.NotEqual(webhookId, 0, nameof(webhookId));
  423. if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0)
  424. Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content));
  425. if (args.Content.Length > DiscordConfig.MaxMessageSize)
  426. throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
  427. options = RequestOptions.CreateOrClone(options);
  428. await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
  429. }
  430. public async Task<Message> UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null)
  431. {
  432. Preconditions.NotNull(args, nameof(args));
  433. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  434. options = RequestOptions.CreateOrClone(options);
  435. if (args.Content.GetValueOrDefault(null) == null)
  436. args.Content = "";
  437. else if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize)
  438. throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
  439. var ids = new BucketIds(channelId: channelId);
  440. return await SendMultipartAsync<Message>("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
  441. }
  442. public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null)
  443. {
  444. if (AuthTokenType != TokenType.Webhook)
  445. throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token.");
  446. Preconditions.NotNull(args, nameof(args));
  447. Preconditions.NotEqual(webhookId, 0, nameof(webhookId));
  448. options = RequestOptions.CreateOrClone(options);
  449. if (args.Content.GetValueOrDefault(null) == null)
  450. args.Content = "";
  451. else if (args.Content.IsSpecified)
  452. {
  453. if (args.Content.Value == null)
  454. args.Content = "";
  455. if (args.Content.Value?.Length > DiscordConfig.MaxMessageSize)
  456. throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
  457. }
  458. await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
  459. }
  460. public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null)
  461. {
  462. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  463. Preconditions.NotEqual(messageId, 0, nameof(messageId));
  464. options = RequestOptions.CreateOrClone(options);
  465. var ids = new BucketIds(channelId: channelId);
  466. await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}", ids, options: options).ConfigureAwait(false);
  467. }
  468. public async Task DeleteMessagesAsync(ulong channelId, DeleteMessagesParams args, RequestOptions options = null)
  469. {
  470. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  471. Preconditions.NotNull(args, nameof(args));
  472. Preconditions.NotNull(args.MessageIds, nameof(args.MessageIds));
  473. Preconditions.AtMost(args.MessageIds.Length, 100, nameof(args.MessageIds.Length));
  474. Preconditions.YoungerThanTwoWeeks(args.MessageIds, nameof(args.MessageIds));
  475. options = RequestOptions.CreateOrClone(options);
  476. switch (args.MessageIds.Length)
  477. {
  478. case 0:
  479. return;
  480. case 1:
  481. await DeleteMessageAsync(channelId, args.MessageIds[0]).ConfigureAwait(false);
  482. break;
  483. default:
  484. var ids = new BucketIds(channelId: channelId);
  485. await SendJsonAsync("POST", () => $"channels/{channelId}/messages/bulk-delete", args, ids, options: options).ConfigureAwait(false);
  486. break;
  487. }
  488. }
  489. public async Task<Message> ModifyMessageAsync(ulong channelId, ulong messageId, Rest.ModifyMessageParams args, RequestOptions options = null)
  490. {
  491. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  492. Preconditions.NotEqual(messageId, 0, nameof(messageId));
  493. Preconditions.NotNull(args, nameof(args));
  494. if (args.Content.IsSpecified)
  495. {
  496. if (!args.Embed.IsSpecified)
  497. Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content));
  498. if (args.Content.Value.Length > DiscordConfig.MaxMessageSize)
  499. throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
  500. }
  501. options = RequestOptions.CreateOrClone(options);
  502. var ids = new BucketIds(channelId: channelId);
  503. return await SendJsonAsync<Message>("PATCH", () => $"channels/{channelId}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
  504. }
  505. public async Task AddReactionAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null)
  506. {
  507. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  508. Preconditions.NotEqual(messageId, 0, nameof(messageId));
  509. Preconditions.NotNullOrWhitespace(emoji, nameof(emoji));
  510. options = RequestOptions.CreateOrClone(options);
  511. var ids = new BucketIds(channelId: channelId);
  512. await SendAsync("PUT", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/@me", ids, options: options).ConfigureAwait(false);
  513. }
  514. public async Task RemoveReactionAsync(ulong channelId, ulong messageId, ulong userId, string emoji, RequestOptions options = null)
  515. {
  516. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  517. Preconditions.NotEqual(messageId, 0, nameof(messageId));
  518. Preconditions.NotNullOrWhitespace(emoji, nameof(emoji));
  519. options = RequestOptions.CreateOrClone(options);
  520. var ids = new BucketIds(channelId: channelId);
  521. await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/{userId}", ids, options: options).ConfigureAwait(false);
  522. }
  523. public async Task RemoveAllReactionsAsync(ulong channelId, ulong messageId, RequestOptions options = null)
  524. {
  525. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  526. Preconditions.NotEqual(messageId, 0, nameof(messageId));
  527. options = RequestOptions.CreateOrClone(options);
  528. var ids = new BucketIds(channelId: channelId);
  529. await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions", ids, options: options).ConfigureAwait(false);
  530. }
  531. public async Task<IReadOnlyCollection<User>> GetReactionUsersAsync(ulong channelId, ulong messageId, string emoji, GetReactionUsersParams args, RequestOptions options = null)
  532. {
  533. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  534. Preconditions.NotEqual(messageId, 0, nameof(messageId));
  535. Preconditions.NotNullOrWhitespace(emoji, nameof(emoji));
  536. Preconditions.NotNull(args, nameof(args));
  537. Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit));
  538. Preconditions.AtMost(args.Limit, DiscordConfig.MaxUsersPerBatch, nameof(args.Limit));
  539. Preconditions.GreaterThan(args.AfterUserId, 0, nameof(args.AfterUserId));
  540. options = RequestOptions.CreateOrClone(options);
  541. int limit = args.Limit.GetValueOrDefault(int.MaxValue);
  542. ulong afterUserId = args.AfterUserId.GetValueOrDefault(0);
  543. var ids = new BucketIds(channelId: channelId);
  544. Expression<Func<string>> endpoint = () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}";
  545. return await SendAsync<IReadOnlyCollection<User>>("GET", endpoint, ids, options: options).ConfigureAwait(false);
  546. }
  547. public async Task AckMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null)
  548. {
  549. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  550. Preconditions.NotEqual(messageId, 0, nameof(messageId));
  551. options = RequestOptions.CreateOrClone(options);
  552. var ids = new BucketIds(channelId: channelId);
  553. await SendAsync("POST", () => $"channels/{channelId}/messages/{messageId}/ack", ids, options: options).ConfigureAwait(false);
  554. }
  555. public async Task TriggerTypingIndicatorAsync(ulong channelId, RequestOptions options = null)
  556. {
  557. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  558. options = RequestOptions.CreateOrClone(options);
  559. var ids = new BucketIds(channelId: channelId);
  560. await SendAsync("POST", () => $"channels/{channelId}/typing", ids, options: options).ConfigureAwait(false);
  561. }
  562. //Channel Permissions
  563. public async Task ModifyChannelPermissionsAsync(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null)
  564. {
  565. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  566. Preconditions.NotEqual(targetId, 0, nameof(targetId));
  567. Preconditions.NotNull(args, nameof(args));
  568. options = RequestOptions.CreateOrClone(options);
  569. var ids = new BucketIds(channelId: channelId);
  570. await SendJsonAsync("PUT", () => $"channels/{channelId}/permissions/{targetId}", args, ids, options: options).ConfigureAwait(false);
  571. }
  572. public async Task DeleteChannelPermissionAsync(ulong channelId, ulong targetId, RequestOptions options = null)
  573. {
  574. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  575. Preconditions.NotEqual(targetId, 0, nameof(targetId));
  576. options = RequestOptions.CreateOrClone(options);
  577. var ids = new BucketIds(channelId: channelId);
  578. await SendAsync("DELETE", () => $"channels/{channelId}/permissions/{targetId}", ids, options: options).ConfigureAwait(false);
  579. }
  580. //Channel Pins
  581. public async Task AddPinAsync(ulong channelId, ulong messageId, RequestOptions options = null)
  582. {
  583. Preconditions.GreaterThan(channelId, 0, nameof(channelId));
  584. Preconditions.GreaterThan(messageId, 0, nameof(messageId));
  585. options = RequestOptions.CreateOrClone(options);
  586. var ids = new BucketIds(channelId: channelId);
  587. await SendAsync("PUT", () => $"channels/{channelId}/pins/{messageId}", ids, options: options).ConfigureAwait(false);
  588. }
  589. public async Task RemovePinAsync(ulong channelId, ulong messageId, RequestOptions options = null)
  590. {
  591. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  592. Preconditions.NotEqual(messageId, 0, nameof(messageId));
  593. options = RequestOptions.CreateOrClone(options);
  594. var ids = new BucketIds(channelId: channelId);
  595. await SendAsync("DELETE", () => $"channels/{channelId}/pins/{messageId}", ids, options: options).ConfigureAwait(false);
  596. }
  597. public async Task<IReadOnlyCollection<Message>> GetPinsAsync(ulong channelId, RequestOptions options = null)
  598. {
  599. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  600. options = RequestOptions.CreateOrClone(options);
  601. var ids = new BucketIds(channelId: channelId);
  602. return await SendAsync<IReadOnlyCollection<Message>>("GET", () => $"channels/{channelId}/pins", ids, options: options).ConfigureAwait(false);
  603. }
  604. //Channel Recipients
  605. public async Task AddGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null)
  606. {
  607. Preconditions.GreaterThan(channelId, 0, nameof(channelId));
  608. Preconditions.GreaterThan(userId, 0, nameof(userId));
  609. options = RequestOptions.CreateOrClone(options);
  610. var ids = new BucketIds(channelId: channelId);
  611. await SendAsync("PUT", () => $"channels/{channelId}/recipients/{userId}", ids, options: options).ConfigureAwait(false);
  612. }
  613. public async Task RemoveGroupRecipientAsync(ulong channelId, ulong userId, RequestOptions options = null)
  614. {
  615. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  616. Preconditions.NotEqual(userId, 0, nameof(userId));
  617. options = RequestOptions.CreateOrClone(options);
  618. var ids = new BucketIds(channelId: channelId);
  619. await SendAsync("DELETE", () => $"channels/{channelId}/recipients/{userId}", ids, options: options).ConfigureAwait(false);
  620. }
  621. //Guilds
  622. public async Task<Guild> GetGuildAsync(ulong guildId, RequestOptions options = null)
  623. {
  624. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  625. options = RequestOptions.CreateOrClone(options);
  626. try
  627. {
  628. var ids = new BucketIds(guildId: guildId);
  629. return await SendAsync<Guild>("GET", () => $"guilds/{guildId}", ids, options: options).ConfigureAwait(false);
  630. }
  631. catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
  632. }
  633. public async Task<Guild> CreateGuildAsync(CreateGuildParams args, RequestOptions options = null)
  634. {
  635. Preconditions.NotNull(args, nameof(args));
  636. Preconditions.NotNullOrWhitespace(args.Name, nameof(args.Name));
  637. Preconditions.NotNullOrWhitespace(args.RegionId, nameof(args.RegionId));
  638. options = RequestOptions.CreateOrClone(options);
  639. return await SendJsonAsync<Guild>("POST", () => "guilds", args, new BucketIds(), options: options).ConfigureAwait(false);
  640. }
  641. public async Task<Guild> DeleteGuildAsync(ulong guildId, RequestOptions options = null)
  642. {
  643. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  644. options = RequestOptions.CreateOrClone(options);
  645. var ids = new BucketIds(guildId: guildId);
  646. return await SendAsync<Guild>("DELETE", () => $"guilds/{guildId}", ids, options: options).ConfigureAwait(false);
  647. }
  648. public async Task<Guild> LeaveGuildAsync(ulong guildId, RequestOptions options = null)
  649. {
  650. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  651. options = RequestOptions.CreateOrClone(options);
  652. var ids = new BucketIds(guildId: guildId);
  653. return await SendAsync<Guild>("DELETE", () => $"users/@me/guilds/{guildId}", ids, options: options).ConfigureAwait(false);
  654. }
  655. public async Task<Guild> ModifyGuildAsync(ulong guildId, Rest.ModifyGuildParams args, RequestOptions options = null)
  656. {
  657. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  658. Preconditions.NotNull(args, nameof(args));
  659. Preconditions.NotEqual(args.AfkChannelId, 0, nameof(args.AfkChannelId));
  660. Preconditions.AtLeast(args.AfkTimeout, 0, nameof(args.AfkTimeout));
  661. Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name));
  662. Preconditions.GreaterThan(args.OwnerId, 0, nameof(args.OwnerId));
  663. Preconditions.NotNull(args.RegionId, nameof(args.RegionId));
  664. options = RequestOptions.CreateOrClone(options);
  665. var ids = new BucketIds(guildId: guildId);
  666. return await SendJsonAsync<Guild>("PATCH", () => $"guilds/{guildId}", args, ids, options: options).ConfigureAwait(false);
  667. }
  668. public async Task<GetGuildPruneCountResponse> BeginGuildPruneAsync(ulong guildId, GuildPruneParams args, RequestOptions options = null)
  669. {
  670. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  671. Preconditions.NotNull(args, nameof(args));
  672. Preconditions.AtLeast(args.Days, 1, nameof(args.Days));
  673. options = RequestOptions.CreateOrClone(options);
  674. var ids = new BucketIds(guildId: guildId);
  675. return await SendJsonAsync<GetGuildPruneCountResponse>("POST", () => $"guilds/{guildId}/prune", args, ids, options: options).ConfigureAwait(false);
  676. }
  677. public async Task<GetGuildPruneCountResponse> GetGuildPruneCountAsync(ulong guildId, GuildPruneParams args, RequestOptions options = null)
  678. {
  679. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  680. Preconditions.NotNull(args, nameof(args));
  681. Preconditions.AtLeast(args.Days, 1, nameof(args.Days));
  682. options = RequestOptions.CreateOrClone(options);
  683. var ids = new BucketIds(guildId: guildId);
  684. return await SendAsync<GetGuildPruneCountResponse>("GET", () => $"guilds/{guildId}/prune?days={args.Days}", ids, options: options).ConfigureAwait(false);
  685. }
  686. //Guild Bans
  687. public async Task<IReadOnlyCollection<Ban>> GetGuildBansAsync(ulong guildId, RequestOptions options = null)
  688. {
  689. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  690. options = RequestOptions.CreateOrClone(options);
  691. var ids = new BucketIds(guildId: guildId);
  692. return await SendAsync<IReadOnlyCollection<Ban>>("GET", () => $"guilds/{guildId}/bans", ids, options: options).ConfigureAwait(false);
  693. }
  694. public async Task CreateGuildBanAsync(ulong guildId, ulong userId, CreateGuildBanParams args, RequestOptions options = null)
  695. {
  696. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  697. Preconditions.NotEqual(userId, 0, nameof(userId));
  698. Preconditions.NotNull(args, nameof(args));
  699. Preconditions.AtLeast(args.DeleteMessageDays, 0, nameof(args.DeleteMessageDays), "Prune length must be within [0, 7]");
  700. Preconditions.AtMost(args.DeleteMessageDays, 7, nameof(args.DeleteMessageDays), "Prune length must be within [0, 7]");
  701. options = RequestOptions.CreateOrClone(options);
  702. var ids = new BucketIds(guildId: guildId);
  703. string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={args.Reason}";
  704. await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false);
  705. }
  706. public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null)
  707. {
  708. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  709. Preconditions.NotEqual(userId, 0, nameof(userId));
  710. options = RequestOptions.CreateOrClone(options);
  711. var ids = new BucketIds(guildId: guildId);
  712. await SendAsync("DELETE", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false);
  713. }
  714. //Guild Embeds
  715. public async Task<GuildEmbed> GetGuildEmbedAsync(ulong guildId, RequestOptions options = null)
  716. {
  717. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  718. options = RequestOptions.CreateOrClone(options);
  719. try
  720. {
  721. var ids = new BucketIds(guildId: guildId);
  722. return await SendAsync<GuildEmbed>("GET", () => $"guilds/{guildId}/embed", ids, options: options).ConfigureAwait(false);
  723. }
  724. catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
  725. }
  726. public async Task<GuildEmbed> ModifyGuildEmbedAsync(ulong guildId, Rest.ModifyGuildEmbedParams args, RequestOptions options = null)
  727. {
  728. Preconditions.NotNull(args, nameof(args));
  729. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  730. options = RequestOptions.CreateOrClone(options);
  731. var ids = new BucketIds(guildId: guildId);
  732. return await SendJsonAsync<GuildEmbed>("PATCH", () => $"guilds/{guildId}/embed", args, ids, options: options).ConfigureAwait(false);
  733. }
  734. //Guild Integrations
  735. public async Task<IReadOnlyCollection<Integration>> GetGuildIntegrationsAsync(ulong guildId, RequestOptions options = null)
  736. {
  737. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  738. options = RequestOptions.CreateOrClone(options);
  739. var ids = new BucketIds(guildId: guildId);
  740. return await SendAsync<IReadOnlyCollection<Integration>>("GET", () => $"guilds/{guildId}/integrations", ids, options: options).ConfigureAwait(false);
  741. }
  742. public async Task<Integration> CreateGuildIntegrationAsync(ulong guildId, CreateGuildIntegrationParams args, RequestOptions options = null)
  743. {
  744. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  745. Preconditions.NotNull(args, nameof(args));
  746. Preconditions.NotEqual(args.Id, 0, nameof(args.Id));
  747. options = RequestOptions.CreateOrClone(options);
  748. var ids = new BucketIds(guildId: guildId);
  749. return await SendAsync<Integration>("POST", () => $"guilds/{guildId}/integrations", ids, options: options).ConfigureAwait(false);
  750. }
  751. public async Task<Integration> DeleteGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null)
  752. {
  753. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  754. Preconditions.NotEqual(integrationId, 0, nameof(integrationId));
  755. options = RequestOptions.CreateOrClone(options);
  756. var ids = new BucketIds(guildId: guildId);
  757. return await SendAsync<Integration>("DELETE", () => $"guilds/{guildId}/integrations/{integrationId}", ids, options: options).ConfigureAwait(false);
  758. }
  759. public async Task<Integration> ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, Rest.ModifyGuildIntegrationParams args, RequestOptions options = null)
  760. {
  761. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  762. Preconditions.NotEqual(integrationId, 0, nameof(integrationId));
  763. Preconditions.NotNull(args, nameof(args));
  764. Preconditions.AtLeast(args.ExpireBehavior, 0, nameof(args.ExpireBehavior));
  765. Preconditions.AtLeast(args.ExpireGracePeriod, 0, nameof(args.ExpireGracePeriod));
  766. options = RequestOptions.CreateOrClone(options);
  767. var ids = new BucketIds(guildId: guildId);
  768. return await SendJsonAsync<Integration>("PATCH", () => $"guilds/{guildId}/integrations/{integrationId}", args, ids, options: options).ConfigureAwait(false);
  769. }
  770. public async Task<Integration> SyncGuildIntegrationAsync(ulong guildId, ulong integrationId, RequestOptions options = null)
  771. {
  772. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  773. Preconditions.NotEqual(integrationId, 0, nameof(integrationId));
  774. options = RequestOptions.CreateOrClone(options);
  775. var ids = new BucketIds(guildId: guildId);
  776. return await SendAsync<Integration>("POST", () => $"guilds/{guildId}/integrations/{integrationId}/sync", ids, options: options).ConfigureAwait(false);
  777. }
  778. //Guild Invites
  779. public async Task<Invite> GetInviteAsync(string inviteId, RequestOptions options = null)
  780. {
  781. Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId));
  782. options = RequestOptions.CreateOrClone(options);
  783. //Remove trailing slash
  784. if (inviteId[inviteId.Length - 1] == '/')
  785. inviteId = inviteId.Substring(0, inviteId.Length - 1);
  786. //Remove leading URL
  787. int index = inviteId.LastIndexOf('/');
  788. if (index >= 0)
  789. inviteId = inviteId.Substring(index + 1);
  790. try
  791. {
  792. return await SendAsync<Invite>("GET", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false);
  793. }
  794. catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
  795. }
  796. public async Task<IReadOnlyCollection<InviteMetadata>> GetGuildInvitesAsync(ulong guildId, RequestOptions options = null)
  797. {
  798. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  799. options = RequestOptions.CreateOrClone(options);
  800. var ids = new BucketIds(guildId: guildId);
  801. return await SendAsync<IReadOnlyCollection<InviteMetadata>>("GET", () => $"guilds/{guildId}/invites", ids, options: options).ConfigureAwait(false);
  802. }
  803. public async Task<IReadOnlyCollection<InviteMetadata>> GetChannelInvitesAsync(ulong channelId, RequestOptions options = null)
  804. {
  805. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  806. options = RequestOptions.CreateOrClone(options);
  807. var ids = new BucketIds(channelId: channelId);
  808. return await SendAsync<IReadOnlyCollection<InviteMetadata>>("GET", () => $"channels/{channelId}/invites", ids, options: options).ConfigureAwait(false);
  809. }
  810. public async Task<InviteMetadata> CreateChannelInviteAsync(ulong channelId, CreateChannelInviteParams args, RequestOptions options = null)
  811. {
  812. Preconditions.NotEqual(channelId, 0, nameof(channelId));
  813. Preconditions.NotNull(args, nameof(args));
  814. Preconditions.AtLeast(args.MaxAge, 0, nameof(args.MaxAge));
  815. Preconditions.AtLeast(args.MaxUses, 0, nameof(args.MaxUses));
  816. options = RequestOptions.CreateOrClone(options);
  817. var ids = new BucketIds(channelId: channelId);
  818. return await SendJsonAsync<InviteMetadata>("POST", () => $"channels/{channelId}/invites", args, ids, options: options).ConfigureAwait(false);
  819. }
  820. public async Task<Invite> DeleteInviteAsync(string inviteId, RequestOptions options = null)
  821. {
  822. Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId));
  823. options = RequestOptions.CreateOrClone(options);
  824. return await SendAsync<Invite>("DELETE", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false);
  825. }
  826. public async Task AcceptInviteAsync(string inviteId, RequestOptions options = null)
  827. {
  828. Preconditions.NotNullOrEmpty(inviteId, nameof(inviteId));
  829. options = RequestOptions.CreateOrClone(options);
  830. await SendAsync("POST", () => $"invites/{inviteId}", new BucketIds(), options: options).ConfigureAwait(false);
  831. }
  832. //Guild Members
  833. public async Task<GuildMember> GetGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null)
  834. {
  835. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  836. Preconditions.NotEqual(userId, 0, nameof(userId));
  837. options = RequestOptions.CreateOrClone(options);
  838. try
  839. {
  840. var ids = new BucketIds(guildId: guildId);
  841. return await SendAsync<GuildMember>("GET", () => $"guilds/{guildId}/members/{userId}", ids, options: options).ConfigureAwait(false);
  842. }
  843. catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
  844. }
  845. public async Task<IReadOnlyCollection<GuildMember>> GetGuildMembersAsync(ulong guildId, GetGuildMembersParams args, RequestOptions options = null)
  846. {
  847. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  848. Preconditions.NotNull(args, nameof(args));
  849. Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit));
  850. Preconditions.AtMost(args.Limit, DiscordConfig.MaxUsersPerBatch, nameof(args.Limit));
  851. Preconditions.GreaterThan(args.AfterUserId, 0, nameof(args.AfterUserId));
  852. options = RequestOptions.CreateOrClone(options);
  853. int limit = args.Limit.GetValueOrDefault(int.MaxValue);
  854. ulong afterUserId = args.AfterUserId.GetValueOrDefault(0);
  855. var ids = new BucketIds(guildId: guildId);
  856. Expression<Func<string>> endpoint = () => $"guilds/{guildId}/members?limit={limit}&after={afterUserId}";
  857. return await SendAsync<IReadOnlyCollection<GuildMember>>("GET", endpoint, ids, options: options).ConfigureAwait(false);
  858. }
  859. public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, string reason, RequestOptions options = null)
  860. {
  861. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  862. Preconditions.NotEqual(userId, 0, nameof(userId));
  863. options = RequestOptions.CreateOrClone(options);
  864. var ids = new BucketIds(guildId: guildId);
  865. reason = string.IsNullOrWhiteSpace(reason) ? "" : $"?reason={reason}";
  866. await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}{reason}", ids, options: options).ConfigureAwait(false);
  867. }
  868. public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.ModifyGuildMemberParams args, RequestOptions options = null)
  869. {
  870. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  871. Preconditions.NotEqual(userId, 0, nameof(userId));
  872. Preconditions.NotNull(args, nameof(args));
  873. options = RequestOptions.CreateOrClone(options);
  874. bool isCurrentUser = userId == CurrentUserId;
  875. if (isCurrentUser && args.Nickname.IsSpecified)
  876. {
  877. var nickArgs = new Rest.ModifyCurrentUserNickParams(args.Nickname.Value ?? "");
  878. await ModifyMyNickAsync(guildId, nickArgs).ConfigureAwait(false);
  879. args.Nickname = Optional.Create<string>(); //Remove
  880. }
  881. if (!isCurrentUser || args.Deaf.IsSpecified || args.Mute.IsSpecified || args.RoleIds.IsSpecified)
  882. {
  883. var ids = new BucketIds(guildId: guildId);
  884. await SendJsonAsync("PATCH", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options).ConfigureAwait(false);
  885. }
  886. }
  887. //Guild Roles
  888. public async Task<IReadOnlyCollection<Role>> GetGuildRolesAsync(ulong guildId, RequestOptions options = null)
  889. {
  890. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  891. options = RequestOptions.CreateOrClone(options);
  892. var ids = new BucketIds(guildId: guildId);
  893. return await SendAsync<IReadOnlyCollection<Role>>("GET", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false);
  894. }
  895. public async Task<Role> CreateGuildRoleAsync(ulong guildId, RequestOptions options = null)
  896. {
  897. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  898. options = RequestOptions.CreateOrClone(options);
  899. var ids = new BucketIds(guildId: guildId);
  900. return await SendAsync<Role>("POST", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false);
  901. }
  902. public async Task DeleteGuildRoleAsync(ulong guildId, ulong roleId, RequestOptions options = null)
  903. {
  904. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  905. Preconditions.NotEqual(roleId, 0, nameof(roleId));
  906. options = RequestOptions.CreateOrClone(options);
  907. var ids = new BucketIds(guildId: guildId);
  908. await SendAsync("DELETE", () => $"guilds/{guildId}/roles/{roleId}", ids, options: options).ConfigureAwait(false);
  909. }
  910. public async Task<Role> ModifyGuildRoleAsync(ulong guildId, ulong roleId, Rest.ModifyGuildRoleParams args, RequestOptions options = null)
  911. {
  912. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  913. Preconditions.NotEqual(roleId, 0, nameof(roleId));
  914. Preconditions.NotNull(args, nameof(args));
  915. Preconditions.AtLeast(args.Color, 0, nameof(args.Color));
  916. Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name));
  917. options = RequestOptions.CreateOrClone(options);
  918. var ids = new BucketIds(guildId: guildId);
  919. return await SendJsonAsync<Role>("PATCH", () => $"guilds/{guildId}/roles/{roleId}", args, ids, options: options).ConfigureAwait(false);
  920. }
  921. public async Task<IReadOnlyCollection<Role>> ModifyGuildRolesAsync(ulong guildId, IEnumerable<Rest.ModifyGuildRolesParams> args, RequestOptions options = null)
  922. {
  923. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  924. Preconditions.NotNull(args, nameof(args));
  925. options = RequestOptions.CreateOrClone(options);
  926. var ids = new BucketIds(guildId: guildId);
  927. return await SendJsonAsync<IReadOnlyCollection<Role>>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false);
  928. }
  929. //Users
  930. public async Task<User> GetUserAsync(ulong userId, RequestOptions options = null)
  931. {
  932. Preconditions.NotEqual(userId, 0, nameof(userId));
  933. options = RequestOptions.CreateOrClone(options);
  934. try
  935. {
  936. return await SendAsync<User>("GET", () => $"users/{userId}", new BucketIds(), options: options).ConfigureAwait(false);
  937. }
  938. catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; }
  939. }
  940. //Current User/DMs
  941. public async Task<User> GetMyUserAsync(RequestOptions options = null)
  942. {
  943. options = RequestOptions.CreateOrClone(options);
  944. return await SendAsync<User>("GET", () => "users/@me", new BucketIds(), options: options).ConfigureAwait(false);
  945. }
  946. public async Task<IReadOnlyCollection<Connection>> GetMyConnectionsAsync(RequestOptions options = null)
  947. {
  948. options = RequestOptions.CreateOrClone(options);
  949. return await SendAsync<IReadOnlyCollection<Connection>>("GET", () => "users/@me/connections", new BucketIds(), options: options).ConfigureAwait(false);
  950. }
  951. public async Task<IReadOnlyCollection<Channel>> GetMyPrivateChannelsAsync(RequestOptions options = null)
  952. {
  953. options = RequestOptions.CreateOrClone(options);
  954. return await SendAsync<IReadOnlyCollection<Channel>>("GET", () => "users/@me/channels", new BucketIds(), options: options).ConfigureAwait(false);
  955. }
  956. public async Task<IReadOnlyCollection<UserGuild>> GetMyGuildsAsync(GetGuildSummariesParams args, RequestOptions options = null)
  957. {
  958. Preconditions.NotNull(args, nameof(args));
  959. Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit));
  960. Preconditions.AtMost(args.Limit, DiscordConfig.MaxGuildsPerBatch, nameof(args.Limit));
  961. Preconditions.GreaterThan(args.AfterGuildId, 0, nameof(args.AfterGuildId));
  962. options = RequestOptions.CreateOrClone(options);
  963. int limit = args.Limit.GetValueOrDefault(int.MaxValue);
  964. ulong afterGuildId = args.AfterGuildId.GetValueOrDefault(0);
  965. return await SendAsync<IReadOnlyCollection<UserGuild>>("GET", () => $"users/@me/guilds?limit={limit}&after={afterGuildId}", new BucketIds(), options: options).ConfigureAwait(false);
  966. }
  967. public async Task<Application> GetMyApplicationAsync(RequestOptions options = null)
  968. {
  969. options = RequestOptions.CreateOrClone(options);
  970. return await SendAsync<Application>("GET", () => "oauth2/applications/@me", new BucketIds(), options: options).ConfigureAwait(false);
  971. }
  972. public async Task<User> ModifySelfAsync(Rest.ModifyCurrentUserParams args, RequestOptions options = null)
  973. {
  974. Preconditions.NotNull(args, nameof(args));
  975. Preconditions.NotNullOrEmpty(args.Username, nameof(args.Username));
  976. options = RequestOptions.CreateOrClone(options);
  977. return await SendJsonAsync<User>("PATCH", () => "users/@me", args, new BucketIds(), options: options).ConfigureAwait(false);
  978. }
  979. public async Task ModifyMyNickAsync(ulong guildId, Rest.ModifyCurrentUserNickParams args, RequestOptions options = null)
  980. {
  981. Preconditions.NotNull(args, nameof(args));
  982. Preconditions.NotNull(args.Nickname, nameof(args.Nickname));
  983. options = RequestOptions.CreateOrClone(options);
  984. var ids = new BucketIds(guildId: guildId);
  985. await SendJsonAsync("PATCH", () => $"guilds/{guildId}/members/@me/nick", args, ids, options: options).ConfigureAwait(false);
  986. }
  987. public async Task<Channel> CreateDMChannelAsync(CreateDMChannelParams args, RequestOptions options = null)
  988. {
  989. Preconditions.NotNull(args, nameof(args));
  990. Preconditions.GreaterThan(args.RecipientId, 0, nameof(args.RecipientId));
  991. options = RequestOptions.CreateOrClone(options);
  992. return await SendJsonAsync<Channel>("POST", () => "users/@me/channels", args, new BucketIds(), options: options).ConfigureAwait(false);
  993. }
  994. //Voice Regions
  995. public async Task<IReadOnlyCollection<VoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null)
  996. {
  997. options = RequestOptions.CreateOrClone(options);
  998. return await SendAsync<IReadOnlyCollection<VoiceRegion>>("GET", () => "voice/regions", new BucketIds(), options: options).ConfigureAwait(false);
  999. }
  1000. public async Task<IReadOnlyCollection<VoiceRegion>> GetGuildVoiceRegionsAsync(ulong guildId, RequestOptions options = null)
  1001. {
  1002. Preconditions.NotEqual(guildId, 0, nameof(guildId));
  1003. options = RequestOptions.CreateOrClone(options);
  1004. var ids = new BucketIds(guildId: guildId);
  1005. return await SendAsync<IReadOnlyCollection<VoiceRegion>>("GET", () => $"guilds/{guildId}/regions", ids, options: options).ConfigureAwait(false);
  1006. }
  1007. //Helpers
  1008. protected void CheckState()
  1009. {
  1010. if (LoginState != LoginState.LoggedIn)
  1011. throw new InvalidOperationException("Client is not logged in.");
  1012. }
  1013. protected static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2);
  1014. protected string SerializeJson(object value)
  1015. {
  1016. var sb = new StringBuilder(256);
  1017. using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture))
  1018. using (JsonWriter writer = new JsonTextWriter(text))
  1019. _serializer.Serialize(writer, value);
  1020. return sb.ToString();
  1021. }
  1022. protected T DeserializeJson<T>(Stream jsonStream)
  1023. {
  1024. using (TextReader text = new StreamReader(jsonStream))
  1025. using (JsonReader reader = new JsonTextReader(text))
  1026. return _serializer.Deserialize<T>(reader);
  1027. }
  1028. internal class BucketIds
  1029. {
  1030. public ulong GuildId { get; internal set; }
  1031. public ulong ChannelId { get; internal set; }
  1032. internal BucketIds(ulong guildId = 0, ulong channelId = 0)
  1033. {
  1034. GuildId = guildId;
  1035. ChannelId = channelId;
  1036. }
  1037. internal object[] ToArray()
  1038. => new object[] { GuildId, ChannelId };
  1039. internal static int? GetIndex(string name)
  1040. {
  1041. switch (name)
  1042. {
  1043. case "guildId": return 0;
  1044. case "channelId": return 1;
  1045. default:
  1046. return null;
  1047. }
  1048. }
  1049. }
  1050. private static string GetEndpoint(Expression<Func<string>> endpointExpr)
  1051. {
  1052. return endpointExpr.Compile()();
  1053. }
  1054. private static string GetBucketId(BucketIds ids, Expression<Func<string>> endpointExpr, TokenType tokenType, string callingMethod)
  1055. {
  1056. return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr))(ids);
  1057. }
  1058. private static Func<BucketIds, string> CreateBucketId(Expression<Func<string>> endpoint)
  1059. {
  1060. try
  1061. {
  1062. //Is this a constant string?
  1063. if (endpoint.Body.NodeType == ExpressionType.Constant)
  1064. return x => (endpoint.Body as ConstantExpression).Value.ToString();
  1065. var builder = new StringBuilder();
  1066. var methodCall = endpoint.Body as MethodCallExpression;
  1067. var methodArgs = methodCall.Arguments.ToArray();
  1068. string format = (methodArgs[0] as ConstantExpression).Value as string;
  1069. //Unpack the array, if one exists (happens with 4+ parameters)
  1070. if (methodArgs.Length > 1 && methodArgs[1].NodeType == ExpressionType.NewArrayInit)
  1071. {
  1072. var arrayExpr = methodArgs[1] as NewArrayExpression;
  1073. var elements = arrayExpr.Expressions.ToArray();
  1074. Array.Resize(ref methodArgs, elements.Length + 1);
  1075. Array.Copy(elements, 0, methodArgs, 1, elements.Length);
  1076. }
  1077. int endIndex = format.IndexOf('?'); //Dont include params
  1078. if (endIndex == -1)
  1079. endIndex = format.Length;
  1080. int lastIndex = 0;
  1081. while (true)
  1082. {
  1083. int leftIndex = format.IndexOf("{", lastIndex);
  1084. if (leftIndex == -1 || leftIndex > endIndex)
  1085. {
  1086. builder.Append(format, lastIndex, endIndex - lastIndex);
  1087. break;
  1088. }
  1089. builder.Append(format, lastIndex, leftIndex - lastIndex);
  1090. int rightIndex = format.IndexOf("}", leftIndex);
  1091. int argId = int.Parse(format.Substring(leftIndex + 1, rightIndex - leftIndex - 1));
  1092. string fieldName = GetFieldName(methodArgs[argId + 1]);
  1093. int? mappedId;
  1094. mappedId = BucketIds.GetIndex(fieldName);
  1095. if(!mappedId.HasValue && rightIndex != endIndex && format.Length > rightIndex + 1 && format[rightIndex + 1] == '/') //Ignore the next slash
  1096. rightIndex++;
  1097. if (mappedId.HasValue)
  1098. builder.Append($"{{{mappedId.Value}}}");
  1099. lastIndex = rightIndex + 1;
  1100. }
  1101. format = builder.ToString();
  1102. return x => string.Format(format, x.ToArray());
  1103. }
  1104. catch (Exception ex)
  1105. {
  1106. throw new InvalidOperationException("Failed to generate the bucket id for this operation", ex);
  1107. }
  1108. }
  1109. private static string GetFieldName(Expression expr)
  1110. {
  1111. if (expr.NodeType == ExpressionType.Convert)
  1112. expr = (expr as UnaryExpression).Operand;
  1113. if (expr.NodeType != ExpressionType.MemberAccess)
  1114. throw new InvalidOperationException("Unsupported expression");
  1115. return (expr as MemberExpression).Member.Name;
  1116. }
  1117. }
  1118. }