From c8a65e794c359589b394cfc5f9d579eb3a5949f7 Mon Sep 17 00:00:00 2001 From: Filip Hoffmann Date: Mon, 14 Apr 2025 15:46:00 +0200 Subject: [PATCH] guilds --- .gitignore | 1 + examples/.github/workflows/test.yml | 23 ++++ examples/.gitignore | 4 + examples/README.md | 24 ++++ examples/gleam.toml | 22 ++++ examples/manifest.toml | 23 ++++ examples/src/examples.gleam | 18 +++ examples/test/examples_test.gleam | 12 ++ gleam.toml | 1 + manifest.toml | 2 + src/flybycord.gleam | 14 --- src/flybycord/client.gleam | 7 ++ src/flybycord/internal/error.gleam | 1 + src/flybycord/internal/requests.gleam | 73 +++++++++++-- src/flybycord/user.gleam | 152 +++++++++++++++++++++----- 15 files changed, 324 insertions(+), 53 deletions(-) create mode 100644 examples/.github/workflows/test.yml create mode 100644 examples/.gitignore create mode 100644 examples/README.md create mode 100644 examples/gleam.toml create mode 100644 examples/manifest.toml create mode 100644 examples/src/examples.gleam create mode 100644 examples/test/examples_test.gleam delete mode 100644 src/flybycord.gleam create mode 100644 src/flybycord/client.gleam diff --git a/.gitignore b/.gitignore index 9907156..73e933b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /build erl_crash.dump /.direnv +.env diff --git a/examples/.github/workflows/test.yml b/examples/.github/workflows/test.yml new file mode 100644 index 0000000..bfb787f --- /dev/null +++ b/examples/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "27.1.2" + gleam-version: "1.9.1" + rebar3-version: "3" + # elixir-version: "1" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..89f7517 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,24 @@ +# examples + +[![Package Version](https://img.shields.io/hexpm/v/examples)](https://hex.pm/packages/examples) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/examples/) + +```sh +gleam add examples@1 +``` +```gleam +import examples + +pub fn main() { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/examples/gleam.toml b/examples/gleam.toml new file mode 100644 index 0000000..c8f2b6d --- /dev/null +++ b/examples/gleam.toml @@ -0,0 +1,22 @@ +name = "examples" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" +flybycord = { path = ".." } +envoy = ">= 1.0.2 and < 2.0.0" +dotenv_gleam = ">= 1.0.7 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/examples/manifest.toml b/examples/manifest.toml new file mode 100644 index 0000000..d2b71f0 --- /dev/null +++ b/examples/manifest.toml @@ -0,0 +1,23 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "dotenv_gleam", version = "1.0.7", build_tools = ["gleam"], requirements = ["envoy", "gleam_erlang", "gleam_stdlib", "simplifile"], otp_app = "dotenv_gleam", source = "hex", outer_checksum = "E5C63C326508C4ABF29FB29257A40F9275D63CBE605EEFC807CDCD571E7B2F51" }, + { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "flybycord", version = "1.0.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_http", "gleam_httpc", "gleam_json", "gleam_stdlib"], source = "local", path = ".." }, + { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" }, + { name = "gleam_http", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "0A62451FC85B98062E0907659D92E6A89F5F3C0FBE4AB8046C99936BF6F91DBC" }, + { name = "gleam_httpc", version = "4.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "1A38507AF26CACA145248733688703EADCB734EA971D4E34FB97B7613DECF132" }, + { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" }, + { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" }, + { name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" }, + { name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" }, +] + +[requirements] +dotenv_gleam = { version = ">= 1.0.7 and < 2.0.0" } +envoy = { version = ">= 1.0.2 and < 2.0.0" } +flybycord = { path = ".." } +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/examples/src/examples.gleam b/examples/src/examples.gleam new file mode 100644 index 0000000..fe844bf --- /dev/null +++ b/examples/src/examples.gleam @@ -0,0 +1,18 @@ +import dotenv_gleam +import envoy +import flybycord/client +import flybycord/user + +pub fn main() { + dotenv_gleam.config() + let assert Ok(token) = envoy.get("TOKEN") + let client = client.Client(token: token) + let assert Ok(user) = + client + |> user.get_user("808763186811633735") + + echo user + + let assert Ok(self) = client |> user.get_current_user + echo self +} diff --git a/examples/test/examples_test.gleam b/examples/test/examples_test.gleam new file mode 100644 index 0000000..3831e7a --- /dev/null +++ b/examples/test/examples_test.gleam @@ -0,0 +1,12 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + 1 + |> should.equal(1) +} diff --git a/gleam.toml b/gleam.toml index 4b8f954..5b08dbd 100644 --- a/gleam.toml +++ b/gleam.toml @@ -17,6 +17,7 @@ gleam_stdlib = ">= 0.44.0 and < 2.0.0" gleam_httpc = ">= 4.1.0 and < 5.0.0" gleam_http = ">= 4.0.0 and < 5.0.0" gleam_json = ">= 2.3.0 and < 3.0.0" +gleam_time = ">= 1.1.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index 77570c2..91a2f7c 100644 --- a/manifest.toml +++ b/manifest.toml @@ -7,6 +7,7 @@ packages = [ { name = "gleam_httpc", version = "4.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "1A38507AF26CACA145248733688703EADCB734EA971D4E34FB97B7613DECF132" }, { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" }, { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" }, + { name = "gleam_time", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "CBAA68E215BEA961B40F3B1BA7B88741B1EB49572EF5E1154E357A5D152A2A2C" }, { name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" }, ] @@ -15,4 +16,5 @@ gleam_http = { version = ">= 4.0.0 and < 5.0.0" } gleam_httpc = { version = ">= 4.1.0 and < 5.0.0" } gleam_json = { version = ">= 2.3.0 and < 3.0.0" } gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +gleam_time = { version = ">= 1.1.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/flybycord.gleam b/src/flybycord.gleam deleted file mode 100644 index 541272c..0000000 --- a/src/flybycord.gleam +++ /dev/null @@ -1,14 +0,0 @@ -import flybycord/client -import flybycord/user - -pub fn main() { - let client = - client.Client( - token: "MTM1Mjk5Nzc2ODY3OTEzMzE4NA.GdrKea.QDZfIY6THPdJjUuFwpcR4Bj8h905fwC6N-EtY0", - ) - let assert Ok(user) = - client - |> user.get_user("808763186811633735") - - echo user -} diff --git a/src/flybycord/client.gleam b/src/flybycord/client.gleam new file mode 100644 index 0000000..428a7f9 --- /dev/null +++ b/src/flybycord/client.gleam @@ -0,0 +1,7 @@ +pub type Client { + Client(token: String) +} + +pub fn version() { + "v0.0.0" +} diff --git a/src/flybycord/internal/error.gleam b/src/flybycord/internal/error.gleam index 1e0273b..1d63b8b 100644 --- a/src/flybycord/internal/error.gleam +++ b/src/flybycord/internal/error.gleam @@ -4,4 +4,5 @@ import gleam/json pub type FlybycordError { HttpError(httpc.HttpError) DecodeError(json.DecodeError) + StatusCodeUnsuccessful(Int) } diff --git a/src/flybycord/internal/requests.gleam b/src/flybycord/internal/requests.gleam index 1d2d35c..b8dcb4d 100644 --- a/src/flybycord/internal/requests.gleam +++ b/src/flybycord/internal/requests.gleam @@ -1,30 +1,83 @@ -import flybycord/client +import flybycord/client.{type Client} import flybycord/internal/error import gleam/http import gleam/http/request import gleam/http/response.{type Response} import gleam/httpc +import gleam/json import gleam/result const discord_url = "discord.com" const discord_api_path = "api/v10" -pub fn get( - client: client.Client, - path: String, -) -> Result(Response(String), error.FlybycordError) { +fn new_request(token: String, path: String) { request.new() |> request.set_scheme(http.Https) |> request.set_host(discord_url) |> request.set_path(discord_api_path <> path) - |> request.set_method(http.Get) - |> request.prepend_header("authorization", "Bot " <> client.token) + |> request.prepend_header("authorization", "Bot " <> token) |> request.prepend_header( "user-agent", - "DiscordBot (https://github.com/folospior/flybycord, v0.0.0)", + "DiscordBot (https://github.com/folospior/flybycord, " + <> client.version() + <> ")", ) |> request.prepend_header("content-type", "application/json") - |> httpc.send - |> result.map_error(error.HttpError) +} + +fn ensure_status_code_success( + response: Response(String), +) -> Result(Response(String), error.FlybycordError) { + case response.status { + status if status >= 200 && status < 300 -> Ok(response) + _ -> Error(error.StatusCodeUnsuccessful(response.status)) + } +} + +pub fn get( + client: Client, + to path: String, +) -> Result(Response(String), error.FlybycordError) { + use response <- result.try( + new_request(client.token, path) + |> request.set_method(http.Get) + |> httpc.send + |> result.map_error(error.HttpError), + ) + + response + |> ensure_status_code_success +} + +pub fn patch( + client: Client, + to path: String, + using data: json.Json, +) -> Result(Response(String), error.FlybycordError) { + use response <- result.try( + new_request(client.token, path) + |> request.set_method(http.Patch) + |> request.set_body(data |> json.to_string) + |> httpc.send + |> result.map_error(error.HttpError), + ) + + response + |> ensure_status_code_success +} + +pub fn delete( + client: Client, + to path: String, +) -> Result(Response(String), error.FlybycordError) { + use response <- result.try( + new_request(client.token, path) + |> request.set_method(http.Delete) + |> httpc.send + |> result.map_error(error.HttpError), + ) + + response + |> ensure_status_code_success } diff --git a/src/flybycord/user.gleam b/src/flybycord/user.gleam index bad077f..5caf798 100644 --- a/src/flybycord/user.gleam +++ b/src/flybycord/user.gleam @@ -1,11 +1,12 @@ -import flybycord/client +import flybycord/client.{type Client} +import flybycord/image import flybycord/internal/error import flybycord/internal/requests import gleam/dynamic/decode import gleam/int import gleam/json import gleam/list -import gleam/option.{type Option} +import gleam/option.{type Option, None} import gleam/result // TYPES ---------------------------------------------------------------------- @@ -23,9 +24,8 @@ pub type User { banner_hash: Option(String), accent_color: Option(Int), locale: Option(String), - flags: Option(Int), + flags: Option(List(Flag)), premium_type: Option(PremiumType), - public_flags: Option(List(PublicFlag)), avatar_decoration_data: Option(AvatarDecorationData), ) } @@ -35,14 +35,14 @@ pub type AvatarDecorationData { } pub type PremiumType { - None + NoPremium NitroClassic Nitro NitroBasic Invalid } -pub type PublicFlag { +pub type Flag { Staff Partner Hypesquad @@ -60,54 +60,93 @@ pub type PublicFlag { ActiveDeveloper } +pub type ModifyCurrentUser { + ModifyCurrentUser(username: String, avatar: image.Data, banner: image.Data) +} + // CONSTANTS ------------------------------------------------------------------ const bits_flags = [ + // 1 << 0 #(1, Staff), + // 1 << 1 #(2, Partner), + // 1 << 2 #(4, Hypesquad), + // 1 << 3 #(8, BugHunterLevel1), + // 1 << 6 #(64, HypesquadBravery), + // 1 << 7 #(128, HypesquadBrilliance), + // 1 << 8 #(256, HypesquadBalance), + // 1 << 9 #(512, PremiumEarlySupporter), + // 1 << 10 #(1024, TeamPseudoUser), + // 1 << 14 #(16_384, BugHunterLevel2), + // 1 << 16 #(65_536, VerifiedBot), + // 1 << 17 #(131_072, VerifiedDeveloper), + // 1 << 18 #(262_144, CertifiedModerator), + // 1 << 19 #(524_288, BotHttpInteractions), + // 1 << 22 #(4_194_304, ActiveDeveloper), ] // DECODERS ------------------------------------------------------------------- -fn user_decoder() -> decode.Decoder(User) { +@internal +pub fn decoder() -> decode.Decoder(User) { use id <- decode.field("id", decode.string) use username <- decode.field("username", decode.string) use discriminator <- decode.field("discriminator", decode.string) use global_name <- decode.field("global_name", decode.optional(decode.string)) use avatar_hash <- decode.field("avatar", decode.optional(decode.string)) - use is_bot <- decode.field("bot", decode.optional(decode.bool)) - use is_system <- decode.field("system", decode.optional(decode.bool)) - use is_mfa_enabled <- decode.field( + use is_bot <- decode.optional_field("bot", None, decode.optional(decode.bool)) + use is_system <- decode.optional_field( + "system", + None, + decode.optional(decode.bool), + ) + use is_mfa_enabled <- decode.optional_field( "mfa_enabled", + None, decode.optional(decode.bool), ) - use banner_hash <- decode.field("banner", decode.optional(decode.string)) - use accent_color <- decode.field("accent_color", decode.optional(decode.int)) - use locale <- decode.field("locale", decode.optional(decode.string)) - use flags <- decode.field("flags", decode.optional(decode.int)) - use premium_type <- decode.field( + use banner_hash <- decode.optional_field( + "banner", + None, + decode.optional(decode.string), + ) + use accent_color <- decode.optional_field( + "accent_color", + None, + decode.optional(decode.int), + ) + use locale <- decode.optional_field( + "locale", + None, + decode.optional(decode.string), + ) + use flags <- decode.optional_field( + "flags", + None, + decode.optional(flags_decoder()), + ) + use premium_type <- decode.optional_field( "premium_type", + None, decode.optional(premium_type_decoder()), ) - use public_flags <- decode.field( - "public_flags", - decode.optional(public_flags_decoder()), - ) - use avatar_decoration_data <- decode.field( + use avatar_decoration_data <- decode.optional_field( "avatar_decoration_data", + None, decode.optional(avatar_decoration_data_decoder()), ) decode.success(User( @@ -124,29 +163,31 @@ fn user_decoder() -> decode.Decoder(User) { locale:, flags:, premium_type:, - public_flags:, avatar_decoration_data:, )) } -fn avatar_decoration_data_decoder() -> decode.Decoder(AvatarDecorationData) { +@internal +pub fn avatar_decoration_data_decoder() -> decode.Decoder(AvatarDecorationData) { use asset <- decode.field("asset", decode.string) use sku_id <- decode.field("sku_id", decode.string) decode.success(AvatarDecorationData(asset:, sku_id:)) } -fn premium_type_decoder() -> decode.Decoder(PremiumType) { +@internal +pub fn premium_type_decoder() -> decode.Decoder(PremiumType) { use variant <- decode.then(decode.int) case variant { - 0 -> decode.success(None) + 0 -> decode.success(NoPremium) 1 -> decode.success(NitroClassic) 2 -> decode.success(Nitro) 3 -> decode.success(NitroBasic) - _ -> decode.success(Invalid) + _ -> decode.failure(Invalid, "Premium Type") } } -fn public_flags_decoder() -> decode.Decoder(List(PublicFlag)) { +@internal +pub fn flags_decoder() -> decode.Decoder(List(Flag)) { use flags <- decode.then(decode.int) bits_flags @@ -160,10 +201,22 @@ fn public_flags_decoder() -> decode.Decoder(List(PublicFlag)) { |> decode.success } -// PUBLIC FUNCTIONS ----------------------------------------------------------- +// ENCODERS ------------------------------------------------------------------- + +fn encode_modify_current_user( + modify_current_user: ModifyCurrentUser, +) -> json.Json { + json.object([ + #("username", json.string(modify_current_user.username)), + #("avatar", json.string(modify_current_user.avatar)), + #("banner", json.string(modify_current_user.banner)), + ]) +} + +// PUBLIC API FUNCTIONS ------------------------------------------------------- pub fn get_user( - client: client.Client, + client: Client, id: String, ) -> Result(User, error.FlybycordError) { use response <- result.try( @@ -172,6 +225,47 @@ pub fn get_user( ) response.body - |> json.parse(using: user_decoder()) + |> json.parse(using: decoder()) + |> result.map_error(error.DecodeError) +} + +pub fn get_current_user( + client: client.Client, +) -> Result(User, error.FlybycordError) { + use response <- result.try( + client + |> requests.get(to: "/users/@me"), + ) + + response.body + |> json.parse(using: decoder()) |> result.map_error(error.DecodeError) } + +pub fn modify_current_user( + client: Client, + with data: ModifyCurrentUser, +) -> Result(User, error.FlybycordError) { + let json_data = data |> encode_modify_current_user + + use response <- result.try( + client + |> requests.patch(to: "/users/@me", using: json_data), + ) + + response.body + |> json.parse(using: decoder()) + |> result.map_error(error.DecodeError) +} + +pub fn leave_guild( + client: Client, + guild_id: String, +) -> Result(Nil, error.FlybycordError) { + use _ <- result.try( + client + |> requests.delete(to: "/users/@me/guilds/" <> guild_id), + ) + + Ok(Nil) +} -- 2.51.2