From caa20f2c9cbf0d0fadea05fbd1cf5c833b02ea02 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Fri, 19 Jun 2020 01:32:28 -0500 Subject: [PATCH] Finish all guild related endpoints and add a guildID field to some types --- src/cache.nim | 12 +- src/channel.nim | 14 +- src/client.nim | 10 +- src/clientobjects.nim | 4 +- src/guild.nim | 595 +++++++++++++++++++++++++++++++++++++++++- src/member.nim | 58 +++- src/message.nim | 2 +- src/role.nim | 38 ++- 8 files changed, 709 insertions(+), 24 deletions(-) diff --git a/src/cache.nim b/src/cache.nim index d7853c2..5089814 100644 --- a/src/cache.nim +++ b/src/cache.nim @@ -26,8 +26,16 @@ proc getGuild*(cache: Cache, id: snowflake): Guild = if (guild.id == id): return guild - return newGuild(sendRequest(endpoint("/guild/" & $id), HttpGet, defaultHeaders(), + return newGuild(sendRequest(endpoint("/guilds/" & $id), HttpGet, defaultHeaders(), id, RateLimitBucketType.guild)) proc getChannelGuild*(channel: Channel, cache: Cache): Guild = - return cache.getGuild(channel.guildID) \ No newline at end of file + return cache.getGuild(channel.guildID) + +#[ proc getGuildMember*(guild: Guild, memberID: snowflake, cache: Cache): GuildMember = + for index, members in cache.members: + if (members.id == id): + return guild + + return newGuildMember(sendRequest(endpoint("/guilds/" & $guild.id & "/members/" & $memberID), + HttpGet, defaultHeaders(), id, RateLimitBucketType.guild)) ]# \ No newline at end of file diff --git a/src/channel.nim b/src/channel.nim index af0ba36..c376afa 100644 --- a/src/channel.nim +++ b/src/channel.nim @@ -31,18 +31,18 @@ type parentID*: snowflake ## ID of the parent category for a channel lastPinTimestamp*: string ## When the last pinned message was pinned - ChannelModify* = ref object - ## Use this type to modify a channel by setting each fields. + ChannelFields* = ref object + ## Use this type to modify or create a channel by setting each fields. name*: Option[string] `type`*: Option[ChannelType] - position*: Option[int] topic*: Option[string] - nsfw*: Option[bool] - rateLimitPerUser*: Option[int] bitrate*: Option[int] userLimit*: Option[int] + rateLimitPerUser*: Option[int] + position*: Option[int] permissionOverwrites*: Option[seq[Permissions]] ## Explicit permission overwrites for members and roles. parentID*: Option[snowflake] + nsfw*: Option[bool] Invite* = object ## Represents a code that when used, adds a user to a guild or group DM channel. @@ -140,14 +140,14 @@ proc sendMessage*(channel: Channel, content: string, tts: bool = false): Message defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), channel.id, RateLimitBucketType.channel, messagePayload)) -proc modifyChannel*(channel: Channel, modify: ChannelModify): Future[Channel] {.async.} = +proc modifyChannel*(channel: Channel, modify: ChannelFields): Future[Channel] {.async.} = ## Modifies the channel. ## ## Examples: ## ## .. code-block:: nim ## var chan = getChannel(703084913510973472) - ## chan = chan.modifyChannel(ChannelModify(topic: some("This is the channel topic"))) + ## chan = chan.modifyChannel(ChannelFields(topic: some("This is the channel topic"))) var modifyPayload = %*{} diff --git a/src/client.nim b/src/client.nim index e8aa4d4..aa3eb59 100644 --- a/src/client.nim +++ b/src/client.nim @@ -1,6 +1,6 @@ import websocket, asyncdispatch, json, httpClient, eventdispatcher, strformat import eventhandler, streams, nimcordutils, discordobject, user, cache, clientobjects -import strutils, channel, options, message, emoji +import strutils, channel, options, message, emoji, guild const nimcordMajor = 0 @@ -104,12 +104,15 @@ proc startConnection*(client: DiscordClient) {.async.} = proc newDiscordClient(tkn: string): DiscordClient = ## Create a DiscordClient using a token. + ## + ## Sets globalDiscordClient to the newly created client. globalToken = tkn var cac: Cache new(cac) result = DiscordClient(token: tkn, cache: cac) + globalDiscordClient = result var tokenStream = newFileStream("token.txt", fmRead) var tkn: string @@ -144,7 +147,7 @@ registerEventListener(EventType.evtMessageCreate, proc(bEvt: BaseEvent) = var channel: Channel = event.message.getMessageChannel(event.client.cache) if (channel != nil): discard channel.sendMessage("Modifing Channel!") - discard channel.modifyChannel(ChannelModify(topic: some(modifyTopic))) + discard channel.modifyChannel(ChannelFields(topic: some(modifyTopic))) elif (event.message.content.startsWith("?deleteChannel")): let channelID = getIDFromJson(event.message.content.substr(15)) var channel: Channel = event.client.cache.getChannel(channelID) @@ -166,6 +169,9 @@ registerEventListener(EventType.evtMessageCreate, proc(bEvt: BaseEvent) = let messages = channel.getMessages(MessagesGetRequest(limit: some(amount), before: some(event.message.id))) discard channel.bulkDeleteMessages(messages) elif (event.message.content.startsWith("?reactToMessage")): + var guild: Guild + discard guild.createGuildRole(name = some("Gamer Role"), color = some(0xff0000)) + var channel: Channel = event.message.getMessageChannel(event.client.cache) if (channel != nil): let emojis = @[newEmoji("⏮️"), newEmoji("⬅️"), newEmoji("⏹️"), newEmoji("➡️"), newEmoji("⏭️")] diff --git a/src/clientobjects.nim b/src/clientobjects.nim index ba4d71b..2a1808c 100644 --- a/src/clientobjects.nim +++ b/src/clientobjects.nim @@ -8,4 +8,6 @@ type DiscordClient* = ref object ws*: AsyncWebSocket heartbeatInterval*: int heartbeatAcked*: bool - lastSequence*: int \ No newline at end of file + lastSequence*: int + +var globalDiscordClient*: DiscordClient ## Global instance of the DiscordClient. \ No newline at end of file diff --git a/src/guild.nim b/src/guild.nim index a84e606..9f6ea71 100644 --- a/src/guild.nim +++ b/src/guild.nim @@ -1,4 +1,6 @@ -import json, discordobject, channel, member, options, nimcordutils, emoji, role, permission, httpcore +import json, discordobject, channel, member, options, nimcordutils, emoji +import role, permission, httpcore, strformat, image, asyncdispatch, user +import permission type VerificationLevel* = enum @@ -78,6 +80,68 @@ type approximateMemberCount*: int approximatePresenceCount*: int + GuildPreview* = ref object of DiscordObject + ## Represents a guild review. + name*: string + icon*: string + splash*: string + discoverySplash*: string + emojis*: seq[Emoji] + features*: seq[string] + approximateMemberCount*: int + approximatePresenceCount*: int + description*: string + + GuildBan* = ref object + ## A guild ban. + reason*: string + user*: User + + VoiceRegion* = ref object + ## Voice region. + id*: string + name*: string + vip*: bool + optimal*: bool + deprecated*: bool + custom*: bool + + IntegrationExpireBehavior* = enum + intExpireBehRemoveRole = 0, + intExpireBehKick = 1 + + IntegrationAccount* = ref object + id*: string + name*: string + + Integration* = ref object of DiscordObject + name*: string + `type`*: string ## Integration type (twitch, youtube, etc) + enabled*: bool + syncing*: bool + roleID*: snowflake + enableEmoticons*: bool + expireBehavior*: IntegrationExpireBehavior + expireGracePeriod*: int + user*: User + account*: IntegrationAccount + syncedAt*: string + + GuildWidget* = ref object + enabled*: bool + channelID*: snowflake + + GuildWidgetStyle* = enum + guildWidgetStyleShield = "shield", ## Shield style widget with Discord icon and guild members online count. + guildWidgetStyleBanner1 = "banner1", ## Large image with guild icon, name and online count. + ## "POWERED BY DISCORD" as the footer of the widget. + guildWidgetStyleBanner2 = "banner2", ## Smaller widget style with guild icon, name and online count. + ## Split on the right with Discord logo. + guildWidgetStyleBanner3 = "banner3", ## Large image with guild icon, name and online count. + ## In the footer, Discord logo on the left and "Chat Now" on the right. + guildWidgetStyleBanner4 = "banner4" + + proc newGuild*(json: JsonNode): Guild {.inline.} = ## Parses a Guild type from json. var g = Guild( @@ -113,7 +177,7 @@ proc newGuild*(json: JsonNode): Guild {.inline.} = if (json.contains("permissions")): g.permissions = newPermissions(json["permissions"]) for role in json["roles"]: - g.roles.add(newRole(role)) + g.roles.add(newRole(role, g.id)) for emoji in json["emojis"]: g.emojis.add(newEmoji(emoji)) #TODO features @@ -130,7 +194,7 @@ proc newGuild*(json: JsonNode): Guild {.inline.} = #TODO: voice_states if (json.contains("members")): for member in json["members"]: - g.members.insert(newGuildMember(member)) + g.members.insert(newGuildMember(member, g.id)) if (json.contains("channels")): for channel in json["channels"]: g.channels.insert(newChannel(channel)) @@ -157,6 +221,7 @@ proc createGuild*(name: string, region: Option[string], icon: Option[string], ve ## Create a new guild. ## ## Some restraints/notes for this endpoint: + ## * This endpoint is only available with bots that are in less than 10 guilds. ## * When using the roles parameter, the first member of the array is used ## to change properties of the guild's @everyone role. If you are trying to ## bootstrap a guild with additional roles, keep this in mind. @@ -231,4 +296,526 @@ proc createGuild*(name: string, region: Option[string], icon: Option[string], ve return newGuild(sendRequest(endpoint("/guilds"), HttpPost, defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), - 0, RateLimitBucketType.global, json)) \ No newline at end of file + 0, RateLimitBucketType.global, json)) + +proc getGuildPreview*(guild: Guild): GuildPreview = + ## Returns the guild preview object for the given id, even if the user is not in the guild. + ## Only available for public guilds! + let json = sendRequest(endpoint(fmt("/guilds/{guild.id}/preview")), HttpPost, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + guild.id, RateLimitBucketType.guild) + + result = GuildPreview( + id: getIDFromJson(json["id"].getStr()), + name: json["name"].getStr(), + icon: json["icon"].getStr(), + splash: json["splash"].getStr(), + discoverySplash: json["discovery_splash"].getStr(), + approximateMemberCount: json["approximate_member_count"].getInt(), + approximatePresenceCount: json["approximate_presence_count"].getInt(), + description: json["description"].getStr() + ) + + for emoji in json["emojis"]: + result.emojis.add(newEmoji(emoji)) + + for feature in json["features"]: + result.features.add(feature.getStr()) + +type GuildModify* = ref object + ## Use this type to modify a guild by setting each fields. + name*: Option[string] + region*: Option[string] + verificationLevel*: Option[VerificationLevel] + defaultMessageNotifications*: Option[MessageNotificationsLevel] + explicitContentFilter*: Option[ExplicitContentFilterLevel] + afkChannelID*: Option[snowflake] + afkTimeout*: Option[int] + icon*: Option[Image] + ownerID*: Option[snowflake] + splash*: Option[Image] + banner*: Option[Image] + systemChannelID*: Option[snowflake] + rulesChannelID*: Option[snowflake] + publicUpdatesChannelID*: Option[snowflake] + preferredLocale*: Option[string] + +proc modifyGuild*(guild: Guild, modify: GuildModify): Future[Guild] {.async.} = + ## Modifies the Guild. + ## + ## Examples: + ## + ## .. code-block:: nim + ## var guild = getGuild(703084913510973472) + ## guild = guild.modifyGuild(GuildModify(name: some("Epic Gamer Guild"))) + + var modifyPayload = %*{} + + if (modify.name.isSome): + modifyPayload.add("name", %modify.name.get()) + + if (modify.region.isSome): + modifyPayload.add("region", %modify.region.get()) + + if (modify.verificationLevel.isSome): + modifyPayload.add("verification_level", %modify.verificationLevel.get()) + + if (modify.defaultMessageNotifications.isSome): + modifyPayload.add("default_message_notifications", %modify.defaultMessageNotifications.get()) + + if (modify.explicitContentFilter.isSome): + modifyPayload.add("explicit_content_filter", %modify.explicitContentFilter.get()) + + if (modify.afkChannelID.isSome): + modifyPayload.add("afk_channel_id", %modify.afkChannelID.get()) + + if (modify.afkTimeout.isSome): + modifyPayload.add("afk_timeout", %modify.afkTimeout.get()) + + if (modify.icon.isSome): + modifyPayload.add("icon", %modify.icon.get().imageToDataURI()) + + if (modify.ownerID.isSome): + modifyPayload.add("owner_id", %modify.ownerID.get()) + + if (modify.splash.isSome): + modifyPayload.add("splash", %modify.splash.get().imageToDataURI()) + + if (modify.banner.isSome): + modifyPayload.add("banner", %modify.banner.get().imageToDataURI()) + + if (modify.systemChannelID.isSome): + modifyPayload.add("system_channel_id", %modify.systemChannelID.get()) + + if (modify.rulesChannelID.isSome): + modifyPayload.add("rules_channel_id", %modify.rulesChannelID.get()) + + if (modify.publicUpdatesChannelID.isSome): + modifyPayload.add("public_updates_channel_id", %modify.publicUpdatesChannelID.get()) + + if (modify.preferredLocale.isSome): + modifyPayload.add("preferred_locale", %modify.preferredLocale.get()) + + return newGuild(sendRequest(endpoint("/guilds/" & $guild.id), HttpPatch, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + guild.id, RateLimitBucketType.guild, modifyPayload)) + +proc deleteGuild*(guild: Guild) {.async.} = + ## Delete the guild. The bot must be the owner of the guild! + discard sendRequest(endpoint("/guilds/" & $guild.id), HttpDelete, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + +proc requestGuildChannels*(guild: var Guild): seq[Channel] = + ## Request all guild channels via Discord's REST API + ## Only use this if for some reason, guild.channels is inaccurate! + ## + ## Also updates the guild's channels when called. + let json = sendRequest(endpoint("/guilds/" & $guild.id & "/channels"), HttpGet, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + + for channel in json: + result.add(newChannel(channel)) + guild.channels = result + +proc createGuildChannel*(guild: var Guild, create: ChannelFields): Future[Channel] {.async.} = + ## Creates a new guild channel. + ## The name field must be set, if you dont a `Defect` exception will be raised. + ## The created channel will be added to the guild's `channels` field. + ## + ## Examples: + ## + ## .. code-block:: nim + ## let guild = getGuild(703084913510973472) + ## let channel = waitFor guild.createGuildChannel(ChannelFields(name: some("Epic Gamer Channel"))) + + var createPayload = %*{} + + # Make sure that the name is supplied since its required for this endpoint. + if (create.name.isSome): + createPayload.add("name", %create.name.get()) + else: + raise newException(Defect, "You must have a channel name when creating it!") + + if (create.`type`.isSome): + createPayload.add("type", %create.`type`.get()) + + if (create.position.isSome): + createPayload.add("position", %create.position.get()) + + if (create.topic.isSome): + createPayload.add("topic", %create.topic.get()) + + if (create.nsfw.isSome): + createPayload.add("nsfw", %create.nsfw.get()) + + if (create.rateLimitPerUser.isSome): + createPayload.add("rate_limit_per_user", %create.rateLimitPerUser.get()) + + if (create.bitrate.isSome): + createPayload.add("bitrate", %create.bitrate.get()) + + if (create.userLimit.isSome): + createPayload.add("user_limit", %create.userLimit.get()) + + if (create.permissionOverwrites.isSome): + var permOverwrites = parseJson("[]") + for perm in create.permissionOverwrites.get(): + permOverwrites.add(perm.permissionsToJson()) + createPayload.add("permission_overwrites", permOverwrites) + + result = newChannel(sendRequest(endpoint("/guilds/" & $guild.id & "/channels"), HttpPost, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + guild.id, RateLimitBucketType.guild, createPayload)) + +proc modifyGuildChannelPositions*(guild: var Guild, channels: seq[Channel]) {.async.} = + ## Modify the positions of a set of channel objects for the guild. + ## The order is determined by the channel's `position` field + + var jsonBody: JsonNode + for channel in channels: + jsonBody.add(%*{"id": channel.id, "position": channel.position}) + + discard sendRequest(endpoint("/guilds/" & $guild.id & "/channels"), HttpPatch, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + guild.id, RateLimitBucketType.guild, jsonBody) + +proc getGuildMember*(guild: var Guild, memberID: snowflake): GuildMember = + ## Get a guild member. + ## This first checks `guild.members`, but if it doesn't exist there, + ## it will be requested from Discord's REST API. + ## + ## If we end up requesting one, it will add it to `guild.members` + + for member in guild.members: + if (member.id == memberID): + return member + + result = newGuildMember(sendRequest(endpoint(fmt("/guilds/{guild.id}/members/{memberID}")), + HttpPatch, defaultHeaders(), guild.id, RateLimitBucketType.guild), guild.id) + guild.members.add(result) + +# Would this endpoint be worth adding? https://discord.com/developers/docs/resources/guild#list-guild-members +# And what about this one? https://discord.com/developers/docs/resources/guild#list-guild-members + +proc modifyCurrentUserNick*(guild: Guild, nick: string) {.async.} = + ## Modifies the nickname of the current user in a guild. + discard sendRequest(endpoint(fmt("/guilds/{guild.id}/members/@me/nick")), HttpPatch, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + guild.id, RateLimitBucketType.guild, %*{"nick": nick}) + +proc kickGuildMember*(guild: Guild, member: GuildMember) {.async.} = + ## Remove a member from a guild. Requires `KICK_MEMBERS` permission. + discard sendRequest(endpoint(fmt("/guilds/{guild.id}/members/{member.id}")), HttpDelete, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + +proc getGuildBans*(guild: Guild): seq[GuildBan] = + ## Get a list of guild bans. Requires the `BAN_MEMBERS` permission. + let json = sendRequest(endpoint(fmt("/guilds/{guild.id}/bans")), HttpGet, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + + for ban in json: + result.add(GuildBan( + reason: json{"reason"}.getStr(), + user: newUser(json["user"]) + )) + +proc getGuildBan*(guild: Guild, userID: snowflake): GuildBan = + ## Returns a ban object for the given user or nil if the ban cannot be found. + ## Requires the BAN_MEMBERS permission. + let response = sendRequest(endpoint(fmt("/guilds/{guild.id}/bans{userID}")), HttpGet, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + + if not response.isNil(): + return GuildBan( + reason: response{"reason"}.getStr(), + user: newUser(response["user"]) + ) + else: + return nil + +proc banGuildMember*(guild: Guild, userID: snowflake, reason: Option[string] = none(string), deleteMessageDays: Option[int] = none(int)) {.async.} = + ## Create a guild ban, and optionally delete previous messages sent by the + ## banned user. Requires the BAN_MEMBERS permission. + + var jsonBody: JsonNode + + if (reason.isSome): + jsonBody.add("reason", %reason.get()) + if (deleteMessageDays.isSome): + jsonBody.add("deleteMessageDays", %deleteMessageDays.get()) + + discard sendRequest(endpoint(fmt("/guilds/{guild.id}/bans/{userID}")), HttpPut, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + guild.id, RateLimitBucketType.guild, jsonBody) + +proc unbanGuildMember*(guild: Guild, userID: snowflake) {.async.} = + ## Remove the ban for a user. Requires the BAN_MEMBERS permissions. + discard sendRequest(endpoint(fmt("/guilds/{guild.id}/bans/{userID}")), HttpDelete, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + +proc requestGuildRoles*(guild: Guild): seq[Role] = + ## Request all guild roles via Discord's REST API + ## Only use this if for some reason, guild.roles is inaccurate! + ## + ## Also updates the guild's roles when called. + + let jsonBody = sendRequest(endpoint(fmt("/guilds/{guild.id}/roles")), HttpGet, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + + for role in jsonBody: + result.add(newRole(role, guild.id)) + guild.roles = result + +proc createGuildRole*(guild: Guild, name: Option[string] = none(string), permissions: Option[Permissions] = none(Permissions), + color: Option[int] = none(int), hoist: Option[bool] = none(bool), mentionable: Option[bool] = none(bool)): Future[Role] {.async.} = + ## Create a new role for the guild. Requires the `MANAGE_ROLES` permission. + ## + ## Example: + ## + ## .. code-block:: nim + ## discard guild.createGuildRole(name = some("Gamer Role"), color = some(0xff0000)) + + var jsonBody: JsonNode + + if (name.isSome): + jsonBody.add("name", %name) + + if (permissions.isSome): + jsonBody.add("permissions", %permissions.get().allowPerms) + + if (color.isSome): + jsonBody.add("color", %color) + + if (hoist.isSome): + jsonBody.add("hoist", %hoist) + + if (mentionable.isSome): + jsonBody.add("mentionable", %mentionable) + + return newRole(sendRequest(endpoint(fmt("/guilds/{guild.id}/roles")), HttpPost, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + guild.id, RateLimitBucketType.guild, jsonBody), guild.id) + +proc modifyGuildRolePositions*(guild: var Guild, roles: seq[Role]) {.async.} = + ## Modify the positions of a set of role objects for the guild. + ## The order is determined by the role's `position` field + + var jsonBody: JsonNode + for role in roles: + jsonBody.add(%*{"id": role.id, "position": role.position}) + + discard sendRequest(endpoint("/guilds/" & $guild.id & "/roles"), HttpPatch, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + guild.id, RateLimitBucketType.guild, jsonBody) + +proc getGuildPruneCount*(guild: Guild, days: int = 7, includedRoles: seq[snowflake] = @[]): int = + ## Returns the number of members that would be removed in a prune operation. + ## Requires the `KICK_MEMBERS` permission. + ## + ## By default, prune will not remove users with roles. You can optionally include + ## specific roles in your prune by providing the `includedRoles` parameter. Any + ## inactive user that has a subset of the provided role(s) will be counted in + ## the prune and users with additional roles will not. + var url = endpoint(fmt("/guilds/{guild.id}/prune")) + + if days != 7: + url &= "?days=" & $days + + if includedRoles.len != 0: + # If the days field was also set, then we need to ad "&" to the url. + if days != 7: + url &= "&" + url &= "include_roles=" & ($includedRoles).substr(1) + + let jsonBody = sendRequest(url, HttpGet, defaultHeaders(), guild.id, RateLimitBucketType.guild) + return jsonBody["pruned"].getInt() + +proc beginGuildPrune*(guild: Guild, days: int = 7, computePruneCount: bool = false, includedRoles: seq[snowflake] = @[]): Future[Option[int]] {.async.} = + ## Returns the number of members that would be removed in a prune operation. + ## Requires the `KICK_MEMBERS` permission. + ## + ## If you specify `computePruneCount` the proc will return the amount of users + ## that were pruned. Not recommended on large guilds! + ## + ## By default, prune will not remove users with roles. You can optionally include + ## specific roles in your prune by providing the `includedRoles` parameter. Any + ## inactive user that has a subset of the provided role(s) will be counted in + ## the prune and users with additional roles will not. + var url = endpoint(fmt("/guilds/{guild.id}/prune")) + + if days != 7: + url &= "?days=" & $days + + if includedRoles.len != 0: + # If the days field was also set, then we need to add "&" to the url. + if days != 7: + url &= "&" + url &= "include_roles=" & ($includedRoles).substr(1) + + if computePruneCount: + # If the days or includedRoles field was also set, then we need to add "&" to the url. + if days != 7 or includedRoles.len != 0: + url &= "&" + url &= "compute_prune_count=" & $computePruneCount + + let jsonBody = sendRequest(url, HttpGet, defaultHeaders(), guild.id, RateLimitBucketType.guild) + + if computePruneCount: + return some(jsonBody["pruned"].getInt()) + +#TODO: https://discord.com/developers/docs/resources/guild#get-guild-voice-regions + +proc getGuildVoiceRegions*(guild: Guild): seq[VoiceRegion] = + ## Returns a list of voice region objects for the guild. + let jsonBody = sendRequest(endpoint(fmt("/guilds/{guild.id}/regions")), HttpGet, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + + for voiceRegion in jsonBody: + result.add(VoiceRegion( + id: jsonBody["id"].getStr(), + name: jsonBody["name"].getStr(), + vip: jsonBody["vip"].getBool(), + optimal: jsonBody["optimal"].getBool(), + deprecated: jsonBody["deprecated"].getBool(), + custom: jsonBody["custom"].getBool() + )) + +proc getGuildInvites*(guild: Guild): seq[Invite] = + ## Returns a list of invite objects (with invite metadata) for the guild. + ## Requires the `MANAGE_GUILD` permission. + let jsonBody = sendRequest(endpoint(fmt("/guilds/{guild.id}/invites")), HttpGet, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + + for invite in jsonBody: + result.add(newInvite(invite)) + +proc getGuildIntegrations*(guild: Guild): seq[Integration] = + ## Returns a list of integration objects for the guild. Requires the `MANAGE_GUILD` permission. + let jsonBody = sendRequest(endpoint(fmt("/guilds/{guild.id}/integrations")), HttpGet, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + + for integration in jsonBody: + result.add(Integration( + id: getIDFromJson(jsonBody["id"].getStr()), + name: jsonBody["name"].getStr(), + `type`: jsonBody["type"].getStr(), + enabled: jsonBody["enabled"].getBool(), + syncing: jsonBody["syncing"].getBool(), + roleID: getIDFromJson(jsonBody["role_id"].getStr()), + enableEmoticons: jsonBody["enable_emoticons"].getBool(), + expireBehavior: IntegrationExpireBehavior(jsonBody["expire_behavior"].getInt()), + expireGracePeriod: jsonBody{"expire_grace_period"}.getInt(), + user: newUser(jsonBody["user"]), + account: IntegrationAccount( + id: jsonBody["account"]["id"].getStr(), + name: jsonBody["account"]["name"].getStr(), + ), + syncedAt: jsonBody["synced_at"].getStr() + )) + +proc createGuildIntegration*(guild: Guild, `type`: string, id: string) {.async.} = + ## Attach an integration object from the current user to the guild. Requires the `MANAGE_GUILD` permission. + let jsonBody = %* { + "type": `type`, + "id": id + } + + discard sendRequest(endpoint("/guilds/" & $guild.id & "/integrations"), HttpPost, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + guild.id, RateLimitBucketType.guild, jsonBody) + +proc modifyGuildIntegration*(guild: Guild, integration: var Integration, + expireBehavior: Option[IntegrationExpireBehavior] = none(IntegrationExpireBehavior), + expireGracePeriod: Option[int] = none(int), enableEmoticons: Option[bool] = none(bool)) {.async.} = + ## Modify the behavior and settings of an integration object for the guild. Requires the `MANAGE_GUILD` permission. + ## + ## The changes are reflected to the given `integration`. + ## + ## Example: + ## + ## .. code-block:: nim + ## discard integration.modifyGuildIntegration(enable_emoticons = true) + var modifyPayload = %*{} + + if (expireBehavior.isSome): + modifyPayload.add("expire_behavior", %expireBehavior.get()) + integration.expireBehavior = (expireBehavior.get()) + + if (expireGracePeriod.isSome): + modifyPayload.add("expire_grace_period", %expireGracePeriod.get()) + integration.expireGracePeriod = expireGracePeriod.get() + + if (enableEmoticons.isSome): + modifyPayload.add("enable_emoticons", %enableEmoticons.get()) + integration.enableEmoticons = enableEmoticons.get() + + discard sendRequest(endpoint(fmt("/guilds/{guild.id}/integration/{integration.id}")), HttpPatch, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + guild.id, RateLimitBucketType.guild, modifyPayload) + +proc deleteGuildIntegration*(guild: Guild, integration: Integration) {.async.} = + ## Delete the attached integration object for the guild. Requires the `MANAGE_GUILD` permission. + discard sendRequest(endpoint(fmt("/guilds/{guild.id}/integrations/{integration.id}")), HttpDelete, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + +proc syncGuildIntegration*(guild: Guild, integration: Integration) {.async.} = + ## Sync an integration. Requires the `MANAGE_GUILD` permission. + discard sendRequest(endpoint(fmt("/guilds/{guild.id}/integrations/{integration.id}")), HttpPost, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + +proc getGuildWidget*(guild: Guild): GuildWidget = + ## Returns the guild widget object. Requires the `MANAGE_GUILD` permission. + + let jsonBody = sendRequest(endpoint(fmt("/guilds/{guild.id}/widget")), HttpGet, + defaultHeaders(), guild.id, RateLimitBucketType.guild) + + return GuildWidget(enabled: jsonBody["enabled"].getBool(), + channelID: getIDFromJson(jsonBody{"channel_id"}.getStr())) + +proc modifyGuildWidget*(guild: Guild, widget: var GuildWidget, enabled: bool, channelID: snowflake) {.async.} = + ## Modify a guild widget object for the guild. Requires the `MANAGE_GUILD` permission. + widget.enabled = enabled + widget.channelID = channelID + + let jsonBody = %* { + "enabled": enabled, + "channelID": channelID + } + + discard sendRequest(endpoint(fmt("/guilds/{guild.id}/widget")), HttpPost, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + guild.id, RateLimitBucketType.guild, jsonBody) + +proc getGuildVanityURL*(guild: Guild): Invite = + ## Returns a partial invite object for guilds with that feature enabled. Requires the `MANAGE_GUILD` permission. + return newInvite(sendRequest(endpoint(fmt("/guilds/{guild.id}/vanity-url")), HttpGet, + defaultHeaders(), guild.id, RateLimitBucketType.guild)) + +proc getGuildWidgetImage*(guild: Guild, style: GuildWidgetStyle): string = + ## Returns a url to this guild's widget image. + ## + ## Style types: + ## * Shield: Shield style widget with Discord icon and guild members + ## online count. [Example](https://discord.com/api/guilds/81384788765712384/widget.png?style=shield) + ## * Banner 1: Large image with guild icon, name and online count. + ## "POWERED BY DISCORD" as the footer of the widget. [Example](https://discord.com/api/guilds/81384788765712384/widget.png?style=banner1) + ## * Banner 2: Smaller widget style with guild icon, name and online + ## count. Split on the right with Discord logo. [Example](https://discord.com/api/guilds/81384788765712384/widget.png?style=banner2) + ## * Banner 3: Large image with guild icon, name and online count. + ## In the footer, Discord logo on the left and "Chat Now" on the right [Example](https://discord.com/api/guilds/81384788765712384/widget.png?style=banner3) + ## * Banner 4: Large Discord logo at the top of the widget. Guild + ## icon, name and online count in the middle portion of the widget + ## and a "JOIN MY SERVER" button at the bottom. [Example](https://discord.com/api/guilds/81384788765712384/widget.png?style=banner4) + result = fmt("guilds/{guild.id}/widget.png") + + case (style) + of (GuildWidgetStyle.guildWidgetStyleShield): + result &= "?style=shield" + of (GuildWidgetStyle.guildWidgetStyleBanner1): + result &= "?style=banner1" + of (GuildWidgetStyle.guildWidgetStyleBanner2): + result &= "?style=banner2" + of (GuildWidgetStyle.guildWidgetStyleBanner3): + result &= "?style=banner3" + of (GuildWidgetStyle.guildWidgetStyleBanner4): + result &= "?style=banner4" \ No newline at end of file diff --git a/src/member.nim b/src/member.nim index 80fa40e..e8ee864 100644 --- a/src/member.nim +++ b/src/member.nim @@ -1,4 +1,4 @@ -import discordobject, user, json, role +import discordobject, user, json, role, options, asyncdispatch, nimcordutils, httpcore, strformat, strutils type GuildMember* = ref object of DiscordObject ## This type is a guild member. @@ -9,9 +9,9 @@ type GuildMember* = ref object of DiscordObject premiumSince*: string ## When the user started boosting the guild. deaf*: bool ## Whether the user is deafened in voice channels. mute*: bool ## Whether the user is muted in voice channels. + guildID*: snowflake ## The guild this member is in. - -proc newGuildMember*(json: JsonNode): GuildMember {.inline.} = +proc newGuildMember*(json: JsonNode, guild: snowflake): GuildMember {.inline.} = ## Construct a GuildMember using json. var member = GuildMember( nick: json{"nick"}.getStr(), @@ -19,14 +19,60 @@ proc newGuildMember*(json: JsonNode): GuildMember {.inline.} = joinedAt: json["joined_at"].getStr(), premiumSince: json{"premium_since"}.getStr(), deaf: json["deaf"].getBool(), - mute: json["mute"].getBool() + mute: json["mute"].getBool(), + guildID: guild ) if (json.contains("user")): member.user = newUser(json["user"]) for role in json: - member.roles.add(newRole(role)) + member.roles.add(newRole(role, member.guildID)) return member - \ No newline at end of file + +type GuildMemberModify* = ref object + nick: Option[string] + roles: Option[seq[snowflake]] + mute: Option[bool] + deaf: Option[bool] + channelID: Option[snowflake] + +proc modifyGuildMember*(member: GuildMember, memberID: snowflake, modify: GuildMemberModify) {.async.} = + ## Modify attributes of a guild member. If the `channel_id` is set to null, + ## this will force the target user to be disconnected from voice. + ## + ## The member's new attributes will be reflected to `guild.members`. + var modifyPayload = %*{} + + if (modify.nick.isSome): + modifyPayload.add("nick", %modify.nick.get()) + + if (modify.roles.isSome): + # Convert the roles array to a string representation and remove the `@` + # that is at the front of a conversion like this. + var rolesStr = ($modify.roles.get()).substr(1) + modifyPayload.add(parseJson(rolesStr)) + + if (modify.mute.isSome): + modifyPayload.add("mute", %modify.mute.get()) + + if (modify.deaf.isSome): + modifyPayload.add("deaf", %modify.deaf.get()) + + if (modify.channelID.isSome): + modifyPayload.add("channel_id", %modify.channelID.get()) + + discard sendRequest(endpoint(fmt("/guilds/{member.guildID}/members/{member.id}")), HttpPatch, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + member.guildID, RateLimitBucketType.guild, modifyPayload) + +proc addGuildMemberRole*(member: GuildMember, roleID: snowflake) {.async.} = + ## Adds a role to a guild member. Requires the `MANAGE_ROLES` permission. + discard sendRequest(endpoint(fmt("/guilds/{member.guildID}/members/{member.id}/roles/{roleID}")), + HttpPut, defaultHeaders(), member.guildID, RateLimitBucketType.guild) + +proc removeGuildMemberRole*(member: GuildMember, roleID: snowflake) {.async.} = + ## Remove's a role to a guild member. Requires the `MANAGE_ROLES` permission. + discard sendRequest(endpoint(fmt("/guilds/{member.guildID}/members/{member.id}/roles/{roleID}")), + HttpDelete, defaultHeaders(), member.guildID, RateLimitBucketType.guild) \ No newline at end of file diff --git a/src/message.nim b/src/message.nim index 661feab..aab5909 100644 --- a/src/message.nim +++ b/src/message.nim @@ -94,7 +94,7 @@ proc newMessage*(messageJson: JsonNode): Message = if (messageJson.contains("author")): msg.author = newUser(messageJson["author"]) if (messageJson.contains("member")): - msg.member = newGuildMember(messageJson["member"]) + msg.member = newGuildMember(messageJson["member"], msg.guildID) if (messageJson.contains("mentions")): let mentionsJson = messageJson["mentions"].getElems() diff --git a/src/role.nim b/src/role.nim index f37a11a..70ae8c2 100644 --- a/src/role.nim +++ b/src/role.nim @@ -1,4 +1,4 @@ -import json, nimcordutils, discordobject, permission +import json, nimcordutils, discordobject, permission, options, httpcore, asyncdispatch, strformat type Role* = ref object of DiscordObject name*: string @@ -24,3 +24,39 @@ proc newRole*(json: JsonNode, guild: snowflake): Role = uint(json["permissions"].getInt())) ) +proc modifyGuildRole*(role: var Role, name: Option[string] = none(string), permissions: Option[Permissions] = none(Permissions), + color: Option[int] = none(int), hoist: Option[bool] = none(bool), mentionable: Option[bool] = none(bool)): Future[Role] {.async.} = + ## Modify a guild role. Requires the `MANAGE_ROLES` permission. + ## The changes will reflect on the `role` object you supplied. + ## + ## Example: + ## + ## .. code-block:: nim + ## discard role.modifyGuildRole(name = some("Gamer Role"), color = some(0xff0000)) + + var jsonBody: JsonNode + + if (name.isSome): + jsonBody.add("name", %name) + + if (permissions.isSome): + jsonBody.add("permissions", %permissions.get().allowPerms) + + if (color.isSome): + jsonBody.add("color", %color) + + if (hoist.isSome): + jsonBody.add("hoist", %hoist) + + if (mentionable.isSome): + jsonBody.add("mentionable", %mentionable) + + result = newRole(sendRequest(endpoint(fmt("/guilds/{role.guildID}/roles/{role.id}")), HttpPatch, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + role.guildID, RateLimitBucketType.guild, jsonBody), role.guildID) + role = result + +proc deleteGuildRole*(role: Role) {.async.} = + ## Delete a guild role. Requires the `MANAGE_ROLES` permission. + discard sendRequest(endpoint(fmt("/guilds/{role.guildID}/roles/{role.id}")), HttpDelete, + defaultHeaders(), role.guildID, RateLimitBucketType.guild) \ No newline at end of file