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

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