diff --git a/src/cache.nim b/src/cache.nim new file mode 100644 index 0000000..8dc7b04 --- /dev/null +++ b/src/cache.nim @@ -0,0 +1,29 @@ +import sequtils, message, member, channel, guild, discordobject, nimcordutils, clientobjects, httpcore + +type Cache* = ref object + members*: seq[GuildMember] + messages*: seq[Message] + channels*: seq[Channel] + guilds*: seq[Guild] + +proc getMessageChannel*(msg: Message, cache: Cache): Channel = + for index, channel in cache.channels: + if (channel.id == msg.channelID): + return channel + + return nil + +proc getChannelGuild*(channel: Channel, cache: Cache): Guild = + for index, guild in cache.guilds: + if (guild.id == channel.guildID): + return guild + + return nil + +proc getChannel*(cache: Cache, id: snowflake): Channel = + for index, channel in cache.channels: + if (channel.id == id): + return channel + + return newChannel(sendRequest(endpoint("/channels/" & $id), HttpGet, + defaultHeaders(), id, RateLimitBucketType.channel)) \ No newline at end of file diff --git a/src/channel.nim b/src/channel.nim new file mode 100644 index 0000000..d94dc29 --- /dev/null +++ b/src/channel.nim @@ -0,0 +1,141 @@ +import json, discordobject, user, options, nimcordutils, message, httpcore, asyncdispatch, asyncfutures, strutils + +type + ChannelType* = enum + chanTypeNIL = -1, + chanTypeGuildText = 0, + chanTypeDM = 1, + chanTypeGuildVoice = 2, + chanTypeGroupDM = 3, + chanTypeGuildCategory = 4, + chanTypeGuildNews = 5, + chanTypeGuildStore = 6 + + Channel* = ref object of DiscordObject + `type`*: ChannelType ## The type of channel. + guildID*: snowflake ## The id of the guild. + position*: int ## Sorting position of the channel. + #permissionOverwrites*: seq[Permissions] ## Explicit permission overwrites for members and roles. + name*: string ## The name of the channel (2-100 characters). + topic*: string ## The channel topic (0-1024 characters). + nsfw*: bool ## Whether the channel is nsfw. + lastMessageID*: snowflake ## The id of the last message sent in this channel (may not point to an existing or valid message). + bitrate*: int ## The bitrate (in bits) of the voice channel. + userLimit*: int ## The user limit of the voice channel. + rateLimitPerUser*: int ## Amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission manage_messages or manage_channel, are unaffected. + recipients*: seq[User] ## The recipients of the DM + icon*: string ## Icon hash + ownerID: snowflake ## ID of the DM creator + applicationID: snowflake ## Application id of the group DM creator if it is bot-created + parentID: snowflake ## ID of the parent category for a channel + lastPinTimestamp: string ## When the last pinned message was pinned + + ChannelModify* {.requiresInit.} = ref object + ## Use this type to modify 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] + #permissionOverwrites*: seq[Permissions] ## Explicit permission overwrites for members and roles. + parentID*: Option[snowflake] + +#[ proc newChannelModify*(name: Option[string], `type`: Option[ChannelType], position: Option[int], + topic: Option[string], nsfw: Option[bool], rateLimitPerUser: Option[int], bitrate: Option[int], + userLimit: Option[int], parentID: Option[snowflake]): ChannelModify = + + return ChannelModify(name: name.get, `type`:`type`, position: position, nsfw: nsfw, + rateLimitPerUser: rateLimitPerUser, bitrate: bitrate, userLimit: userLimit, parentID: parentID) ]# + +proc newChannel*(channel: JsonNode): Channel {.inline.} = + var chan = Channel( + id: getIDFromJson(channel["id"].getStr()), + `type`: ChannelType(channel["type"].getInt()), + ) + + if (channel.contains("guild_id")): + chan.guildID = getIDFromJson(channel["guild_id"].getStr()) + if (channel.contains("position")): + chan.position = channel["position"].getInt() + if (channel.contains("permission_overwrites")): + echo "permission_overwrites" + if (channel.contains("name")): + chan.name = channel["name"].getStr() + if (channel.contains("topic")): + chan.topic = channel["topic"].getStr() + if (channel.contains("nsfw")): + chan.nsfw = channel["nsfw"].getBool() + if (channel.contains("last_message_id")): + chan.lastMessageID = getIDFromJson(channel["last_message_id"].getStr()) + if (channel.contains("bitrate")): + chan.bitrate = channel["bitrate"].getInt() + if (channel.contains("user_limit")): + chan.userLimit = channel["user_limit"].getInt() + if (channel.contains("rate_limit_per_user")): + chan.rateLimitPerUser = channel["rate_limit_per_user"].getInt() + if (channel.contains("recipients")): + for recipient in channel["recipients"]: + chan.recipients.insert(newUser(recipient)) + if (channel.contains("icon")): + chan.icon = channel["icon"].getStr() + if (channel.contains("owner_id")): + chan.ownerID = getIDFromJson(channel["owner_id"].getStr()) + if (channel.contains("application_id")): + chan.applicationID = getIDFromJson(channel["application_id"].getStr()) + if (channel.contains("parent_id")): + chan.parentID = getIDFromJson(channel["parent_id"].getStr()) + if (channel.contains("last_pin_timestamp")): + chan.lastPinTimestamp = channel["last_pin_timestamp"].getStr() + + return chan + +proc sendMessage*(channel: Channel, content: string, tts: bool = false): Message = + let messagePayload = %*{"content": content, "tts": tts} + + return newMessage(sendRequest(endpoint("/channels/" & $channel.id & "/messages"), HttpPost, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), channel.id, + RateLimitBucketType.channel, messagePayload)) + +proc modifyChannel*(channel: Channel, modify: ChannelModify): Future[Channel] {.async.} = + var modifyPayload = %*{} + + if (modify.name.isSome): + modifyPayload.add("name", %modify.name.get()) + + if (modify.`type`.isSome): + modifyPayload.add("type", %modify.`type`.get()) + + if (modify.position.isSome): + modifyPayload.add("position", %modify.position.get()) + + if (modify.topic.isSome): + modifyPayload.add("topic", %modify.topic.get()) + + if (modify.nsfw.isSome): + modifyPayload.add("nsfw", %modify.nsfw.get()) + + if (modify.rateLimitPerUser.isSome): + modifyPayload.add("rate_limit_per_user", %modify.rateLimitPerUser.get()) + + if (modify.bitrate.isSome): + modifyPayload.add("bitrate", %modify.bitrate.get()) + + if (modify.userLimit.isSome): + modifyPayload.add("user_limit", %modify.userLimit.get()) + + #[ if (modify.name.isSome): + modifyPayload.add("permission_overwrites", %modify.parentID.get()0 ]# + + if (modify.parentID.isSome): + modifyPayload.add("parent_id", %modify.parentID.get()) + + return newChannel(sendRequest(endpoint("/channels/" & $channel.id), HttpPatch, + defaultHeaders(newHttpHeaders({"Content-Type": "application/json"})), + channel.id, RateLimitBucketType.channel, modifyPayload)) + +proc deleteChannel*(channel: Channel) {.async.} = + discard sendRequest(endpoint("/channels/" & $channel.id), HttpDelete, + defaultHeaders(), channel.id, RateLimitBucketType.channel) \ No newline at end of file diff --git a/src/client.nim b/src/client.nim index e9ce022..d622c66 100644 --- a/src/client.nim +++ b/src/client.nim @@ -1,4 +1,11 @@ -import websocket, asyncnet, asyncdispatch, json, httpClient, eventdispatcher, strformat, eventhandler, streams, nimcordutils, discordobject +import websocket, asyncnet, asyncdispatch, json, httpClient, eventdispatcher, strformat +import eventhandler, streams, nimcordutils, discordobject, user, cache, clientobjects +import strutils, channel, options + +const + nimcordMajor = 0 + nimcordMinor = 0 + nimcordMicro = 0 type DiscordOpCode = enum @@ -14,41 +21,12 @@ type opHello = 10, opHeartbeatAck = 11 - DiscordClient* = ref object ## Discord Client - token*: string - #user*: User - #cache: Cache - ws: AsyncWebSocket - httpClient: AsyncHttpClient - heartbeatInterval: int - heartbeatAcked: bool - lastSequence: int - -var globalClient: DiscordClient - proc defaultHeaders*(client: DiscordClient, added: HttpHeaders = newHttpHeaders()): HttpHeaders = added.add("Authorization", fmt("Bot {client.token}")) added.add("User-Agent", "NimCord (https://github.com/SeanOMik/nimcord, v0.0.0)") added.add("X-RateLimit-Precision", "millisecond") return added; -proc sendRequest*(endpoint: string, httpMethod: HttpMethod, headers: HttpHeaders, objectID: snowflake = 0, bucketType: RateLimitBucketType = global, jsonBody: JsonNode = %*{}): JsonNode = - var client = newHttpClient() - # Add headers - client.headers = headers - - var strPayload: string - if ($jsonBody == "{}"): - strPayload = "" - else: - strPayload = $jsonBody - - echo "Sending GET request, URL: ", endpoint, ", body: ", strPayload - - waitForRateLimits(objectID, bucketType) - - return handleResponse(client.request(endpoint, httpMethod, strPayload), objectId, bucketType) - proc sendGatewayRequest*(client: DiscordClient, request: JsonNode, msg: string = "") {.async.} = if (msg.len == 0): echo "Sending gateway payload: ", request @@ -96,16 +74,11 @@ proc handleWebsocketPacket(client: DiscordClient) {.async.} = of ord(DiscordOpCode.opHeartbeatAck): client.heartbeatAcked = true of ord(DiscordOpCode.opDispatch): - handleDiscordEvent(json["d"], json["t"].getStr()) + discard handleDiscordEvent(client, json["d"], json["t"].getStr()) else: discard proc startConnection*(client: DiscordClient) {.async.} = - globalClient = client - - client.httpClient = newAsyncHttpClient() - client.httpClient.headers = newHttpHeaders({"Authorization": fmt("Bot {client.token}")}) - let urlResult = sendRequest(endpoint("/gateway/bot"), HttpMethod.HttpGet, client.defaultHeaders()) if (urlResult.contains("url")): let url = urlResult["url"].getStr() @@ -122,6 +95,14 @@ proc startConnection*(client: DiscordClient) {.async.} = e.msg = "Failed to get gateway url, token may of been incorrect!" raise e +proc newDiscordClient(tkn: string): DiscordClient = + globalToken = tkn + + var cac: Cache + new(cac) + + result = DiscordClient(token: tkn, cache: cac) + var tokenStream = newFileStream("token.txt", fmRead) var tkn: string if (not isNil(tokenStream)): @@ -130,21 +111,45 @@ if (not isNil(tokenStream)): tokenStream.close() -var bot = DiscordClient(token: tkn) +var bot = newDiscordClient(tkn) registerEventListener(EventType.evtReady, proc(bEvt: BaseEvent) = let event = ReadyEvent(bEvt) - echo "Ready and connected 1!" -) + bot.clientUser = event.clientUser -registerEventListener(EventType.evtReady, proc(bEvt: BaseEvent) = - let event = ReadyEvent(bEvt) - echo "Ready and connected 2!" + echo "Ready! (v", nimcordMajor, ".", nimcordMinor, ".", nimcordMicro, ")" + echo "Logged in as: ", bot.clientUser.username, "#", bot.clientUser.discriminator + echo "ID: ", bot.clientUser.id + echo "--------------------" ) registerEventListener(EventType.evtMessageCreate, proc(bEvt: BaseEvent) = let event = MessageCreateEvent(bEvt) - echo "Message was created!" + + if (event.message.content == "?ping"): + var channel: Channel = event.message.getMessageChannel(event.client.cache) + if (channel != nil): + discard channel.sendMessage("PONG") + elif (event.message.content.startsWith("?modifyChannelTopic")): + let modifyTopic = event.message.content.substr(20) + var channel: Channel = event.message.getMessageChannel(event.client.cache) + if (channel != nil): + discard channel.sendMessage("Modifing Channel!") + discard channel.modifyChannel(ChannelModify(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) + if (channel != nil): + discard channel.sendMessage("Deleting Channel!") + discard channel.deleteChannel() + discard channel.sendMessage("Deleted Channel!") ) +#[ registerEventListener(EventType.evtGuildCreate, proc(bEvt: BaseEvent) = + let event = GuildCreateEvent(bEvt) + + echo "Guild has ", event.guild.members.len, " members and ", event.guild.channels.len, " channels..." +) ]# + waitFor bot.startConnection() \ No newline at end of file diff --git a/src/clientobjects.nim b/src/clientobjects.nim new file mode 100644 index 0000000..fdd28b7 --- /dev/null +++ b/src/clientobjects.nim @@ -0,0 +1,10 @@ +import websocket, cache, user + +type DiscordClient* = ref object ## Discord Client + token*: string + clientUser*: User + cache*: Cache + ws*: AsyncWebSocket + heartbeatInterval*: int + heartbeatAcked*: bool + lastSequence*: int \ No newline at end of file