Make a bunch of image url getters and memory optimize the image hashes
This commit is contained in:
parent
fadf533066
commit
0764147bed
|
@ -22,7 +22,7 @@ proc getIdentifyPacket(shard: Shard): JsonNode
|
||||||
proc handleGatewayDisconnect(shard: Shard, error: string) {.async.}
|
proc handleGatewayDisconnect(shard: Shard, error: string) {.async.}
|
||||||
proc handleHeartbeat(shard: Shard) {.async.}
|
proc handleHeartbeat(shard: Shard) {.async.}
|
||||||
proc handleWebsocketPacket(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 newShard(shardID: int, client: DiscordClient): Shard
|
||||||
proc reconnectShard(shard: Shard) {.async.}
|
proc reconnectShard(shard: Shard) {.async.}
|
||||||
proc sendGatewayRequest*(shard: Shard, request: JsonNode, msg: string = "") {.async.}
|
proc sendGatewayRequest*(shard: Shard, request: JsonNode, msg: string = "") {.async.}
|
||||||
|
|
|
@ -96,3 +96,21 @@ proc deleteEmoji*(emoji: Emoji) {.async.} =
|
||||||
## Delete the given emoji. Requires the `MANAGE_EMOJIS` permission.
|
## Delete the given emoji. Requires the `MANAGE_EMOJIS` permission.
|
||||||
discard sendRequest(endpoint(fmt("/guilds/{emoji.guildID}/emojis/{emoji.id}")), HttpDelete,
|
discard sendRequest(endpoint(fmt("/guilds/{emoji.guildID}/emojis/{emoji.id}")), HttpDelete,
|
||||||
defaultHeaders(), emoji.guildID, RateLimitBucketType.guild)
|
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"
|
|
@ -1,6 +1,6 @@
|
||||||
import json, discordobject, channel, member, options, nimcordutils, emoji
|
import json, discordobject, channel, member, options, nimcordutils, emoji
|
||||||
import role, permission, httpcore, strformat, image, asyncdispatch, user
|
import role, permission, httpcore, strformat, image, asyncdispatch, user
|
||||||
import permission, presence, tables
|
import permission, presence, tables, strutils
|
||||||
|
|
||||||
type
|
type
|
||||||
VerificationLevel* = enum
|
VerificationLevel* = enum
|
||||||
|
@ -68,9 +68,10 @@ type
|
||||||
Guild* = ref object of DiscordObject
|
Guild* = ref object of DiscordObject
|
||||||
## Discord Guild object
|
## Discord Guild object
|
||||||
name*: string ## The name of the current guild
|
name*: string ## The name of the current guild
|
||||||
icon*: string ## The hash of the current guild's icon
|
iconRaw: array[2, uint64] ## The split hash for the 128bit hexadeximal icon.
|
||||||
splash*: string ## The hash of the current guild's splash
|
isIconGif: bool ## Wether the avatar is a gif.
|
||||||
discoverySplash*: string
|
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
|
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
|
ownerID: Snowflake ## The snowflake id of the current guild's owner
|
||||||
permissions*: Permissions
|
permissions*: Permissions
|
||||||
|
@ -102,7 +103,7 @@ type
|
||||||
maxMembers*: int ## The maximum amount of members in the current guild?
|
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)
|
vanityUrlCode*: string ## The vanity invite for the current guild (ex: https://discord.gg/discord-api)
|
||||||
description*: string
|
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
|
premiumTier*: PremiumTier
|
||||||
premiumSubscriptionCount*: int
|
premiumSubscriptionCount*: int
|
||||||
preferredLocale*: string
|
preferredLocale*: string
|
||||||
|
@ -175,9 +176,8 @@ proc newGuild*(json: JsonNode): Guild {.inline.} =
|
||||||
var g = Guild(
|
var g = Guild(
|
||||||
id: getIDFromJson(json["id"].getStr()),
|
id: getIDFromJson(json["id"].getStr()),
|
||||||
name: json["name"].getStr(),
|
name: json["name"].getStr(),
|
||||||
icon: json["icon"].getStr(),
|
splashRaw: splitAvatarHash(json["splash"].getStr()), # No need to remove prefixed "a_", can't be animated.
|
||||||
splash: json["splash"].getStr(),
|
discoverySplashRaw: splitAvatarHash(json["discovery_splash"].getStr()), # No need to remove prefixed "a_", can't be animated.
|
||||||
discoverySplash: json["discovery_splash"].getStr(),
|
|
||||||
ownerID: getIDFromJson(json["owner_id"].getStr()),
|
ownerID: getIDFromJson(json["owner_id"].getStr()),
|
||||||
region: json["region"].getStr(),
|
region: json["region"].getStr(),
|
||||||
afkChannelID: getIDFromJson(json["afk_channel_id"].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()),
|
rulesChannelID: getIDFromJson(json["rules_channel_id"].getStr()),
|
||||||
vanityUrlCode: json["vanity_url_code"].getStr(),
|
vanityUrlCode: json["vanity_url_code"].getStr(),
|
||||||
description: json["description"].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()),
|
premiumTier: PremiumTier(json["premium_tier"].getInt()),
|
||||||
preferredLocale: json["preferred_locale"].getStr(),
|
preferredLocale: json["preferred_locale"].getStr(),
|
||||||
publicUpdatesChannelID: getIDFromJson(json["public_updates_channel_id"].getStr())
|
publicUpdatesChannelID: getIDFromJson(json["public_updates_channel_id"].getStr())
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parse all non guaranteed fields
|
# 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"):
|
if json.contains("owner"):
|
||||||
g.owner = json["owner"].getBool()
|
g.owner = json["owner"].getBool()
|
||||||
if json.contains("permissions"):
|
if json.contains("permissions"):
|
||||||
|
@ -916,3 +926,96 @@ proc getGuildMemberRoles*(guild: Guild, member: GuildMember): seq[Role] =
|
||||||
for role in guild.roles:
|
for role in guild.roles:
|
||||||
if member.roles.contains(role.id):
|
if member.roles.contains(role.id):
|
||||||
result.add(role)
|
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
|
|
@ -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
|
from discordobject import Snowflake
|
||||||
|
|
||||||
|
type ImageType* = enum
|
||||||
|
imgTypeAuto = 0,
|
||||||
|
imgTypeWebp = 1,
|
||||||
|
imgTypePng = 2,
|
||||||
|
imgTypeJpeg = 3,
|
||||||
|
imgTypeGif = 4
|
||||||
|
|
||||||
proc getIDFromJson*(str: string): uint64 =
|
proc getIDFromJson*(str: string): uint64 =
|
||||||
var num: uint64
|
var num: uint64
|
||||||
discard parseBiggestUInt(str, num)
|
discard parseBiggestUInt(str, num)
|
||||||
|
@ -17,12 +24,22 @@ proc endpoint*(url: string): string =
|
||||||
var globalToken*: string
|
var globalToken*: string
|
||||||
|
|
||||||
proc defaultHeaders*(added: HttpHeaders = newHttpHeaders()): HttpHeaders =
|
proc defaultHeaders*(added: HttpHeaders = newHttpHeaders()): HttpHeaders =
|
||||||
# added.add("Authorization", fmt("Bot {globalToken}"))
|
added.add("Authorization", fmt("Bot {globalToken}"))
|
||||||
added.add("Authorization", fmt("{globalToken}"))
|
|
||||||
added.add("User-Agent", "NimCord (https://github.com/SeanOMik/nimcord, v0.0.0)")
|
added.add("User-Agent", "NimCord (https://github.com/SeanOMik/nimcord, v0.0.0)")
|
||||||
added.add("X-RateLimit-Precision", "millisecond")
|
added.add("X-RateLimit-Precision", "millisecond")
|
||||||
return added
|
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
|
type
|
||||||
RateLimitBucketType* = enum
|
RateLimitBucketType* = enum
|
||||||
channel,
|
channel,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import json, discordobject, nimcordutils
|
import json, discordobject, nimcordutils, strutils
|
||||||
|
|
||||||
type
|
type
|
||||||
NitroSubscription* = enum
|
NitroSubscription* = enum
|
||||||
|
@ -7,41 +7,52 @@ type
|
||||||
nitro = 2
|
nitro = 2
|
||||||
|
|
||||||
User* = ref object of DiscordObject
|
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.
|
username*: string ## The user's username, not unique across the platform.
|
||||||
discriminator*: cushort ## The user's 4-digit discord-tag.
|
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.
|
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).
|
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.
|
publicFlags*: int ## The public [flags](https://discord.com/developers/docs/resources/user#user-object-user-flags) on a user's account. (User Badges)
|
||||||
premiumType*: NitroSubscription ## The type of Nitro subscription on a user's account.
|
avatarRaw: array[2, uint64] ## The split hash for the 128bit hexadeximal avatar.
|
||||||
publicFlags*: int ## The public flags on a user's account.
|
isAvatarGif: bool ## Wether the avatar is a gif.
|
||||||
|
|
||||||
|
|
||||||
ClientUser* = ref object of User
|
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.
|
mfaEnabled*: bool ## Whether the user has two factor authentication enabled on their account.
|
||||||
locale*: string ## The user's chosen language option.
|
locale*: string ## The user's chosen language option.
|
||||||
verified*: bool ## Whether or not the current user has a verified email.
|
verified*: bool ## Whether or not the current user has a verified email.
|
||||||
email*: string ## The current user's 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.} =
|
proc newUser*(user: JsonNode): User {.inline.} =
|
||||||
return User(
|
result = User(
|
||||||
id: getIDFromJson(user["id"].getStr()),
|
id: getIDFromJson(user["id"].getStr()),
|
||||||
username: user["username"].getStr(),
|
username: user["username"].getStr(),
|
||||||
discriminator: cushort(parseIntEasy(user["discriminator"].getStr())),
|
discriminator: cushort(parseIntEasy(user["discriminator"].getStr())),
|
||||||
avatar: user["avatar"].getStr(),
|
|
||||||
bot: user{"bot"}.getBool(),
|
bot: user{"bot"}.getBool(),
|
||||||
system: user{"system"}.getBool(),
|
system: user{"system"}.getBool(),
|
||||||
flags: user{"flags"}.getInt(),
|
|
||||||
premiumType: NitroSubscription(user{"premium_type"}.getInt()),
|
|
||||||
publicFlags: user{"public_flags"}.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.} =
|
proc newClientUser*(clientUser: JsonNode): ClientUser {.inline.} =
|
||||||
return ClientUser(
|
result = ClientUser(
|
||||||
id: getIDFromJson(clientUser["id"].getStr()),
|
id: getIDFromJson(clientUser["id"].getStr()),
|
||||||
username: clientUser["username"].getStr(),
|
username: clientUser["username"].getStr(),
|
||||||
discriminator: cushort(parseIntEasy(clientUser["discriminator"].getStr())),
|
discriminator: cushort(parseIntEasy(clientUser["discriminator"].getStr())),
|
||||||
avatar: clientUser["avatar"].getStr(),
|
|
||||||
bot: clientUser{"bot"}.getBool(),
|
bot: clientUser{"bot"}.getBool(),
|
||||||
system: clientUser{"system"}.getBool(),
|
system: clientUser{"system"}.getBool(),
|
||||||
mfaEnabled: clientUser{"mfa_enabled"}.getBool(),
|
mfaEnabled: clientUser{"mfa_enabled"}.getBool(),
|
||||||
|
@ -52,3 +63,47 @@ proc newClientUser*(clientUser: JsonNode): ClientUser {.inline.} =
|
||||||
premiumType: NitroSubscription(clientUser{"premium_type"}.getInt()),
|
premiumType: NitroSubscription(clientUser{"premium_type"}.getInt()),
|
||||||
publicFlags: clientUser{"public_flags"}.getInt()
|
publicFlags: clientUser{"public_flags"}.getInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
Reference in New Issue