From 2ecad41c529a15b5f6a7bf5dd4ebe78f15d5626b Mon Sep 17 00:00:00 2001 From: Yuzu Date: Mon, 9 Dec 2024 15:59:58 -0500 Subject: [PATCH] bunch of changes --- README.md | 11 --- src/http.zig | 16 +++- src/shard.zig | 149 +++++++++++++++++++++++++++++++- src/structures/application.zig | 54 ++++++++++++ src/structures/gateway.zig | 2 +- src/structures/message.zig | 11 +++ src/structures/monetization.zig | 12 +++ vendor/zjson/json.zig | 6 +- 8 files changed, 241 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index c491284..52d64b0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ A high-performance bleeding edge Discord library in Zig, featuring full API cove * 100% API Coverage & Fully Typed: Offers complete access to Discord's API with strict typing for reliable and safe code. * High Performance: Faster than whichever library you can name (WIP) * Flexible Payload Parsing: Supports payload parsing through both zlib and zstd*. -* Language Agnostic: Primarily in Zig, but also compatible with JavaScript. (PERHAPS?) ```zig const Client = @import("discord.zig").Client; @@ -75,23 +74,13 @@ Contributions are welcome! Please open an issue or pull request if you'd like to ## http methods missing | Endpoint | Support | |----------------------------------------|---------| -| Application related | ❌ | | Audit log | ❌ | | Automod | ❌ | -| Channel related | ✅ | -| Emoji related | ✅ | -| Entitlement related | ❌ | -| Guild related | ✅ | | Guild Scheduled Event related | ❌ | | Guild template related | ❌ | -| Invite related | ✅ | -| Message related | ✅ | -| Poll related | ✅ | -| SKU related | ❌ | | Soundboard related | ❌ | | Stage Instance related | ❌ | | Sticker related | ❌ | | Subscription related | ❌ | -| User related | ✅ | | Voice related | ❌ | | Webhook related | ❌ | diff --git a/src/http.zig b/src/http.zig index bb2fe17..45bf4b2 100644 --- a/src/http.zig +++ b/src/http.zig @@ -65,8 +65,11 @@ pub const FetchReq = struct { try self.extra_headers.append(http.Header{ .name = name, .value = value }); } - pub fn addQueryParam(self: *FetchReq, name: []const u8, value: []const u8) !void { - try self.query_params.put(name, value); + pub fn addQueryParam(self: *FetchReq, name: []const u8, value: anytype) !void { + if (value == null) + return; + var buf: [256]u8 = undefined; + try self.query_params.put(name, try std.fmt.bufPrint(&buf, "{any}", .{value})); } fn formatQueryParams(self: *FetchReq) ![]const u8 { @@ -171,6 +174,15 @@ pub const FetchReq = struct { return error.FailedRequest; } + pub fn put4(self: *FetchReq, comptime T: type, path: []const u8) !zjson.Owned(T) { + const result = try self.makeRequest(.PUT, path, null); + + if (result.status != .ok) + return error.FailedRequest; + + return try zjson.parse(T, self.allocator, try self.body.toOwnedSlice()); + } + pub fn post(self: *FetchReq, comptime T: type, path: []const u8, object: anytype) !zjson.Owned(T) { var buf: [4096]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&buf); diff --git a/src/shard.zig b/src/shard.zig index 3473899..3da0ece 100644 --- a/src/shard.zig +++ b/src/shard.zig @@ -796,14 +796,18 @@ pub const RequestFailedError = zjson.ParserError || MakeRequestError || error{Fa /// If operating on a guild channel, this endpoint requires the current user to have the `VIEW_CHANNEL` permission. /// If the channel is a voice channel, they must also have the `CONNECT` permission. /// If the current user is missing the `READ_MESSAGE_HISTORY` permission in the channel, then no messages will be returned. -/// TODO: add query params -pub fn fetchMessages(self: *Self, channel_id: Snowflake) RequestFailedError!zjson.Owned([]Types.Message) { +pub fn fetchMessages(self: *Self, channel_id: Snowflake, query: Types.GetMessagesQuery) RequestFailedError!zjson.Owned([]Types.Message) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/channels/{d}/messages", .{channel_id.into()}); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addQueryParam("limit", query.limit); + try req.addQueryParam("around", query.around); + try req.addQueryParam("before", query.before); + try req.addQueryParam("after", query.after); + const messages = try req.get([]Types.Message, path); return messages; } @@ -2423,7 +2427,8 @@ pub fn fetchAnswerVoters( /// Immediately ends the poll. /// You cannot end polls from other users. /// -/// Returns a message object. Fires a Message Update Gateway event. +/// Returns a message object. +/// Fires a Message Update Gateway event. pub fn endPoll( self: *Self, channel_id: Snowflake, @@ -2438,3 +2443,141 @@ pub fn endPoll( const msg = try req.post(Types.Message, path); return msg; } + +/// Returns the application object associated with the requesting bot user. +pub fn fetchMyApplication(self: *Self) RequestFailedError!zjson.Owned(Types.Application) { + var req = FetchReq.init(self.allocator, self.details.token); + defer req.deinit(); + + const app = try req.get(Types.Application, "/applications/@me"); + return app; +} + +/// Edit properties of the app associated with the requesting bot user. +/// Only properties that are passed will be updated. +/// Returns the updated application object on success. +pub fn editMyApplication(self: *Self, params: Types.ModifyApplication) RequestFailedError!zjson.Owned(Types.Application) { + var req = FetchReq.init(self.allocator, self.details.token); + defer req.deinit(); + + const app = try req.patch(Types.Application, "/applications/@me", params); + return app; +} + +/// Returns a serialized activity instance, if it exists. +/// Useful for preventing unwanted activity sessions. +pub fn fetchActivityInstance(self: *Self, application_id: Snowflake, insance: []const u8) RequestFailedError!zjson.Owned(Types.ActivityInstance) { + var buf: [256]u8 = undefined; + const path = try std.fmt.bufPrint(&buf, "/applications/{d}/activity-instances/{s}", .{ application_id.into(), insance }); + + var req = FetchReq.init(self.allocator, self.details.token); + defer req.deinit(); + + const activity_instance = try req.get(Types.ActivityInstance, path); + return activity_instance; +} + +/// Returns a list of application role connection metadata objects for the given application. +pub fn fetchApplicationRoleConnectionMetadataRecords(self: *Self, application_id: Snowflake) RequestFailedError!zjson.Owned([]Types.ApplicationRoleConnection) { + var buf: [256]u8 = undefined; + const path = try std.fmt.bufPrint(&buf, "/applications/{d}/role-connection/metadata", .{application_id.into()}); + + var req = FetchReq.init(self.allocator, self.details.token); + defer req.deinit(); + + return req.get([]Types.ApplicationRoleConnection, path); +} + +/// Updates and returns a list of application role connection metadata objects for the given application. +pub fn updateApplicationRoleConnectionMetadataRecords(self: *Self, application_id: Snowflake) RequestFailedError!zjson.Owned([]Types.ApplicationRoleConnection) { + var buf: [256]u8 = undefined; + const path = try std.fmt.bufPrint(&buf, "/applications/{d}/role-connection/metadata", .{application_id.into()}); + + var req = FetchReq.init(self.allocator, self.details.token); + defer req.deinit(); + + return req.put4([]Types.ApplicationRoleConnection, path); +} + +/// Returns all entitlements for a given app, active and expired. +pub fn fetchEntitlements(self: *Self, application_id: Snowflake) RequestFailedError!zjson.Owned([]Types.Entitlement) { + var buf: [256]u8 = undefined; + const path = try std.fmt.bufPrint(&buf, "/applications/{d}/entitlements", .{application_id.into()}); + + var req = FetchReq.init(self.allocator, self.details.token); + defer req.deinit(); + + const entitlements = try req.get([]Types.Entitlement, path); + return entitlements; +} + +/// Returns an entitlement. +pub fn fetchEntitlement(self: *Self, application_id: Snowflake, entitlement_id: Snowflake) RequestFailedError!zjson.Owned(Types.Entitlement) { + var buf: [256]u8 = undefined; + const path = try std.fmt.bufPrint(&buf, "/applications/{d}/entitlements/{d}", .{ application_id.into(), entitlement_id.into() }); + + var req = FetchReq.init(self.allocator, self.details.token); + defer req.deinit(); + + const entitlement = try req.get(Types.Entitlement, path); + return entitlement; +} + +/// For One-Time Purchase consumable SKUs, marks a given entitlement for the user as consumed. +/// The entitlement will have consumed: true when using List Entitlements. +/// +/// Returns a 204 No Content on success. +pub fn consumeEntitlement(self: *Self, application_id: Snowflake, entitlement_id: Snowflake) RequestFailedError!void { + var buf: [256]u8 = undefined; + const path = try std.fmt.bufPrint(&buf, "/applications/{d}/entitlements/{d}/consume", .{ application_id.into(), entitlement_id.into() }); + + var req = FetchReq.init(self.allocator, self.details.token); + defer req.deinit(); + + try req.post5(path); +} + +/// Creates a test entitlement to a given SKU for a given guild or user. +/// Discord will act as though that user or guild has entitlement to your premium offering. +/// +/// This endpoint returns a partial entitlement object. It will not contain `subscription_id`, `starts_at`, or `ends_at`, as it's valid in perpetuity. +/// +/// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. +pub fn createTestEntitlement( + self: *Self, + application_id: Snowflake, + params: Types.CreateTestEntitlement, +) RequestFailedError!zjson.Owned(Partial(Types.Entitlement)) { + var buf: [256]u8 = undefined; + const path = try std.fmt.bufPrint(&buf, "/applications/{d}/entitlements", .{application_id.into()}); + + var req = FetchReq.init(self.allocator, self.details.token); + defer req.deinit(); + + return req.post(Partial(Types.Entitlement), path, params); +} + +/// Deletes a currently-active test entitlement. Discord will act as though that user or guild no longer has entitlement to your premium offering. +/// +/// Returns 204 No Content on success. +pub fn deleteTestEntitlement(self: *Self, application_id: Snowflake) RequestFailedError!void { + var buf: [256]u8 = undefined; + const path = try std.fmt.bufPrint(&buf, "/applications/{d}/entitlements", .{application_id.into()}); + + var req = FetchReq.init(self.allocator, self.details.token); + defer req.deinit(); + + try req.delete(path); +} + +/// Returns all SKUs for a given application. +pub fn fetchSkus(self: *Self, application_id: Snowflake) RequestFailedError!zjson.Owner([]Types.Sku) { + var buf: [256]u8 = undefined; + const path = try std.fmt.bufPrint(&buf, "/applications/{d}/skus", .{application_id.into()}); + + var req = FetchReq.init(self.allocator, self.details.token); + defer req.deinit(); + + const skus = try req.get([]Types.Sku, path); + return skus; +} diff --git a/src/structures/application.zig b/src/structures/application.zig index 7d17213..77bbea2 100644 --- a/src/structures/application.zig +++ b/src/structures/application.zig @@ -107,3 +107,57 @@ pub const InstallParams = struct { /// Permissions to request for the bot role permissions: []const u8, }; + +pub const ModifyApplication = struct { + /// Default custom authorization URL for the app, if enabled + custom_install_url: ?[]const u8, + /// Description of the app + description: ?[]const u8, + /// Role connection verification URL for the app + role_connections_verification_url: ?[]const u8, + /// Settings for the app's default in-app authorization link, if enabled + install_params: ?InstallParams, + /// Default scopes and permissions for each supported installation context. + integration_types_config: ?ApplicationIntegrationType, + /// App's public flags + /// @remarks + /// Only limited intent flags (`GATEWAY_PRESENCE_LIMITED`, `GATEWAY_GUILD_MEMBERS_LIMITED`, and `GATEWAY_MESSAGE_CONTENT_LIMITED`) can be updated via the API. + flags: ?ApplicationFlags, + /// Icon for the app + icon: ?[]const u8, + /// Default rich presence invite cover image for the app + cover_image: ?[]const u8, + /// Interactions endpoint URL for the app + /// @remarks + /// To update an Interactions endpoint URL via the API, the URL must be valid + interaction_endpoint_url: ?[]const u8, + /// List of tags describing the content and functionality of the app (max of 20 characters per tag) + /// @remarks + /// There can only be a max of 5 tags + tags: ?[][]const u8, + /// Event webhook URL for the app to receive webhook events + event_webhooks_url: ?[]const u8, + /// If webhook events are enabled for the app. 1 to disable, and 2 to enable. + event_webhooks_status: ?ApplicationEventWebhookStatus, + /// List of Webhook event types the app subscribes to + event_webhooks_types: ?[]WebhookEventType, +}; + +pub const ApplicationEventWebhookStatus = enum(u8) { + /// Webhook events are disabled by developer + Disabled = 1, + /// Webhook events are enabled by developer */ + Enabled = 2, + /// Webhook events are disabled by Discord, usually due to inactivity */ + DisabledByDiscord = 3, +}; + +/// https://discord.com/developers/docs/events/webhook-events#event-types +pub const WebhookEventType = union(enum) { + /// Sent when an app was authorized by a user to a server or their account + APPLICATION_AUTHORIZED, + /// Entitlement was created + ENTITLEMENT_CREATE, + /// User was added to a Quest (currently unavailable) + QUEST_USER_ENROLLMENT, +}; diff --git a/src/structures/gateway.zig b/src/structures/gateway.zig index b1b38be..f01aa2a 100644 --- a/src/structures/gateway.zig +++ b/src/structures/gateway.zig @@ -126,7 +126,7 @@ pub const ActivityLocation = struct { }; /// https://discord.com/developers/docs/resources/application#get-application-activity-instance-activity-location-kind-enum -pub const ActivityLocationKind = enum { +pub const ActivityLocationKind = union(enum) { /// The Location is a Guild Channel gc, /// The Location is a Private Channel, such as a DM or GDM diff --git a/src/structures/message.zig b/src/structures/message.zig index 55262db..b6daaf1 100644 --- a/src/structures/message.zig +++ b/src/structures/message.zig @@ -370,3 +370,14 @@ pub const AllowedMentions = struct { /// For replies, whether to mention the author of the message being replied to (default false) replied_user: ?bool, }; + +pub const GetMessagesQuery = struct { + /// Get messages around this message ID, + around: ?Snowflake, + /// Get messages before this message ID + before: ?Snowflake, + /// Get messages after this message ID + after: ?Snowflake, + /// Max number of messages to return (1-100), + limit: ?usize = 50, +}; diff --git a/src/structures/monetization.zig b/src/structures/monetization.zig index 5d51a89..26e6988 100644 --- a/src/structures/monetization.zig +++ b/src/structures/monetization.zig @@ -88,3 +88,15 @@ pub const SkuType = enum(u4) { /// System-generated group for each SUBSCRIPTION SKU created SubscriptionGroup = 6, }; + +pub const CreateTestEntitlement = struct { + /// ID of the SKU to grant the entitlement to + sku_id: []const u8, + /// ID of the guild or user to grant the entitlement top + owner_id: []const u8, + /// 1 for a guild subscription, 2 for a user subscription + owner_type: enum(u8) { + guild_subscription = 1, + user_subscription = 2, + }, +}; diff --git a/vendor/zjson/json.zig b/vendor/zjson/json.zig index 8796304..6c3f29e 100644 --- a/vendor/zjson/json.zig +++ b/vendor/zjson/json.zig @@ -1101,12 +1101,12 @@ pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Er } /// meant to handle a `JsonType` value and handling the deinitialization thereof -pub fn Owned(comptime Struct: type) type { - if (@typeInfo(Struct) != .@"struct") @compileError("expected a `struct` type"); +pub fn Owned(comptime T: type) type { + // if (@typeInfo(Struct) != .@"struct") @compileError("expected a `struct` type"); return struct { arena: *std.heap.ArenaAllocator, - value: Struct, + value: T, pub fn deinit(self: @This()) void { const allocator = self.arena.child_allocator;