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 78 kB

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