diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ace7a77f..e1e9feb05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## 3.3.0 +__15.03.2022__ + +- feature: Guild emoji improvements (#305) + - Added missing properties on `IBaseGuildEmoji`. + - Partial emoji can be now resolved to it's full instance with `resolve()` method + - Author of emoji can be now resolved with `fetchCreator()` +- feature: Allow editing messages to remove content (#313) +- feature: Add previous state to *UpdateEvents (#311) +- bug: fix: initialize name and format values for PartialSticker (#308) +- bug: Make IHttpResponseError subclass Exception (#303) +- bug: Update documentation (#302) + +## 3.3.0-dev.1 +__05.03.2022__ + +- feature: Guild emoji improvements (#305) + - Added missing properties on `IBaseGuildEmoji`. + - Partial emoji can be now resolved to it's full instance with `resolve()` method + - Author of emoji can be now resolved with `fetchCreator()` +- bug: Make IHttpResponseError subclass Exception (#303) +- bug: Update documentation (#302) + +## 3.3.0-dev.0 +__08.02.2022__ + +- feature: Implement TextInput component type + ## 3.2.7 __08.02.2022__ diff --git a/example/emojis.dart b/example/emojis.dart new file mode 100644 index 000000000..32034e9ec --- /dev/null +++ b/example/emojis.dart @@ -0,0 +1,43 @@ +import 'package:nyxx/nyxx.dart'; + +void main(List args) { + // Create new bot instance. Replace string with your token + final bot = NyxxFactory.createNyxxWebsocket("", GatewayIntents.allUnprivileged) + ..registerPlugin(Logging()) // Default logging plugin + ..registerPlugin(CliIntegration()) // Cli integration for nyxx allows stopping application via SIGTERM and SIGKILl + ..registerPlugin(IgnoreExceptions()) // Plugin that handles uncaught exceptions that may occur + ..connect(); + bot.eventsWs.onReady.listen((_) { + print('Ready!'); + }); + // This event is called when a message is received + bot.eventsWs.onMessageReceived.listen((event) async { + if(event.message.content == '!emoji') { + final emoji = event.message.guild?.getFromCache()?.emojis.values.firstWhere((emo) => emo.name == 'nyxx'); + final msg = await event.message.channel.sendMessage(MessageBuilder.content('Look at this emoji: $emoji')); + msg.createReaction(emoji!); + // For unicode emoji use `UnicodeEmoji` class + msg.createReaction(UnicodeEmoji('🤔')); + } + }); + + // This event is called when a reaction has been added to a message + bot.eventsWs.onMessageReactionAdded.listen((event) { + if (event.emoji is UnicodeEmoji) { + event.message?.channel.sendMessage( + MessageBuilder.content( + 'Woah! This is a unicode emoji: ${event.emoji}', + ), + ); + } else if (event.emoji is IGuildEmojiPartial) { + if(event.emoji is IResolvableGuildEmojiPartial) { + final emoji = (event.emoji as IResolvableGuildEmojiPartial).resolve(); + event.message?.channel.sendMessage( + MessageBuilder.content( + 'Woah! This is a custom emoji: ${emoji.name}', + ), + ); + } + } + }); +} diff --git a/lib/nyxx.dart b/lib/nyxx.dart index b9275bdf4..6450bb9e4 100644 --- a/lib/nyxx.dart +++ b/lib/nyxx.dart @@ -48,7 +48,7 @@ export 'src/core/guild/status.dart' show IClientStatus, UserStatus; export 'src/core/guild/webhook.dart' show IWebhook, WebhookType; export 'src/core/message/attachment.dart' show IAttachment; export 'src/core/message/emoji.dart' show IEmoji; -export 'src/core/message/guild_emoji.dart' show IBaseGuildEmoji, IGuildEmoji, IGuildEmojiPartial; +export 'src/core/message/guild_emoji.dart' show IBaseGuildEmoji, IGuildEmoji, IGuildEmojiPartial, IResolvableGuildEmojiPartial; export 'src/core/message/message.dart' show IMessage; export 'src/core/message/message_flags.dart' show MessageFlags; export 'src/core/message/message_reference.dart' show IMessageReference; @@ -58,7 +58,7 @@ export 'src/core/message/reaction.dart' show IReaction; export 'src/core/message/referenced_message.dart' show IReferencedMessage; export 'src/core/message/sticker.dart' show IStandardSticker, IStickerPack, ISticker, IGuildSticker, IPartialSticker; export 'src/core/message/unicode_emoji.dart' show IUnicodeEmoji, UnicodeEmoji; -export 'src/core/message/components/component_style.dart' show ComponentStyle; +export 'src/core/message/components/component_style.dart' show ButtonStyle; export 'src/core/message/components/message_component.dart' show IMessageButton, @@ -69,7 +69,8 @@ export 'src/core/message/components/message_component.dart' IMessageMultiselect, IMessageMultiselectOption, MessageComponentEmoji, - ComponentType; + ComponentType, + IMessageTextInput; export 'src/core/permissions/permission_overrides.dart' show IPermissionsOverrides; export 'src/core/permissions/permissions.dart' show IPermissions; export 'src/core/permissions/permissions_constants.dart' show PermissionsConstants; diff --git a/lib/src/client_options.dart b/lib/src/client_options.dart index 7cad12336..299cb3ef3 100644 --- a/lib/src/client_options.dart +++ b/lib/src/client_options.dart @@ -1,11 +1,6 @@ +import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/nyxx.dart'; -import 'package:nyxx/src/core/allowed_mentions.dart'; -import 'package:nyxx/src/core/channel/channel.dart'; -import 'package:nyxx/src/core/message/message.dart'; -import 'package:nyxx/src/core/user/member.dart'; -import 'package:nyxx/src/internal/cache/cache_policy.dart'; import 'package:nyxx/src/internal/shard/shard.dart'; -import 'package:nyxx/src/utils/builders/presence_builder.dart'; /// Options for configuring cache. Allows to specify where and which entities should be cached and preserved in cache class CacheOptions { diff --git a/lib/src/core/allowed_mentions.dart b/lib/src/core/allowed_mentions.dart index ce62f10a8..a29d7342a 100644 --- a/lib/src/core/allowed_mentions.dart +++ b/lib/src/core/allowed_mentions.dart @@ -18,6 +18,7 @@ class AllowedMentions extends Builder { /// Allow @everyone and @here if [everyone] is true /// Allow @user if [users] is true /// Allow @role if [roles] is true + /// Mention the user on reply if [reply] is true void allow({bool? reply, bool? everyone, bool? users, bool? roles}) { if (everyone != null) { _allowEveryone = everyone; diff --git a/lib/src/core/audit_logs/audit_log_entry.dart b/lib/src/core/audit_logs/audit_log_entry.dart index 00a054a4d..5225cc845 100644 --- a/lib/src/core/audit_logs/audit_log_entry.dart +++ b/lib/src/core/audit_logs/audit_log_entry.dart @@ -67,11 +67,9 @@ class AuditLogEntry extends SnowflakeEntity implements IAuditLogEntry { user = UserCacheable(client, Snowflake(raw["user_id"])); type = AuditLogEntryType._create(raw["action_type"] as int); - if (raw["options"] != null) { - options = raw["options"] as String; - } + options = raw["options"] as String?; - reason = raw["reason"] as String; + reason = raw["reason"] as String?; } } diff --git a/lib/src/core/channel/guild/guild_channel.dart b/lib/src/core/channel/guild/guild_channel.dart index 693461bf0..b18813ca8 100644 --- a/lib/src/core/channel/guild/guild_channel.dart +++ b/lib/src/core/channel/guild/guild_channel.dart @@ -35,7 +35,7 @@ abstract class IGuildChannel implements IMinimalGuildChannel { /// Fetches and returns all channel"s [Invite]s /// /// ``` - /// var invites = await chan.getChannelInvites(); + /// var invites = await chan.fetchChannelInvites(); /// ``` Stream fetchChannelInvites(); diff --git a/lib/src/core/channel/text_channel.dart b/lib/src/core/channel/text_channel.dart index 2a9f2c26f..bcd6bf7e3 100644 --- a/lib/src/core/channel/text_channel.dart +++ b/lib/src/core/channel/text_channel.dart @@ -17,16 +17,16 @@ abstract class ITextChannel implements IChannel, ISend { /// Returns [IMessage] downloaded from API Future fetchMessage(Snowflake id); - /// Sends message to channel. Performs `toString()` on thing passed to [content]. Allows to send embeds with [embed] field. + /// Sends message to channel. Allows to send embeds with [MessageBuilder.embed()] method. /// /// ``` - /// await channel.sendMessage(content: "Very nice message!"); + /// await channel.sendMessage(MessageBuilder.content("Very nice message!")); /// ``` /// /// Can be used in combination with Emoji. Just run `toString()` on Emoji instance: /// ``` /// final emoji = guild.emojis.findOne((e) => e.name.startsWith("dart")); - /// await channel.send(content: "Dart is superb! ${emoji.toString()}"); + /// await channel.sendMessage(MessageBuilder.content("Dart is superb! ${emoji.toString()}")); /// ``` /// Embeds can be sent very easily: /// ``` @@ -34,35 +34,53 @@ abstract class ITextChannel implements IChannel, ISend { /// ..title = "Example Title" /// ..addField(name: "Memory usage", value: "${ProcessInfo.currentRss / 1024 / 1024}MB"); /// - /// await channel.sendMessage(embed: embed); + /// await channel.sendMessage(MessageBuilder.embed(embed)); /// ``` /// - /// Method also allows to send file and optional [content] with [embed]. - /// Use `expandAttachment(String file)` method to expand file names in embed + /// Method also allows to send multiple files and optional [content] with [embed]. /// /// ``` - /// await channel.sendMessage(files: [new File("kitten.png"), new File("kitten.jpg")], content: "Kittens ^-^"]); + /// await event.message.channel.sendMessage( + /// MessageBuilder.files( + /// [ + /// AttachmentBuilder.file( + /// File("kitten.png"), + /// name: "kitten.png", + /// ), + /// ], + /// )..content = "Kittens ^-^", + /// ); /// ``` + /// You can refer the sent attachments in embeds by prefixing them with `attachment://`: /// ``` - /// var embed = new nyxx.EmbedBuilder() + /// var embed = EmbedBuilder() /// ..title = "Example Title" - /// ..thumbnailUrl = "${attach("kitten.jpg")}"; + /// ..thumbnailUrl = "attachment://kitten.jpg"; /// - /// channel.sendMessage(files: [new File("kitten.jpg")], embed: embed, content: "HEJKA!"); + /// await event.message.channel.sendMessage( + /// MessageBuilder.files( + /// [ + /// AttachmentBuilder.file( + /// File("kitten.jpg"), + /// ), + /// ], + /// ) + /// ..embeds = [embed] + /// ..content = "HEJKA!", + /// ); /// ``` @override Future sendMessage(MessageBuilder builder); - /// Bulk removes many messages by its ids. [messages] is list of messages ids to delete. + /// Bulk removes many referenced messages. Where [messages] is list of messages to delete. /// /// ``` - /// var toDelete = channel.messages.take(5); + /// var toDelete = channel.messageCache.take(5); /// await channel.bulkRemoveMessages(toDelete); /// ``` Future bulkRemoveMessages(Iterable messages); - /// Gets several [IMessage] objects from API. Only one of [after], [before], [around] can be specified, - /// otherwise, it will throw. + /// Gets several [IMessage] objects from API. /// /// ``` /// var messages = await channel.downloadMessages(limit: 100, after: Snowflake("222078108977594368")); diff --git a/lib/src/core/channel/thread_channel.dart b/lib/src/core/channel/thread_channel.dart index b541a388b..f858ceb14 100644 --- a/lib/src/core/channel/thread_channel.dart +++ b/lib/src/core/channel/thread_channel.dart @@ -114,6 +114,7 @@ abstract class IThreadChannel implements MinimalGuildChannel, ITextChannel { /// Adds [user] to [ThreadChannel] Future addThreadMember(SnowflakeEntity user); + /// Edits this [ThreadChannel] and returns the edited [ThreadChannel] Future edit(ThreadBuilder builder); } diff --git a/lib/src/core/embed/embed.dart b/lib/src/core/embed/embed.dart index 4a4fd9545..b4474c2f2 100644 --- a/lib/src/core/embed/embed.dart +++ b/lib/src/core/embed/embed.dart @@ -107,52 +107,60 @@ class Embed implements IEmbed { /// Creates an instance [Embed] Embed(RawApiMap raw) { - if (raw["title"] != null) { - title = raw["title"] as String; - } + title = raw["title"] as String?; - if (raw["url"] != null) { - url = raw["url"] as String; - } + url = raw["url"] as String?; - if (raw["type"] != null) { - type = raw["type"] as String; - } + type = raw["type"] as String?; - if (raw["description"] != null) { - description = raw["description"] as String; - } + description = raw["description"] as String?; if (raw["timestamp"] != null) { timestamp = DateTime.parse(raw["timestamp"] as String); + } else { + timestamp = null; } if (raw["color"] != null) { color = DiscordColor.fromInt(raw["color"] as int); + } else { + color = null; } if (raw["author"] != null) { author = EmbedAuthor(raw["author"] as RawApiMap); + } else { + author = null; } if (raw["video"] != null) { video = EmbedVideo(raw["video"] as RawApiMap); + } else { + video = null; } if (raw["image"] != null) { image = EmbedThumbnail(raw["image"] as RawApiMap); + } else { + image = null; } if (raw["footer"] != null) { footer = EmbedFooter(raw["footer"] as RawApiMap); + } else { + footer = null; } if (raw["thumbnail"] != null) { thumbnail = EmbedThumbnail(raw["thumbnail"] as RawApiMap); + } else { + thumbnail = null; } if (raw["provider"] != null) { provider = EmbedProvider(raw["provider"] as RawApiMap); + } else { + provider = null; } fields = [ diff --git a/lib/src/core/embed/embed_provider.dart b/lib/src/core/embed/embed_provider.dart index 7426d7889..b367b7e5d 100644 --- a/lib/src/core/embed/embed_provider.dart +++ b/lib/src/core/embed/embed_provider.dart @@ -20,12 +20,8 @@ class EmbedProvider implements IEmbedProvider { /// Creates an instance of [EmbedProvider] EmbedProvider(RawApiMap raw) { - if (raw["name"] != null) { - name = raw["name"] as String?; - } + name = raw["name"] as String?; - if (raw["url"] != null) { - url = raw["url"] as String?; - } + url = raw["url"] as String?; } } diff --git a/lib/src/core/guild/guild.dart b/lib/src/core/guild/guild.dart index 8ad9f387c..d8ae99762 100644 --- a/lib/src/core/guild/guild.dart +++ b/lib/src/core/guild/guild.dart @@ -183,12 +183,12 @@ abstract class IGuild implements SnowflakeEntity { /// Fetches emoji from API Future fetchEmoji(Snowflake emojiId); - /// Allows to create new guild emoji. [name] is required and you have to specify one of other parameters: [imageFile], [imageBytes] or [encodedImage]. - /// [imageBytes] can be useful if you want to create image from http response. + /// Allows to create new guild emoji. [name] is required. You can allow to set [roles] to restrict emoji usage. + /// Put your image in [emojiAttachment] field. /// /// ``` - /// var emojiFile = new File("weed.png"); - /// vare emoji = await guild.createEmoji("weed, image: emojiFile"); + /// var emojiFile = File("weed.png"); + /// var emoji = await guild.createEmoji("weed", emojiAttachment: AttachmentBuilder.file(emojiFile)); /// ``` Future createEmoji(String name, {List? roles, AttachmentBuilder? emojiAttachment}); @@ -220,7 +220,7 @@ abstract class IGuild implements SnowflakeEntity { /// https://discordapp.com/developers/docs/resources/audit-log /// /// ``` - /// var logs = await guild.getAuditLogs(actionType: 1); + /// var logs = await guild.fetchAuditLogs(actionType: 1); /// ``` Future fetchAuditLogs({Snowflake? userId, int? actionType, Snowflake? before, int? limit}); @@ -493,6 +493,7 @@ class Guild extends SnowflakeEntity implements IGuild { mfaLevel = raw["mfa_level"] as int; verificationLevel = raw["verification_level"] as int; notificationLevel = raw["default_message_notifications"] as int; + available = !(raw["unavailable"] as bool? ?? false); icon = raw["icon"] as String?; discoverySplash = raw["discoverySplash"] as String?; @@ -525,10 +526,14 @@ class Guild extends SnowflakeEntity implements IGuild { if (raw["embed_channel_id"] != null) { embedChannel = ChannelCacheable(client, Snowflake(raw["embed_channel_id"])); + } else { + embedChannel = null; } if (raw["system_channel_id"] != null) { systemChannel = ChannelCacheable(client, Snowflake(raw["system_channel_id"])); + } else { + systemChannel = null; } features = (raw["features"] as RawApiList).map((e) => GuildFeature.from(e.toString())); @@ -576,10 +581,14 @@ class Guild extends SnowflakeEntity implements IGuild { if (raw["rules_channel_id"] != null) { rulesChannel = ChannelCacheable(client, Snowflake(raw["rules_channel_id"])); + } else { + rulesChannel = null; } if (raw["public_updates_channel_id"] != null) { publicUpdatesChannel = CacheableTextChannel(client, Snowflake(raw["public_updates_channel_id"])); + } else { + publicUpdatesChannel = null; } stageInstances = [ @@ -628,12 +637,12 @@ class Guild extends SnowflakeEntity implements IGuild { @override Future fetchEmoji(Snowflake emojiId) => client.httpEndpoints.fetchGuildEmoji(id, emojiId); - /// Allows to create new guild emoji. [name] is required and you have to specify one of other parameters: [imageFile], [imageBytes] or [encodedImage]. - /// [imageBytes] can be useful if you want to create image from http response. + /// Allows to create new guild emoji. [name] is required. You can allow to set [roles] to restrict emoji usage. + /// Put your image in [emojiAttachment] field. /// /// ``` - /// var emojiFile = new File("weed.png"); - /// vare emoji = await guild.createEmoji("weed, image: emojiFile"); + /// var emojiFile = File("weed.png"); + /// var emoji = await guild.createEmoji("weed", emojiAttachment: AttachmentBuilder.file(emojiFile)); /// ``` @override Future createEmoji(String name, {List? roles, AttachmentBuilder? emojiAttachment}) => @@ -676,7 +685,7 @@ class Guild extends SnowflakeEntity implements IGuild { /// https://discordapp.com/developers/docs/resources/audit-log /// /// ``` - /// var logs = await guild.getAuditLogs(actionType: 1); + /// var logs = await guild.fetchAuditLogs(actionType: 1); /// ``` @override Future fetchAuditLogs({Snowflake? userId, int? actionType, Snowflake? before, int? limit}) => diff --git a/lib/src/core/guild/guild_preview.dart b/lib/src/core/guild/guild_preview.dart index e117fcc8d..e0e2dfb23 100644 --- a/lib/src/core/guild/guild_preview.dart +++ b/lib/src/core/guild/guild_preview.dart @@ -97,19 +97,13 @@ class GuildPreview extends SnowflakeEntity implements IGuildPreview { GuildPreview(this.client, RawApiMap raw) : super(Snowflake(raw["id"])) { name = raw["name"] as String; - if (iconHash != null) { - iconHash = raw["icon"] as String; - } + iconHash = raw["icon"] as String?; - if (splashHash != null) { - splashHash = raw["splash"] as String; - } + splashHash = raw["splash"] as String?; - if (discoveryHash != null) { - discoveryHash = raw["discovery_splash"] as String; - } + discoveryHash = raw["discovery_splash"] as String?; - emojis = [for (var rawEmoji in raw["emojis"]) GuildEmoji(client, rawEmoji as RawApiMap, id)]; + emojis = [for (final rawEmoji in raw["emojis"]) GuildEmoji(client, rawEmoji as RawApiMap, id)]; features = (raw["features"] as RawApiList).map((e) => GuildFeature.from(e.toString())); diff --git a/lib/src/core/message/components/component_style.dart b/lib/src/core/message/components/component_style.dart index 6f6ff8891..e5795eac2 100644 --- a/lib/src/core/message/components/component_style.dart +++ b/lib/src/core/message/components/component_style.dart @@ -1,23 +1,23 @@ import 'package:nyxx/src/utils/enum.dart'; /// Style for a button. -class ComponentStyle extends IEnum { +class ButtonStyle extends IEnum { /// A blurple button - static const primary = ComponentStyle._create(1); + static const primary = ButtonStyle._create(1); /// A grey button - static const secondary = ComponentStyle._create(2); + static const secondary = ButtonStyle._create(2); /// A green button - static const success = ComponentStyle._create(3); + static const success = ButtonStyle._create(3); /// A red button - static const danger = ComponentStyle._create(4); + static const danger = ButtonStyle._create(4); /// A button that navigates to a URL - static const link = ComponentStyle._create(5); + static const link = ButtonStyle._create(5); /// Creates instance of [ComponentStyle] - ComponentStyle.from(int value) : super(value); - const ComponentStyle._create(int value) : super(value); + ButtonStyle.from(int value) : super(value); + const ButtonStyle._create(int value) : super(value); } diff --git a/lib/src/core/message/components/message_component.dart b/lib/src/core/message/components/message_component.dart index d8a725770..30aebf501 100644 --- a/lib/src/core/message/components/message_component.dart +++ b/lib/src/core/message/components/message_component.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:nyxx/src/core/snowflake.dart'; +import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/core/message/emoji.dart'; import 'package:nyxx/src/core/message/guild_emoji.dart'; import 'package:nyxx/src/core/message/unicode_emoji.dart'; @@ -16,6 +16,8 @@ class ComponentType extends IEnum { static const ComponentType button = ComponentType._create(2); static const ComponentType select = ComponentType._create(3); + static const ComponentType text = ComponentType._create(4); + const ComponentType._create(int value) : super(value); /// Create [ComponentType] from [value] @@ -86,7 +88,7 @@ abstract class MessageComponent implements IMessageComponent { /// Empty constructor MessageComponent(); - factory MessageComponent.deserialize(Map raw) { + factory MessageComponent.deserialize(RawApiMap raw) { final type = raw["type"] as int; switch (type) { @@ -94,12 +96,39 @@ abstract class MessageComponent implements IMessageComponent { return MessageButton.deserialize(raw); case 3: return MessageMultiselect(raw); + case 4: + return MessageTextInput(raw); } throw ArgumentError("Unknown interaction type: [$type]: ${jsonEncode(raw)}"); } } +/// Text input component +abstract class IMessageTextInput implements IMessageComponent { + /// Custom id of components set by user + String get customId; + + /// Value of component + String get value; +} + +class MessageTextInput extends MessageComponent implements IMessageTextInput { + @override + ComponentType get type => ComponentType.text; + + @override + late final String customId; + + @override + late final String value; + + MessageTextInput(RawApiMap raw) { + customId = raw['custom_id'] as String; + value = raw['value'] as String; + } +} + abstract class IMessageMultiselectOption { /// Option label String get label; @@ -139,7 +168,7 @@ class MessageMultiselectOption implements IMessageMultiselectOption { late final bool isDefault; /// Creates an instance of [MessageMultiselectOption] - MessageMultiselectOption(Map raw) { + MessageMultiselectOption(RawApiMap raw) { label = raw["label"] as String; value = raw["value"] as String; @@ -195,7 +224,7 @@ class MessageMultiselect extends MessageComponent implements IMessageMultiselect late final Iterable options; /// Creates an instance of [MessageMultiselect] - MessageMultiselect(Map raw) : super() { + MessageMultiselect(RawApiMap raw) : super() { customId = raw["custom_id"] as String; placeholder = raw["placeholder"] as String?; minValues = raw["min_values"] as int? ?? 1; @@ -209,7 +238,7 @@ abstract class IMessageButton implements IMessageComponent { String? get label; /// Component style, appearance - ComponentStyle get style; + ButtonStyle get style; /// Additional emoji that will be displayed before label IMessageComponentEmoji? get emoji; @@ -229,7 +258,7 @@ class MessageButton extends MessageComponent implements IMessageButton { /// Component style, appearance @override - late final ComponentStyle style; + late final ButtonStyle style; /// Additional emoji that will be displayed before label @override @@ -240,7 +269,7 @@ class MessageButton extends MessageComponent implements IMessageButton { late final bool disabled; factory MessageButton.deserialize(RawApiMap raw) { - if (raw["style"] == ComponentStyle.link.value) { + if (raw["style"] == ButtonStyle.link.value) { return LinkMessageButton(raw); } @@ -248,9 +277,9 @@ class MessageButton extends MessageComponent implements IMessageButton { } /// Creates an instance of [MessageButton] - MessageButton(Map raw) : super() { + MessageButton(RawApiMap raw) : super() { label = raw["label"] as String?; - style = ComponentStyle.from(raw["style"] as int); + style = ButtonStyle.from(raw["style"] as int); if (raw["emoji"] != null) { emoji = MessageComponentEmoji(raw["emoji"] as RawApiMap); diff --git a/lib/src/core/message/guild_emoji.dart b/lib/src/core/message/guild_emoji.dart index 66d834d24..e095b4033 100644 --- a/lib/src/core/message/guild_emoji.dart +++ b/lib/src/core/message/guild_emoji.dart @@ -1,21 +1,21 @@ -import 'package:nyxx/src/nyxx.dart'; -import 'package:nyxx/src/core/snowflake.dart'; -import 'package:nyxx/src/core/snowflake_entity.dart'; -import 'package:nyxx/src/core/guild/guild.dart'; -import 'package:nyxx/src/core/guild/role.dart'; -import 'package:nyxx/src/core/message/emoji.dart'; +import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/internal/cache/cacheable.dart'; -import 'package:nyxx/src/typedefs.dart'; abstract class IBaseGuildEmoji implements SnowflakeEntity, IEmoji { /// True if emoji is partial. bool get isPartial; - /// Returns cdn url to emoji - String get cdnUrl; + /// The name of the emoji. + String get name; + + /// Whether this emoji is animated. + bool get animated; /// Creates partial emoji from given String or Snowflake. - factory IBaseGuildEmoji.fromId(Snowflake id) => GuildEmojiPartial(id); + factory IBaseGuildEmoji.fromId(Snowflake id) => GuildEmojiPartial({"id": id.toString()}); + + /// Returns cdn url to emoji + String cdnUrl({String? format, int? size}); } abstract class BaseGuildEmoji extends SnowflakeEntity implements IBaseGuildEmoji { @@ -23,18 +23,44 @@ abstract class BaseGuildEmoji extends SnowflakeEntity implements IBaseGuildEmoji @override bool get isPartial; - /// Returns cdn url to emoji + /// Whether this emoji is animated. @override - String get cdnUrl => "https://cdn.discordapp.com/emojis/$id.png"; + bool get animated; + + /// The name of the emoji. + @override + String get name; /// Creates an instance of [BaseGuildEmoji] BaseGuildEmoji(RawApiMap raw) : super(Snowflake(raw["id"])); + /// Returns cdn url to emoji + @override + String cdnUrl({String? format, int? size}) { + var url = "${Constants.cdnUrl}/emojis/$id."; + + if (format == null) { + if (animated) { + url += "gif"; + } else { + url += "webp"; + } + } else { + url += format; + } + + if (size != null) { + url += "?size=$size"; + } + + return url; + } + @override - String formatForMessage() => "<:nyxx:$id>"; + String formatForMessage() => "<${animated ? 'a' : ''}:$name:$id>"; @override - String encodeForAPI() => id.toString(); + String encodeForAPI() => '$name:$id'; /// Returns encoded string ready to send via message. @override @@ -43,16 +69,64 @@ abstract class BaseGuildEmoji extends SnowflakeEntity implements IBaseGuildEmoji abstract class IGuildEmojiPartial implements IBaseGuildEmoji {} +abstract class IResolvableGuildEmojiPartial implements IGuildEmojiPartial { + /// Reference to [INyxx] + INyxx get client; + + /// Resolves this [IResolvableGuildEmojiPartial] to [IGuildEmoji] + IGuildEmoji resolve(); +} + class GuildEmojiPartial extends BaseGuildEmoji implements IGuildEmojiPartial { + /// True if emoji is partial. @override bool get isPartial => true; + /// The name of the emoji. + @override + late final String name; + + /// Whether this emoji is animated. + @override + late final bool animated; + /// Creates an instance of [GuildEmojiPartial] - GuildEmojiPartial(Snowflake id) : super({"id": id.toString()}); + GuildEmojiPartial(RawApiMap raw) : super({"id": raw["id"].toString()}) { + name = raw["name"] as String? ?? "nyxx"; + animated = raw["animated"] as bool? ?? false; + } +} + +class ResolvableGuildEmojiPartial extends BaseGuildEmoji implements IResolvableGuildEmojiPartial { + /// Whether this emoji is animated. + @override + late final bool animated; + + /// Reference to [INyxx] + @override + final INyxx client; + + /// Whether this emoji is partial. + @override + bool get isPartial => true; + + /// The name of the emoji. + @override + late final String name; + + /// Creates an instance of [ResolvableGuildEmojiPartial] + ResolvableGuildEmojiPartial(RawApiMap raw, this.client) : super(raw) { + name = raw["name"] as String? ?? "nyxx"; + animated = raw["animated"] as bool? ?? false; + } + + /// Resolves this [IResolvableGuildEmojiPartial] to [IGuildEmoji] + @override + IGuildEmoji resolve() => client.guilds.values.expand((guild) => guild.emojis.values).firstWhere((emoji) => emoji.id == id) as IGuildEmoji; } abstract class IGuildEmoji implements IBaseGuildEmoji { - /// Reference to client + /// Reference to [INyxx] INyxx get client; /// Reference to guild where emoji belongs to @@ -67,8 +141,8 @@ abstract class IGuildEmoji implements IBaseGuildEmoji { /// whether this emoji is managed bool get managed; - /// whether this emoji is animated - bool get animated; + /// Fetches the creator of this emoji + Future fetchCreator(); /// Allows to delete guild emoji Future delete(); @@ -78,7 +152,7 @@ abstract class IGuildEmoji implements IBaseGuildEmoji { } class GuildEmoji extends BaseGuildEmoji implements IGuildEmoji { - /// Reference to client + /// Reference to [INyxx] @override final INyxx client; @@ -102,6 +176,11 @@ class GuildEmoji extends BaseGuildEmoji implements IGuildEmoji { @override late final bool animated; + /// The name of the emoji. + @override + late final String name; + + /// True if emoji is partial. @override bool get isPartial => false; @@ -109,12 +188,21 @@ class GuildEmoji extends BaseGuildEmoji implements IGuildEmoji { GuildEmoji(this.client, RawApiMap raw, Snowflake guildId) : super(raw) { guild = GuildCacheable(client, guildId); + name = raw["name"] as String; requireColons = raw["require_colons"] as bool? ?? false; managed = raw["managed"] as bool? ?? false; animated = raw["animated"] as bool? ?? false; roles = [for (final roleId in raw["roles"]) RoleCacheable(client, Snowflake(roleId), guild)]; } + /// Returns encoded emoji for usage in message + @override + String formatForMessage() => "<${animated ? 'a' : ''}:$name:$id>"; + + /// Fetches the creator of this emoji + @override + Future fetchCreator() => client.httpEndpoints.fetchEmojiCreator(guild.id, id); + /// Allows to delete guild emoji @override Future delete() => client.httpEndpoints.deleteGuildEmoji(guild.id, id); diff --git a/lib/src/core/message/message.dart b/lib/src/core/message/message.dart index fbed4968e..ef01acb08 100644 --- a/lib/src/core/message/message.dart +++ b/lib/src/core/message/message.dart @@ -295,7 +295,7 @@ class Message extends SnowflakeEntity implements IMessage { reactions = [ if (raw["reactions"] != null && raw["reactions"].isNotEmpty as bool) - for (var r in raw["reactions"]) Reaction(r as RawApiMap) + for (var r in raw["reactions"]) Reaction(r as RawApiMap, client) ]; if (raw["mentions"] != null && raw["mentions"].isNotEmpty as bool) { @@ -366,6 +366,32 @@ class Message extends SnowflakeEntity implements IMessage { ]; } + Message.copy(Message other) + : client = other.client, + super(other.id) { + author = other.author; + content = other.content; + channel = other.channel; + editedTimestamp = other.editedTimestamp; + mentions = other.mentions; + embeds = other.embeds; + attachments = other.attachments; + pinned = other.pinned; + tts = other.tts; + mentionEveryone = other.mentionEveryone; + reactions = other.reactions; + type = other.type; + flags = other.flags; + partialStickers = other.partialStickers; + referencedMessage = other.referencedMessage; + components = other.components; + nonce = other.nonce; + applicationId = other.applicationId; + crossPostReference = other.crossPostReference; + member = other.member; + roleMentions = other.roleMentions; + } + /// Suppresses embeds in message. Can be executed in other users messages. @override Future suppressEmbeds() => client.httpEndpoints.suppressMessageEmbeds(channel.id, id); diff --git a/lib/src/core/message/reaction.dart b/lib/src/core/message/reaction.dart index a7386cf9a..c7488e910 100644 --- a/lib/src/core/message/reaction.dart +++ b/lib/src/core/message/reaction.dart @@ -1,6 +1,5 @@ -import 'package:nyxx/src/core/message/emoji.dart'; -import 'package:nyxx/src/core/message/unicode_emoji.dart'; -import 'package:nyxx/src/typedefs.dart'; +import 'package:nyxx/nyxx.dart'; +import 'package:nyxx/src/core/message/guild_emoji.dart'; abstract class IReaction { /// Time this emoji has been used to react @@ -28,7 +27,7 @@ class Reaction implements IReaction { late final IEmoji emoji; /// Creates an instance of [Reaction] - Reaction(RawApiMap raw) { + Reaction(RawApiMap raw, INyxx client) { count = raw["count"] as int; me = raw["me"] as bool; @@ -36,8 +35,7 @@ class Reaction implements IReaction { if (rawEmoji["id"] == null) { emoji = UnicodeEmoji(rawEmoji["name"] as String); } else { - //TODO: EMOJIS STUUF - //this.emoji = PartialGuildEmoji._new(rawEmoji); + emoji = ResolvableGuildEmojiPartial(rawEmoji, client); } } diff --git a/lib/src/core/message/sticker.dart b/lib/src/core/message/sticker.dart index 40f273de9..67e7be20c 100644 --- a/lib/src/core/message/sticker.dart +++ b/lib/src/core/message/sticker.dart @@ -1,12 +1,12 @@ -import 'package:nyxx/src/nyxx.dart'; +import 'package:nyxx/src/core/guild/guild.dart'; import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/core/snowflake_entity.dart'; -import 'package:nyxx/src/core/guild/guild.dart'; import 'package:nyxx/src/core/user/user.dart'; import 'package:nyxx/src/internal/cache/cacheable.dart'; +import 'package:nyxx/src/nyxx.dart'; import 'package:nyxx/src/typedefs.dart'; -import 'package:nyxx/src/utils/enum.dart'; import 'package:nyxx/src/utils/builders/sticker_builder.dart'; +import 'package:nyxx/src/utils/enum.dart'; /// Base interface for all sticker types abstract class ISticker implements SnowflakeEntity { @@ -80,7 +80,10 @@ class PartialSticker extends Sticker implements IPartialSticker { StickerType get type => StickerType.partial; /// Creates an instance of [PartialSticker] - PartialSticker(RawApiMap raw, INyxx client) : super(raw, client); + PartialSticker(RawApiMap raw, INyxx client) : super(raw, client) { + name = raw["name"] as String; + format = StickerFormat.from(raw["format_type"] as int); + } } abstract class IGuildSticker implements ISticker { diff --git a/lib/src/core/user/member.dart b/lib/src/core/user/member.dart index e3c217d80..d73e70474 100644 --- a/lib/src/core/user/member.dart +++ b/lib/src/core/user/member.dart @@ -131,6 +131,7 @@ class Member extends SnowflakeEntity implements IMember { /// Highest role of member @override + @Deprecated('Use `roles` and sort by position instead. Attempting to use this field will throw an error.') late final Cacheable hoistedRole; /// When the user starting boosting the guild @@ -191,13 +192,7 @@ class Member extends SnowflakeEntity implements IMember { roles = [for (var id in raw["roles"]) RoleCacheable(client, Snowflake(id), guild)]; - if (raw["hoisted_role"] != null) { - hoistedRole = RoleCacheable(client, Snowflake(raw["hoisted_role"]), guild); - } - - if (raw["joined_at"] != null) { - joinedAt = DateTime.parse(raw["joined_at"] as String).toUtc(); - } + joinedAt = DateTime.parse(raw["joined_at"] as String).toUtc(); if (client.cacheOptions.userCachePolicyLocation.objectConstructor) { final userRaw = raw["user"] as RawApiMap; diff --git a/lib/src/events/channel_events.dart b/lib/src/events/channel_events.dart index 6554178f6..9bf701116 100644 --- a/lib/src/events/channel_events.dart +++ b/lib/src/events/channel_events.dart @@ -78,6 +78,8 @@ class ChannelPinsUpdateEvent implements IChannelPinsUpdateEvent { ChannelPinsUpdateEvent(RawApiMap raw, INyxx client) { if (raw["d"]["last_pin_timestamp"] != null) { lastPingTimestamp = DateTime.parse(raw["d"]["last_pin_timestamp"] as String); + } else { + lastPingTimestamp = null; } channel = CacheableTextChannel(client, Snowflake(raw["d"]["channel_id"])); @@ -93,6 +95,9 @@ class ChannelPinsUpdateEvent implements IChannelPinsUpdateEvent { abstract class IChannelUpdateEvent { /// The channel after the update. IChannel get updatedChannel; + + /// The channel before the update, if it was cached. + IChannel? get oldChannel; } /// Sent when a channel is updated. @@ -101,15 +106,18 @@ class ChannelUpdateEvent implements IChannelUpdateEvent { @override late final IChannel updatedChannel; + @override + late final IChannel? oldChannel; + /// Creates na instance of [ChannelUpdateEvent] ChannelUpdateEvent(RawApiMap raw, INyxx client) { updatedChannel = Channel.deserialize(client, raw["d"] as RawApiMap); - final oldChannel = client.channels[updatedChannel.id]; + oldChannel = client.channels[updatedChannel.id]; // Move messages to new channel if (updatedChannel is ITextChannel && oldChannel is ITextChannel) { - (updatedChannel as ITextChannel).messageCache.addAll(oldChannel.messageCache); + (updatedChannel as ITextChannel).messageCache.addAll((oldChannel as ITextChannel).messageCache); } client.channels[updatedChannel.id] = updatedChannel; diff --git a/lib/src/events/guild_events.dart b/lib/src/events/guild_events.dart index f4984e16a..f5155795b 100644 --- a/lib/src/events/guild_events.dart +++ b/lib/src/events/guild_events.dart @@ -31,6 +31,9 @@ class GuildCreateEvent implements IGuildCreateEvent { abstract class IGuildUpdateEvent { /// The guild after the update. IGuild get guild; + + /// The guild before the update, if it was cached. + IGuild? get oldGuild; } /// Sent when a guild is updated. @@ -39,13 +42,16 @@ class GuildUpdateEvent implements IGuildUpdateEvent { @override late final IGuild guild; + @override + late final IGuild? oldGuild; + /// Creates na instance of [GuildUpdateEvent] GuildUpdateEvent(RawApiMap json, INyxx client) { guild = Guild(client, json["d"] as RawApiMap); - final oldGuild = client.guilds[guild.id]; + oldGuild = client.guilds[guild.id]; if (oldGuild != null) { - guild.members.addAll(oldGuild.members); + guild.members.addAll(oldGuild!.members); } client.guilds[guild.id] = guild; @@ -115,9 +121,12 @@ abstract class IGuildMemberUpdateEvent { /// The member after the update if member is updated. Cacheable get member; - /// User if user is updated. Will be null if member is not null. + /// The user of the updated member. IUser get user; + /// The user of the member before it was updated, if it was cached. + IUser? get oldUser; + /// Guild in which member is Cacheable get guild; } @@ -132,6 +141,9 @@ class GuildMemberUpdateEvent implements IGuildMemberUpdateEvent { @override late final IUser user; + @override + late final IUser? oldUser; + /// Guild in which member is @override late final Cacheable guild; @@ -140,8 +152,10 @@ class GuildMemberUpdateEvent implements IGuildMemberUpdateEvent { GuildMemberUpdateEvent(RawApiMap raw, INyxx client) { guild = GuildCacheable(client, Snowflake(raw["d"]["guild_id"])); member = MemberCacheable(client, Snowflake(raw["d"]["user"]["id"]), guild); + user = User(client, raw["d"]["user"] as RawApiMap); + + oldUser = client.users[user.id]; - final user = User(client, raw["d"]["user"] as RawApiMap); if (client.cacheOptions.userCachePolicyLocation.event) { client.users[user.id] = user; } @@ -362,6 +376,9 @@ abstract class IRoleUpdateEvent { /// The role after the update. IRole get role; + /// The role before it was updated, if it was cached. + IRole? get oldRole; + /// The guild that the member was banned from. Cacheable get guild; } @@ -372,6 +389,9 @@ class RoleUpdateEvent implements IRoleUpdateEvent { @override late final IRole role; + @override + late final IRole? oldRole; + /// The guild that the member was banned from. @override late final Cacheable guild; @@ -383,6 +403,7 @@ class RoleUpdateEvent implements IRoleUpdateEvent { final guildInstance = guild.getFromCache(); if (guildInstance != null) { + oldRole = guildInstance.roles[role.id]; guildInstance.roles[role.id] = role; } } diff --git a/lib/src/events/message_events.dart b/lib/src/events/message_events.dart index f0c85c25c..76aa3bc81 100644 --- a/lib/src/events/message_events.dart +++ b/lib/src/events/message_events.dart @@ -194,7 +194,7 @@ abstract class MessageReactionEvent { if (json["d"]["emoji"]["id"] == null) { emoji = UnicodeEmoji(json["d"]["emoji"]["name"] as String); } else { - emoji = GuildEmojiPartial(Snowflake(json["d"]["emoji"]['id'])); + emoji = ResolvableGuildEmojiPartial(json["d"]["emoji"] as RawApiMap, client); } } } @@ -320,7 +320,7 @@ class MessageReactionRemoveEmojiEvent implements IMessageReactionRemoveEmojiEven if (json["d"]["emoji"]["id"] == null) { emoji = UnicodeEmoji(json["d"]["emoji"]["name"] as String); } else { - emoji = GuildEmojiPartial(Snowflake(json["d"]["emoji"]['id'])); + emoji = ResolvableGuildEmojiPartial(json["d"]["emoji"] as RawApiMap, client); } final messageInstance = message.getFromCache(); @@ -334,6 +334,9 @@ abstract class IMessageUpdateEvent { /// Edited message with updated fields IMessage? get updatedMessage; + /// The message before it was updated, if it was cached. + IMessage? get oldMessage; + /// Id of channel where message was edited CacheableTextChannel get channel; @@ -347,6 +350,9 @@ class MessageUpdateEvent implements IMessageUpdateEvent { @override late final IMessage? updatedMessage; + @override + late final IMessage? oldMessage; + /// Id of channel where message was edited @override late final CacheableTextChannel channel; @@ -365,11 +371,14 @@ class MessageUpdateEvent implements IMessageUpdateEvent { return; } - updatedMessage = channelInstance.messageCache[messageId]; - if (updatedMessage == null) { + oldMessage = channelInstance.messageCache[messageId]; + + if (oldMessage == null) { return; } + updatedMessage = Message.copy(oldMessage as Message); + if (raw["d"]["content"] != updatedMessage!.content) { (updatedMessage! as Message).content = raw["d"]["content"].toString(); } @@ -399,5 +408,7 @@ class MessageUpdateEvent implements IMessageUpdateEvent { for (final rawRow in raw['d']["components"]) [for (final componentRaw in rawRow["components"]) MessageComponent.deserialize(componentRaw as RawApiMap)] ]; } + + channelInstance.messageCache[messageId] = updatedMessage!; } } diff --git a/lib/src/events/voice_state_update_event.dart b/lib/src/events/voice_state_update_event.dart index dd62bb6a6..f4c768034 100644 --- a/lib/src/events/voice_state_update_event.dart +++ b/lib/src/events/voice_state_update_event.dart @@ -6,6 +6,9 @@ abstract class IVoiceStateUpdateEvent { /// Used to represent a user's voice connection status. IVoiceState get state; + /// The previous voice state, if it was cached. + IVoiceState? get oldState; + /// Raw gateway response RawApiMap get raw; } @@ -16,6 +19,9 @@ class VoiceStateUpdateEvent implements IVoiceStateUpdateEvent { @override late final IVoiceState state; + @override + late final IVoiceState? oldState; + /// Raw gateway response @override final RawApiMap raw; @@ -24,6 +30,8 @@ class VoiceStateUpdateEvent implements IVoiceStateUpdateEvent { VoiceStateUpdateEvent(this.raw, INyxx client) { state = VoiceState(client, raw["d"] as RawApiMap); + oldState = state.guild?.getFromCache()?.voiceStates[state.user.id]; + if (state.channel != null) { state.guild?.getFromCache()?.voiceStates[state.user.id] = state; } else { diff --git a/lib/src/internal/cache/cache_policy.dart b/lib/src/internal/cache/cache_policy.dart index 69ce4bb27..bd75f5725 100644 --- a/lib/src/internal/cache/cache_policy.dart +++ b/lib/src/internal/cache/cache_policy.dart @@ -1,10 +1,4 @@ -import 'package:nyxx/src/core/snowflake_entity.dart'; -import 'package:nyxx/src/core/channel/channel.dart'; -import 'package:nyxx/src/core/channel/text_channel.dart'; -import 'package:nyxx/src/core/channel/thread_channel.dart'; -import 'package:nyxx/src/core/channel/guild/voice_channel.dart'; -import 'package:nyxx/src/core/message/message.dart'; -import 'package:nyxx/src/core/user/member.dart'; +import 'package:nyxx/nyxx.dart'; /// Predicate which will decide if entity could be cached typedef CachePolicyPredicate = bool Function(T); @@ -21,7 +15,7 @@ class CachePolicyLocation { bool other = false; /// Allows entities downloaded from http api to be cached - bool http = false; + bool http = true; /// Default options. /// [event] and [http] will be enabled by default @@ -129,6 +123,6 @@ class MessageCachePolicy extends CachePolicy { /// Default policy is [all] static final CachePolicy def = all; - /// Constructor0 + /// Constructor MessageCachePolicy(CachePolicyPredicate predicate) : super(predicate); } diff --git a/lib/src/internal/cache/cacheable.dart b/lib/src/internal/cache/cacheable.dart index c5d9dc140..14b5fa6e6 100644 --- a/lib/src/internal/cache/cacheable.dart +++ b/lib/src/internal/cache/cacheable.dart @@ -108,7 +108,7 @@ class GuildCacheable extends Cacheable { } class UserCacheable extends Cacheable { - /// Creates an instance of [ChannelCacheable] + /// Creates an instance of [UserCacheable] UserCacheable(INyxx client, Snowflake id) : super(client, id); @override diff --git a/lib/src/internal/constants.dart b/lib/src/internal/constants.dart index b779ba105..d19a55da6 100644 --- a/lib/src/internal/constants.dart +++ b/lib/src/internal/constants.dart @@ -33,7 +33,7 @@ class Constants { static const int apiVersion = 9; /// Version of Nyxx - static const String version = "3.2.7"; + static const String version = "3.3.0"; /// Url to Nyxx repo static const String repoUrl = "https://github.com/nyxx-discord/nyxx"; diff --git a/lib/src/internal/http/http_response.dart b/lib/src/internal/http/http_response.dart index 90de1c162..00dbdc529 100644 --- a/lib/src/internal/http/http_response.dart +++ b/lib/src/internal/http/http_response.dart @@ -61,7 +61,7 @@ class HttpResponseSuccess extends HttpResponse implements IHttpResponseSucess { HttpResponseSuccess(http.StreamedResponse response) : super(response); } -abstract class IHttpResponseError implements IHttpResponse, Error { +abstract class IHttpResponseError implements IHttpResponse, Error, Exception { /// Message why http request failed String get errorMessage; diff --git a/lib/src/internal/http_endpoints.dart b/lib/src/internal/http_endpoints.dart index d008ffb9c..367f20b9d 100644 --- a/lib/src/internal/http_endpoints.dart +++ b/lib/src/internal/http_endpoints.dart @@ -92,6 +92,9 @@ abstract class IHttpEndpoints { /// Creates emoji in given guild Future createEmoji(Snowflake guildId, String name, {List? roles, AttachmentBuilder? emojiAttachment}); + /// Fetches a [IUser] that created the emoji from the given [emojiId] + Future fetchEmojiCreator(Snowflake guildId, Snowflake emojiId); + /// Returns how many user will be pruned in prune operation Future guildPruneCount(Snowflake guildId, int days, {Iterable? includeRoles}); @@ -470,7 +473,7 @@ class HttpEndpoints implements IHttpEndpoints { final body = { if (name != null) "name": name, - if (roles != null) "roles": roles.map((r) => r.toString()), + if (roles != null) "roles": roles.map((r) => r.toString()).toList(), if (avatarAttachment != null) "avatar": avatarAttachment.getBase64() }; @@ -543,7 +546,7 @@ class HttpEndpoints implements IHttpEndpoints { Future createEmoji(Snowflake guildId, String name, {List? roles, AttachmentBuilder? emojiAttachment}) async { final body = { "name": name, - if (roles != null) "roles": roles.map((r) => r.id.toString()), + if (roles != null) "roles": roles.map((r) => r.id.toString()).toList(), if (emojiAttachment != null) "image": emojiAttachment.getBase64() }; @@ -556,6 +559,25 @@ class HttpEndpoints implements IHttpEndpoints { return Future.error(response); } + @override + Future fetchEmojiCreator(Snowflake guildId, Snowflake emojiId) async { + final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/emojis/$emojiId")); + + if (response is HttpResponseSuccess) { + if (response.jsonBody["managed"] as bool) { + return Future.error(ArgumentError("Emoji is managed")); + } + + if (response.jsonBody["user"] == null) { + return Future.error(ArgumentError("Could not find user creator, make sure you have the correct permissions")); + } + + return User(client, response.jsonBody["user"] as RawApiMap); + } + + return Future.error(response); + } + @override Future guildPruneCount(Snowflake guildId, int days, {Iterable? includeRoles}) async { final response = await httpHandler.execute(BasicRequest("/guilds/$guildId/prune", @@ -1103,6 +1125,10 @@ class HttpEndpoints implements IHttpEndpoints { @override Future editMessage(Snowflake channelId, Snowflake messageId, MessageBuilder builder) async { + if (!builder.canBeUsedAsNewMessage()) { + return Future.error(ArgumentError("Cannot edit a message to have neither content nor embeds")); + } + HttpResponse response; if (builder.hasFiles()) { response = await httpHandler.execute(MultipartRequest("/channels/$channelId/messages/$messageId", builder.getMappedFiles().toList(), diff --git a/lib/src/nyxx.dart b/lib/src/nyxx.dart index 6594fd119..2fe108452 100644 --- a/lib/src/nyxx.dart +++ b/lib/src/nyxx.dart @@ -4,6 +4,7 @@ import 'package:logging/logging.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx/src/client_options.dart'; import 'package:nyxx/src/core/channel/invite.dart'; +import 'package:nyxx/src/core/message/guild_emoji.dart'; import 'package:nyxx/src/core/snowflake.dart'; import 'package:nyxx/src/core/application/client_oauth2_application.dart'; import 'package:nyxx/src/core/channel/channel.dart'; @@ -252,14 +253,14 @@ abstract class INyxxWebsocket implements INyxxRest { /// Returns channel with specified id. /// ``` - /// var channel = await client.getChannel(Snowflake("473853847115137024")); + /// var channel = await client.fetchChannel(Snowflake("473853847115137024")); /// ``` Future fetchChannel(Snowflake channelId); /// Get user instance with specified id. + /// ```dart + /// var user = await client.fetchUser(Snowflake("302359032612651009")); /// ``` - /// var user = client.getUser(Snowflake("302359032612651009")); - /// `` Future fetchUser(Snowflake userId); /// Gets a webhook by its id and/or token. @@ -281,16 +282,23 @@ abstract class INyxxWebsocket implements INyxxRest { /// /// Code below will display bot presence as `Playing Super duper game`: /// ```dart - /// bot.setPresence(game: Activity.of("Super duper game")) + /// bot.setPresence( + /// PresenceBuilder.of( + /// activity: ActivityBuilder.game("Super duper game"), + /// ), + /// ); /// ``` /// /// Bots cannot set custom status - only game, listening and stream available. /// /// To set bot presence to streaming use: /// ```dart - /// bot.setPresence(game: Activity.of("Super duper game", type: ActivityType.streaming, url: "https://twitch.tv/l7ssha")) + /// bot.setPresence( + /// PresenceBuilder.of( + /// activity: ActivityBuilder.streaming("Super duper game", "https://twitch.tv/l7ssha"), + /// ), + /// ); /// ``` - /// `url` property in `Activity` can be only set when type is set to `streaming` void setPresence(PresenceBuilder presenceBuilder); /// Join [ThreadChannel] with given [channelId] @@ -369,7 +377,7 @@ class NyxxWebsocket extends NyxxRest implements INyxxWebsocket { /// Returns channel with specified id. /// ``` - /// var channel = await client.getChannel(Snowflake("473853847115137024")); + /// var channel = await client.fetchChannel(Snowflake("473853847115137024")); /// ``` @override Future fetchChannel(Snowflake channelId) => httpEndpoints.fetchChannel(channelId); diff --git a/lib/src/utils/builders/embed_builder.dart b/lib/src/utils/builders/embed_builder.dart index c693e1d93..0dde7f1b7 100644 --- a/lib/src/utils/builders/embed_builder.dart +++ b/lib/src/utils/builders/embed_builder.dart @@ -39,7 +39,7 @@ class EmbedBuilder extends Builder { EmbedAuthorBuilder? author; /// Embed custom fields; - late final List fields; + late List fields; /// Creates clean instance [EmbedBuilder] EmbedBuilder() { diff --git a/lib/src/utils/builders/message_builder.dart b/lib/src/utils/builders/message_builder.dart index 95b7ef859..039b99d85 100644 --- a/lib/src/utils/builders/message_builder.dart +++ b/lib/src/utils/builders/message_builder.dart @@ -152,13 +152,13 @@ class MessageBuilder { Future send(ISend entity) => entity.sendMessage(this); /// Returns if this instance of message builder can be used when editing message - bool canBeUsedAsNewMessage() => content.isNotEmpty || embeds != null || (files != null && files!.isNotEmpty); + bool canBeUsedAsNewMessage() => content.isNotEmpty || (embeds != null && embeds!.isNotEmpty) || (files != null && files!.isNotEmpty); RawApiMap build([AllowedMentions? defaultAllowedMentions]) { allowedMentions ??= defaultAllowedMentions; return { - if (content.isNotEmpty) "content": content.toString(), + "content": content.toString(), if (embeds != null) "embeds": [for (final e in embeds!) e.build()], if (allowedMentions != null) "allowed_mentions": allowedMentions!.build(), if (replyBuilder != null) "message_reference": replyBuilder!.build(), @@ -193,10 +193,10 @@ class MessageDecoration extends IEnum { /// Strike text is surrounded with `~~` static const MessageDecoration strike = MessageDecoration._new("~~"); - /// Inline code text is surrounded with ``` + /// Inline code text is surrounded with `` ` `` static const MessageDecoration codeSimple = MessageDecoration._new("`"); - /// Multiline code block is surrounded with ````` + /// Multiline code block is surrounded with `` ``` `` static const MessageDecoration codeLong = MessageDecoration._new("```"); /// Underlined text is surrounded with `__` diff --git a/pubspec.yaml b/pubspec.yaml index 06dffb8da..8a502828a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: nyxx -version: 3.2.7 +version: 3.3.0 description: A Discord library for Dart. Simple, robust framework for creating discord bots for Dart language. homepage: https://github.com/nyxx-discord/nyxx repository: https://github.com/nyxx-discord/nyxx diff --git a/test/integration/integration.dart b/test/integration/integration.dart index c8d29de9c..44785659c 100644 --- a/test/integration/integration.dart +++ b/test/integration/integration.dart @@ -57,7 +57,7 @@ main() async { expect(guildPreview.discoveryURL(), isNull); expect(guildPreview.splashURL(), isNull); - expect(guildPreview.iconURL(), isNull); + expect(guildPreview.iconURL(), isNotNull); }); test("basic message functionality", () async { @@ -123,11 +123,9 @@ main() async { expect(message.attachments, hasLength(2)); - final editedMessage = await message.edit( - MessageBuilder() - ..attachments = [message.attachments.first.toBuilder()] - ..files = [AttachmentBuilder.path('test/files/3.png')] - ); + final editedMessage = await message.edit(MessageBuilder() + ..attachments = [message.attachments.first.toBuilder()] + ..files = [AttachmentBuilder.path('test/files/3.png')]); expect(editedMessage.attachments, hasLength(2)); diff --git a/test/unit/builders_test.dart b/test/unit/builders_test.dart index 5d1c18469..de91483c2 100644 --- a/test/unit/builders_test.dart +++ b/test/unit/builders_test.dart @@ -162,6 +162,7 @@ main() { expect( result, equals({ + 'content': '', 'embeds': [ {'description': 'test1'}, {'description': 'test2'}