From 0764147bed9dbdbaedb662973b274e83874cde69 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Thu, 13 Aug 2020 19:39:38 -0500 Subject: [PATCH] Make a bunch of image url getters and memory optimize the image hashes --- src/nimcord/client.nim | 2 +- src/nimcord/emoji.nim | 18 ++++++ src/nimcord/guild.nim | 121 ++++++++++++++++++++++++++++++++--- src/nimcord/nimcordutils.nim | 25 ++++++-- src/nimcord/user.nim | 81 +++++++++++++++++++---- 5 files changed, 220 insertions(+), 27 deletions(-) diff --git a/src/nimcord/client.nim b/src/nimcord/client.nim index ddf0783..212255f 100644 --- a/src/nimcord/client.nim +++ b/src/nimcord/client.nim @@ -22,7 +22,7 @@ proc getIdentifyPacket(shard: Shard): JsonNode proc handleGatewayDisconnect(shard: Shard, error: string) {.async.} proc handleHeartbeat(shard: Shard) {.async.} proc handleWebsocketPacket(shard: Shard) {.async.} -proc newDiscordClient*(tkn: string): DiscordClient +proc newDiscordClient*(tkn: string, commandPrefix: string): DiscordClient proc newShard(shardID: int, client: DiscordClient): Shard proc reconnectShard(shard: Shard) {.async.} proc sendGatewayRequest*(shard: Shard, request: JsonNode, msg: string = "") {.async.} diff --git a/src/nimcord/emoji.nim b/src/nimcord/emoji.nim index 19d060a..9bf6246 100644 --- a/src/nimcord/emoji.nim +++ b/src/nimcord/emoji.nim @@ -96,3 +96,21 @@ proc deleteEmoji*(emoji: Emoji) {.async.} = ## Delete the given emoji. Requires the `MANAGE_EMOJIS` permission. discard sendRequest(endpoint(fmt("/guilds/{emoji.guildID}/emojis/{emoji.id}")), HttpDelete, defaultHeaders(), emoji.guildID, RateLimitBucketType.guild) + +proc getEmojiURL*(emoji: Emoji, imageType: ImageType = ImageType.imgTypeAuto): string = + ## Get the URL for the emoji's. + result = "https://cdn.discordapp.com/emojis/" & $emoji.id + + # Choose the type of image automaticly. + var tmp = imageType + if emoji.animated: + tmp = ImageType.imgTypeGif + else: + tmp = ImageType.imgTypePng + + case tmp: + of ImageType.imgTypeGif: + result &= ".gif" + discard + else: # The only other possible image type is png + result &= ".png" \ No newline at end of file diff --git a/src/nimcord/guild.nim b/src/nimcord/guild.nim index 4fe4a59..fbb0d1c 100644 --- a/src/nimcord/guild.nim +++ b/src/nimcord/guild.nim @@ -1,6 +1,6 @@ import json, discordobject, channel, member, options, nimcordutils, emoji import role, permission, httpcore, strformat, image, asyncdispatch, user -import permission, presence, tables +import permission, presence, tables, strutils type VerificationLevel* = enum @@ -68,9 +68,10 @@ type Guild* = ref object of DiscordObject ## Discord Guild object name*: string ## The name of the current guild - icon*: string ## The hash of the current guild's icon - splash*: string ## The hash of the current guild's splash - discoverySplash*: string + iconRaw: array[2, uint64] ## The split hash for the 128bit hexadeximal icon. + isIconGif: bool ## Wether the avatar is a gif. + splashRaw: array[2, uint64] ## The split hash for the 128bit hexadeximal splash. + discoverySplashRaw: array[2, uint64] ## The split hash for the 128bit hexadeximal discovery splash. owner*: bool ## Whether or not the current user is the owner of the current guild ownerID: Snowflake ## The snowflake id of the current guild's owner permissions*: Permissions @@ -102,7 +103,7 @@ type maxMembers*: int ## The maximum amount of members in the current guild? vanityUrlCode*: string ## The vanity invite for the current guild (ex: https://discord.gg/discord-api) description*: string - banner*: string ## The hash code of the current guild + bannerRaw: array[2, uint64] ## The split hash for the 128bit hexadeximal banner. premiumTier*: PremiumTier premiumSubscriptionCount*: int preferredLocale*: string @@ -175,9 +176,8 @@ proc newGuild*(json: JsonNode): Guild {.inline.} = var g = Guild( id: getIDFromJson(json["id"].getStr()), name: json["name"].getStr(), - icon: json["icon"].getStr(), - splash: json["splash"].getStr(), - discoverySplash: json["discovery_splash"].getStr(), + splashRaw: splitAvatarHash(json["splash"].getStr()), # No need to remove prefixed "a_", can't be animated. + discoverySplashRaw: splitAvatarHash(json["discovery_splash"].getStr()), # No need to remove prefixed "a_", can't be animated. ownerID: getIDFromJson(json["owner_id"].getStr()), region: json["region"].getStr(), afkChannelID: getIDFromJson(json["afk_channel_id"].getStr()), @@ -192,13 +192,23 @@ proc newGuild*(json: JsonNode): Guild {.inline.} = rulesChannelID: getIDFromJson(json["rules_channel_id"].getStr()), vanityUrlCode: json["vanity_url_code"].getStr(), description: json["description"].getStr(), - banner: json["banner"].getStr(), + bannerRaw: splitAvatarHash(json["banner"].getStr()), # No need to remove prefixed "a_", can't be animated. premiumTier: PremiumTier(json["premium_tier"].getInt()), preferredLocale: json["preferred_locale"].getStr(), publicUpdatesChannelID: getIDFromJson(json["public_updates_channel_id"].getStr()) ) # Parse all non guaranteed fields + if json.contains("icon"): + let iconStr = json["icon"].getStr() + + # If the icon is animated we need to remove the prefixed "a_" + if iconStr.startsWith("a_"): + g.isIconGif = true + g.iconRaw = splitAvatarHash(iconStr.substr(2)) + else: + g.isIconGif = false + g.iconRaw = splitAvatarHash(iconStr) if json.contains("owner"): g.owner = json["owner"].getBool() if json.contains("permissions"): @@ -916,3 +926,96 @@ proc getGuildMemberRoles*(guild: Guild, member: GuildMember): seq[Role] = for role in guild.roles: if member.roles.contains(role.id): result.add(role) + +proc getGuildIconURL*(guild: Guild, imageType: ImageType = ImageType.imgTypeAuto): string = + ## Get the URL for the guild's icon. + result = "https://cdn.discordapp.com/icons/" & $guild.id & "/" & $combineAvatarHash(guild.iconRaw) + + # If we're finding the image type automaticly, then we need to + # check if the avatar is a gif. + var tmp = imageType + if (imageType == ImageType.imgTypeAuto): + if guild.isIconGif: + tmp = ImageType.imgTypeGif + else: + tmp = ImageType.imgTypePng + + case tmp: + of ImageType.imgTypeGif: + result &= ".gif" + discard + of ImageType.imgTypeJpeg: + result &= ".jpeg" + discard + of ImageType.imgTypePng: + result &= ".png" + discard + of ImageType.imgTypeWebp: + result &= ".webp" + discard + of ImageType.imgTypeAuto: + result &= ".png" # Just incase + discard + +proc getGuildSplashURL*(guild: Guild, imageType: ImageType = ImageType.imgTypePng): string = + ## Get the URL for the guild's splash. + result = "https://cdn.discordapp.com/splashes/" & $guild.id & "/" & $combineAvatarHash(guild.splashRaw) + + case imageType: + of ImageType.imgTypeGif: + result &= ".png" # The guild's splash can't be a gif. + discard + of ImageType.imgTypeJpeg: + result &= ".jpeg" + discard + of ImageType.imgTypePng: + result &= ".png" + discard + of ImageType.imgTypeWebp: + result &= ".webp" + discard + of ImageType.imgTypeAuto: + result &= ".png" + discard + +proc getGuildDiscoverySplashURL*(guild: Guild, imageType: ImageType = ImageType.imgTypePng): string = + ## Get the URL for the guild's discovery splash. + result = "https://cdn.discordapp.com/discovery-splashes/" & $guild.id & "/" & $combineAvatarHash(guild.discoverySplashRaw) + + case imageType: + of ImageType.imgTypeGif: + result &= ".png" # The guild's discovery splash can't be a gif. + discard + of ImageType.imgTypeJpeg: + result &= ".jpeg" + discard + of ImageType.imgTypePng: + result &= ".png" + discard + of ImageType.imgTypeWebp: + result &= ".webp" + discard + of ImageType.imgTypeAuto: + result &= ".png" # Just incase + discard + +proc getGuildBannerURL*(guild: Guild, imageType: ImageType = ImageType.imgTypePng): string = + ## Get the URL for the guild's banner. + result = "https://cdn.discordapp.com/banners/" & $guild.id & "/" & $combineAvatarHash(guild.bannerRaw) + + case imageType: + of ImageType.imgTypeGif: + result &= ".png" # The guild's banner can't be a gif. + discard + of ImageType.imgTypeJpeg: + result &= ".jpeg" + discard + of ImageType.imgTypePng: + result &= ".png" + discard + of ImageType.imgTypeWebp: + result &= ".webp" + discard + of ImageType.imgTypeAuto: + result &= ".png" # Just incase + discard \ No newline at end of file diff --git a/src/nimcord/nimcordutils.nim b/src/nimcord/nimcordutils.nim index a980f80..29a57e0 100644 --- a/src/nimcord/nimcordutils.nim +++ b/src/nimcord/nimcordutils.nim @@ -1,6 +1,13 @@ -import parseutils, json, httpClient, strformat, tables, times, asyncdispatch +import parseutils, json, httpClient, strformat, tables, times, asyncdispatch, strutils from discordobject import Snowflake +type ImageType* = enum + imgTypeAuto = 0, + imgTypeWebp = 1, + imgTypePng = 2, + imgTypeJpeg = 3, + imgTypeGif = 4 + proc getIDFromJson*(str: string): uint64 = var num: uint64 discard parseBiggestUInt(str, num) @@ -17,12 +24,22 @@ proc endpoint*(url: string): string = var globalToken*: string proc defaultHeaders*(added: HttpHeaders = newHttpHeaders()): HttpHeaders = - # added.add("Authorization", fmt("Bot {globalToken}")) - added.add("Authorization", fmt("{globalToken}")) + added.add("Authorization", fmt("Bot {globalToken}")) added.add("User-Agent", "NimCord (https://github.com/SeanOMik/nimcord, v0.0.0)") added.add("X-RateLimit-Precision", "millisecond") return added +proc splitAvatarHash*(hash: string): array[2, uint64] = + var first: uint64 + discard parseBiggestUInt(hash.substr(0, 16), first) + var second: uint64 + discard parseBiggestUInt(hash.substr(0, 16), first) + + return [first, second] + +proc combineAvatarHash*(hash: array[2, uint64]): string = + return (BiggestInt hash[0]).toHex(16) & (BiggestInt hash[1]).toHex(16) + type RateLimitBucketType* = enum channel, @@ -125,4 +142,4 @@ proc sendRequest*(endpoint: string, httpMethod: HttpMethod, headers: HttpHeaders waitForRateLimits(objectID, bucketType) let response = client.request(endpoint, httpMethod, strPayload) - return handleResponse(response, objectId, bucketType) + return handleResponse(response, objectId, bucketType) \ No newline at end of file diff --git a/src/nimcord/user.nim b/src/nimcord/user.nim index 97afc99..1018f1d 100644 --- a/src/nimcord/user.nim +++ b/src/nimcord/user.nim @@ -1,4 +1,4 @@ -import json, discordobject, nimcordutils +import json, discordobject, nimcordutils, strutils type NitroSubscription* = enum @@ -7,41 +7,52 @@ type nitro = 2 User* = ref object of DiscordObject - ## This type is a discord user. + ## This type is any discord user. username*: string ## The user's username, not unique across the platform. discriminator*: cushort ## The user's 4-digit discord-tag. - avatar*: string ## The user's avatar hash. bot*: bool ## Whether the user belongs to an OAuth2 application. system*: bool ## Whether the user is an Official Discord System user (part of the urgent message system). - flags*: int ## The flags on a user's account. - premiumType*: NitroSubscription ## The type of Nitro subscription on a user's account. - publicFlags*: int ## The public flags on a user's account. + publicFlags*: int ## The public [flags](https://discord.com/developers/docs/resources/user#user-object-user-flags) on a user's account. (User Badges) + avatarRaw: array[2, uint64] ## The split hash for the 128bit hexadeximal avatar. + isAvatarGif: bool ## Wether the avatar is a gif. + ClientUser* = ref object of User + ## This type is the clients discord user. mfaEnabled*: bool ## Whether the user has two factor authentication enabled on their account. locale*: string ## The user's chosen language option. verified*: bool ## Whether or not the current user has a verified email. email*: string ## The current user's email + premiumType*: NitroSubscription ## The type of Nitro subscription on a user's account. + flags*: int ## The [flags](https://discord.com/developers/docs/resources/user#user-object-user-flags) on a user's account. proc newUser*(user: JsonNode): User {.inline.} = - return User( + result = User( id: getIDFromJson(user["id"].getStr()), username: user["username"].getStr(), discriminator: cushort(parseIntEasy(user["discriminator"].getStr())), - avatar: user["avatar"].getStr(), bot: user{"bot"}.getBool(), system: user{"system"}.getBool(), - flags: user{"flags"}.getInt(), - premiumType: NitroSubscription(user{"premium_type"}.getInt()), publicFlags: user{"public_flags"}.getInt() ) + if user.contains("avatar"): + let avatarStr = user["avatar"].getStr() + + # If the avatar is animated we need to remove the prefixed "a_" + if avatarStr.startsWith("a_"): + result.isAvatarGif = true + result.avatarRaw = splitAvatarHash(avatarStr.substr(2)) + else: + result.isAvatarGif = false + result.avatarRaw = splitAvatarHash(avatarStr) + + proc newClientUser*(clientUser: JsonNode): ClientUser {.inline.} = - return ClientUser( + result = ClientUser( id: getIDFromJson(clientUser["id"].getStr()), username: clientUser["username"].getStr(), discriminator: cushort(parseIntEasy(clientUser["discriminator"].getStr())), - avatar: clientUser["avatar"].getStr(), bot: clientUser{"bot"}.getBool(), system: clientUser{"system"}.getBool(), mfaEnabled: clientUser{"mfa_enabled"}.getBool(), @@ -51,4 +62,48 @@ proc newClientUser*(clientUser: JsonNode): ClientUser {.inline.} = flags: clientUser{"flags"}.getInt(), premiumType: NitroSubscription(clientUser{"premium_type"}.getInt()), publicFlags: clientUser{"public_flags"}.getInt() - ) \ No newline at end of file + ) + + if clientUser.contains("avatar"): + let avatarStr = clientUser["avatar"].getStr() + + # If the avatar is animated we need to remove the prefixed "a_" + if avatarStr.startsWith("a_"): + result.isAvatarGif = true + result.avatarRaw = splitAvatarHash(avatarStr.substr(2)) + else: + result.isAvatarGif = false + result.avatarRaw = splitAvatarHash(avatarStr) + +proc getUserAvatarURL*(user: User, imageType: ImageType = ImageType.imgTypeAuto): string = + # If the user doesn't have an avatar, then return a default avatar url. + if user.avatarRaw.len == 0: + return "https://cdn.discordapp.com/embed/avatars/" & $(user.discriminator mod 5) & ".png" + + result = "https://cdn.discordapp.com/avatars/" & $user.id & "/" & $combineAvatarHash(user.avatarRaw) + + # If we're finding the image type automaticly, then we need to + # check if the avatar is a gif. + var tmp = imageType + if (imageType == ImageType.imgTypeAuto): + if user.isAvatarGif: + tmp = ImageType.imgTypeGif + else: + tmp = ImageType.imgTypePng + + case tmp: + of ImageType.imgTypeGif: + result &= ".gif" + discard + of ImageType.imgTypeJpeg: + result &= ".jpeg" + discard + of ImageType.imgTypePng: + result &= ".png" + discard + of ImageType.imgTypeWebp: + result &= ".webp" + discard + of ImageType.imgTypeAuto: + result &= ".png" # Just incase + discard \ No newline at end of file