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

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