Merge pull request #76 from l7ssha/rewrite_modular_nocache

Dont require cache when possible
This commit is contained in:
Szymon Uglis 2020-07-26 15:41:16 +02:00 committed by GitHub
commit 3efad683c8
106 changed files with 3186 additions and 3949 deletions

3
.gitignore vendored
View file

@ -30,4 +30,5 @@ pubspec.lock
/test/mirrors.dart
/private
private-*.dart
[Rr]pc*
[Rr]pc*
**/doc/api/**

View file

@ -1,840 +0,0 @@
Index: nyxx/lib/src/core/guild/Guild.dart
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- nyxx/lib/src/core/guild/Guild.dart (revision c17657e7566a5f411077c568f61ea18dcb1bdb5a)
+++ nyxx/lib/src/core/guild/Guild.dart (date 1588628776000)
@@ -1,295 +1,16 @@
part of nyxx;
-/// [Guild] object represents single `Discord Server`.
-/// Guilds are a collection of members, channels, and roles that represents one community.
-///
-/// ---------
-///
-/// [channels] property is Map of [Channel]s but it can be cast to specific Channel subclasses. Example with getting all [TextChannel]s in [Guild]:
-/// ```
-/// var textChannels = channels.where((channel) => channel is MessageChannel) as List<TextChannel>;
-/// ```
-/// If you want to get [icon] or [splash] of [Guild] use `iconURL()` method - [icon] property returns only hash, same as [splash] property.
-class Guild extends SnowflakeEntity implements Disposable {
+class DataGuild extends SnowflakeEntity {
Nyxx client;
- /// The guild's name.
- late final String name;
-
- /// The guild's icon hash.
- late String? icon;
-
- /// Splash hash
- late String? splash;
-
- /// Discovery splash hash
- late String? discoverySplash;
-
- /// System channel where system messages are sent
- late final TextChannel? systemChannel;
-
- /// enabled guild features
- late final List<String> features;
-
- /// The guild's afk channel ID, null if not set.
- late VoiceChannel? afkChannel;
-
- /// The guild's voice region.
- late String region;
-
- /// The channel ID for the guild's widget if enabled.
- late final GuildChannel? embedChannel;
-
- /// The guild's AFK timeout.
- late final int afkTimeout;
-
- /// The guild's verification level.
- late final int verificationLevel;
-
- /// The guild's notification level.
- late final int notificationLevel;
-
- /// The guild's MFA level.
- late final int mfaLevel;
-
- /// If the guild's widget is enabled.
- late final bool? embedEnabled;
-
- /// Whether or not the guild is available.
- late final bool available;
-
- /// System Channel Flags
- late final int systemChannelFlags;
-
- /// Channel where "PUBLIC" guilds display rules and/or guidelines
- late final GuildChannel? rulesChannel;
-
- /// The guild owner's ID
- late final User? owner;
-
- /// The guild's members.
- late final Cache<Snowflake, Member> members;
-
- /// The guild's channels.
- late final ChannelCache channels;
-
- /// The guild's roles.
- late final Cache<Snowflake, Role> roles;
-
- /// Guild custom emojis
- late final Cache<Snowflake, GuildEmoji> emojis;
-
- /// Boost level of guild
- late final PremiumTier premiumTier;
-
- /// The number of boosts this server currently has
- late final int? premiumSubscriptionCount;
-
- /// the preferred locale of a "PUBLIC" guild used
- /// in server discovery and notices from Discord; defaults to "en-US"
- late final String preferredLocale;
-
- /// the id of the channel where admins and moderators
- /// of "PUBLIC" guilds receive notices from Discord
- late final GuildChannel? publicUpdatesChannel;
-
- /// Permission of current(bot) user in this guild
- Permissions? currentUserPermissions;
-
- /// Users state cache
- late final Cache<Snowflake, VoiceState> voiceStates;
-
- /// Returns url to this guild.
- String get url => "https://discordapp.com/channels/${this.id.toString()}";
-
- Role get everyoneRole =>
- roles.values.firstWhere((r) => r.name == "@everyone");
-
- /// Returns member object for bot user
- Member? get selfMember => members[client.self.id];
-
- /// Upload limit for this guild in bytes
- int get fileUploadLimit {
- var megabyte = 1024 * 1024;
-
- if(this.premiumTier == PremiumTier.tier2) {
- return 50 * megabyte;
- }
-
- if(this.premiumTier == PremiumTier.tier3) {
- return 100 * megabyte;
- }
-
- return 8 * megabyte;
- }
-
- Guild._new(this.client, Map<String, dynamic> raw,
- [this.available = true, bool guildCreate = false])
- : super(Snowflake(raw['id'] as String)) {
- if (!this.available) return;
-
- this.name = raw['name'] as String;
- this.region = raw['region'] as String;
- this.afkTimeout = raw['afk_timeout'] as int;
- this.mfaLevel = raw['mfa_level'] as int;
- this.verificationLevel = raw['verification_level'] as int;
- this.notificationLevel = raw['default_message_notifications'] as int;
-
- this.icon = raw['icon'] as String?;
- this.discoverySplash = raw['discoverySplash'] as String?;
- this.splash = raw['splash'] as String?;
- this.embedEnabled = raw['embed_enabled'] as bool?;
-
- this.channels = ChannelCache._new();
-
- if (raw['roles'] != null) {
- this.roles = _SnowflakeCache<Role>();
- raw['roles'].forEach((o) {
- var role = Role._new(o as Map<String, dynamic>, this, client);
- this.roles[role.id] = role;
- });
- }
-
- this.emojis = _SnowflakeCache();
- if (raw['emojis'] != null) {
- raw['emojis'].forEach((dynamic o) {
- var emoji = GuildEmoji._new(o as Map<String, dynamic>, this, client);
- this.emojis[emoji.id] = emoji;
- });
- }
-
- if (raw.containsKey('embed_channel_id'))
- this.embedChannel =
- client.channels[Snowflake(raw['embed_channel_id'] as String)]
- as GuildChannel;
-
- if (raw['system_channel_id'] != null) {
- var snow = Snowflake(raw['system_channel_id'] as String);
- if (this.channels.hasKey(snow))
- this.systemChannel = this.channels[snow] as TextChannel;
- }
-
- this.features = (raw['features'] as List<dynamic>).cast<String>();
-
- if (raw['permissions'] != null) {
- this.currentUserPermissions =
- Permissions.fromInt(raw['permissions'] as int);
- }
-
- if (raw['afk_channel_id'] != null) {
- var snow = Snowflake(raw['afk_channel_id'] as String);
- if (this.channels.hasKey(snow))
- this.afkChannel = this.channels[snow] as VoiceChannel;
- }
-
- this.systemChannelFlags = raw['system_channel_flags'] as int;
- this.premiumTier = PremiumTier.from(raw['premium_tier'] as int);
- this.premiumSubscriptionCount = raw['premium_subscription_count'] as int?;
- this.preferredLocale = raw['preferred_locale'] as String;
-
- this.members = _SnowflakeCache();
-
- if (!guildCreate) return;
-
- raw['channels'].forEach((o) {
- late GuildChannel channel;
-
- if (o['type'] == 0 || o['type'] == 5 || o['type'] == 6)
- channel = TextChannel._new(o as Map<String, dynamic>, this, client);
- else if (o['type'] == 2)
- channel = VoiceChannel._new(o as Map<String, dynamic>, this, client);
- else if (o['type'] == 4)
- channel = CategoryChannel._new(o as Map<String, dynamic>, this, client);
-
- this.channels[channel.id] = channel;
- client.channels[channel.id] = channel;
- });
-
- if (client._options.cacheMembers) {
- raw['members'].forEach((o) {
- final member = Member._standard(o as Map<String, dynamic>, this, client);
- this.members[member.id] = member;
- client.users[member.id] = member;
- });
- }
-
- raw['presences'].forEach((o) {
- var member = this.members[Snowflake(o['user']['id'] as String)];
- if (member != null) {
- member.status = ClientStatus._deserialize(o['client_status'] as Map<String, dynamic>);
-
- if (o['game'] != null) {
- member.presence = Activity._new(o['game'] as Map<String, dynamic>);
- }
- }
- });
-
- this.owner = this.members[Snowflake(raw['owner_id'] as String)];
-
- this.voiceStates = _SnowflakeCache();
- if (raw['voice_states'] != null) {
- raw['voice_states'].forEach((o) {
- var state = VoiceState._new(o as Map<String, dynamic>, client, this);
-
- if (state.user != null)
- this.voiceStates[state.user!.id] = state;
- });
- }
-
- if(raw['rules_channel_id'] != null) {
- this.rulesChannel = this.channels[Snowflake(raw['rules_channel_id'])] as GuildChannel?;
- }
-
- if(raw['public_updates_channel_id'] != null) {
- this.publicUpdatesChannel = this.channels[Snowflake(raw['public_updates_channel_id'])] as GuildChannel?;
- }
- }
-
- /// The guild's icon, represented as URL.
- /// If guild doesn't have icon it returns null.
- String? iconURL({String format = 'webp', int size = 128}) {
- if (this.icon != null)
- return 'https://cdn.${_Constants.cdnHost}/icons/${this.id}/${this.icon}.$format?size=$size';
-
- return null;
- }
-
- /// URL to guild's splash.
- /// If guild doesn't have splash it returns null.
- String? splashURL({String format = 'webp', int size = 128}) {
- if (this.splash != null)
- return 'https://cdn.${_Constants.cdnHost}/splashes/${this.id}/${this.splash}.$format?size=$size';
-
- return null;
- }
-
- /// URL to guild's splash.
- /// If guild doesn't have splash it returns null.
- String? discoveryURL({String format = 'webp', int size = 128}) {
- if (this.splash != null)
- return 'https://cdn.${_Constants.cdnHost}/discovery-splashes/${this.id}/${this.splash}.$format?size=$size';
-
- return null;
- }
-
- /// Allows to download [Guild] widget aka advert png
- /// Possible options for [style]: shield (default), banner1, banner2, banner3, banner4
- String guildWidgetUrl([String style = "shield"]) {
- return "http://cdn.${_Constants.cdnHost}/guilds/${this.id.toString()}/widget.png?style=${style}";
- }
-
- /// Returns a string representation of this object - Guild name.
- @override
- String toString() => this.name;
+ DataGuild._new(Snowflake id, this.client) : super(id);
/// Gets Guild Emoji based on Id
///
/// ```
/// var emoji = await guild.getEmoji(Snowflake("461449676218957824"));
/// ```
- Future<GuildEmoji> getEmoji(Snowflake emojiId, [bool useCache = true]) async {
- if (emojis.hasKey(emojiId) && useCache) return emojis[emojiId] as GuildEmoji;
-
+ Future<GuildEmoji> fetchEmoji(Snowflake emojiId, [bool useCache = true]) async {
var response = await client._http._execute(
BasicRequest._new("/guilds/$id/emojis/${emojiId.toString()}"));
@@ -336,11 +57,12 @@
return Future.error(response);
}
+ // TODO: `include_roles parameter`
/// Returns [int] indicating the number of members that would be removed in a prune operation.
Future<int> pruneCount(int days) async {
var response = await client._http._execute(
- BasicRequest._new("/guilds/$id/prune", body: {
- "days": days
+ BasicRequest._new("/guilds/$id/prune", queryParams: {
+ "days": days.toString()
}));
if(response is HttpResponseSuccess) {
@@ -415,26 +137,6 @@
BasicRequest._new("/users/@me/guilds/$id", method: "DELETE"));
}
- Future<Invite> createInvite(
- {int maxAge = 0,
- int maxUses = 0,
- bool temporary = false,
- bool unique = false,
- String? auditReason}) async {
- var chan = this.channels.first as GuildChannel?;
-
- if (chan == null) {
- return Future.error("Cannot get any channel to create invite to");
- }
-
- return chan.createInvite(
- maxUses: maxUses,
- maxAge: maxAge,
- temporary: temporary,
- unique: unique,
- auditReason: auditReason);
- }
-
/// Returns list of Guilds invites
Stream<Invite> getGuildInvites() async* {
var response = await client._http._execute(BasicRequest._new( "/guilds/$id/invites"));
@@ -456,9 +158,9 @@
/// ```
Future<AuditLog> getAuditLogs(
{Snowflake? userId,
- int? actionType,
- Snowflake? before,
- int? limit}) async {
+ int? actionType,
+ Snowflake? before,
+ int? limit}) async {
var queryParams = <String, String> {
if (userId != null) 'user_id' : userId.toString(),
if (actionType != null) 'action_type' : actionType.toString(),
@@ -559,12 +261,12 @@
/// ```
Future<GuildChannel> createChannel(String name, ChannelType type,
{int? bitrate,
- String? topic,
- CategoryChannel? parent,
- bool? nsfw,
- int? userLimit,
- PermissionsBuilder? permissions,
- String? auditReason}) async {
+ String? topic,
+ CategoryChannel? parent,
+ bool? nsfw,
+ int? userLimit,
+ PermissionsBuilder? permissions,
+ String? auditReason}) async {
// Checks to avoid API panic
if (type == ChannelType.dm || type == ChannelType.groupDm) {
return Future.error("Cannot create DM channel.");
@@ -665,12 +367,12 @@
/// Edits the guild.
Future<Guild> edit(
{String? name,
- int? verificationLevel,
- int? notificationLevel,
- VoiceChannel? afkChannel,
- int? afkTimeout,
- String? icon,
- String? auditReason}) async {
+ int? verificationLevel,
+ int? notificationLevel,
+ VoiceChannel? afkChannel,
+ int? afkTimeout,
+ String? icon,
+ String? auditReason}) async {
var body = <String, dynamic> {
if(name != null) "name" : name,
if(verificationLevel != null) "verification_level" : verificationLevel,
@@ -691,21 +393,12 @@
return Future.error(response);
}
- /// Gets a [Member] object. Caches fetched member if not cached.
- ///
- /// ```
- /// var member = guild.getMember(user);
- /// ```
- Future<Member> getMember(User user) async => getMemberById(user.id);
-
- /// Gets a [Member] object by id. Caches fetched member if not cached.
+ /// Gets a [Member] object by id.
///
/// ```
/// var member = guild.getMember(Snowflake('302359795648954380'));
/// ```
- Future<Member> getMemberById(Snowflake id) async {
- if (this.members.hasKey(id)) return this.members[id] as Member;
-
+ Future<Member> fetchMemberById(Snowflake id) async {
var response = await client._http._execute(
BasicRequest._new('/guilds/${this.id}/members/${id.toString()}'));
@@ -716,6 +409,22 @@
return Future.error(response);
}
+ /// Returns a [Stream] of [Member] objects whose username or nickname starts with a provided string.
+ /// By default limits to one entry - can be changed with [limit] parameter.
+ Stream<Member> searchMembers(String query, {int limit = 1}) async* {
+ var response = await client._http._execute(
+ BasicRequest._new("/guilds/${this.id}/members/search",
+ queryParams: { "query" : query, "limit": limit.toString() }));
+
+ if(response is HttpResponseError) {
+ yield* Stream.error(response);
+ }
+
+ for(Map<String, dynamic> member in (response as HttpResponseSuccess).jsonBody) {
+ yield Member._standard(member, this, client);
+ }
+ }
+
/// Gets all of the webhooks for this channel.
Stream<Webhook> getWebhooks() async* {
var response = await client._http._execute(
@@ -736,6 +445,351 @@
BasicRequest._new("/guilds/${this.id}", method: "DELETE"));
}
+}
+
+/// [Guild] object represents single `Discord Server`.
+/// Guilds are a collection of members, channels, and roles that represents one community.
+///
+/// ---------
+///
+/// [channels] property is Map of [Channel]s but it can be cast to specific Channel subclasses. Example with getting all [TextChannel]s in [Guild]:
+/// ```
+/// var textChannels = channels.where((channel) => channel is MessageChannel) as List<TextChannel>;
+/// ```
+/// If you want to get [icon] or [splash] of [Guild] use `iconURL()` method - [icon] property returns only hash, same as [splash] property.
+class Guild extends DataGuild implements Disposable {
+ /// The guild's name.
+ late final String name;
+
+ /// The guild's icon hash.
+ late String? icon;
+
+ /// Splash hash
+ late String? splash;
+
+ /// Discovery splash hash
+ late String? discoverySplash;
+
+ /// System channel where system messages are sent
+ late final TextChannel? systemChannel;
+
+ /// enabled guild features
+ late final List<String> features;
+
+ /// The guild's afk channel ID, null if not set.
+ late VoiceChannel? afkChannel;
+
+ /// The guild's voice region.
+ late String region;
+
+ /// The channel ID for the guild's widget if enabled.
+ late final GuildChannel? embedChannel;
+
+ /// The guild's AFK timeout.
+ late final int afkTimeout;
+
+ /// The guild's verification level.
+ late final int verificationLevel;
+
+ /// The guild's notification level.
+ late final int notificationLevel;
+
+ /// The guild's MFA level.
+ late final int mfaLevel;
+
+ /// If the guild's widget is enabled.
+ late final bool? embedEnabled;
+
+ /// Whether or not the guild is available.
+ late final bool available;
+
+ /// System Channel Flags
+ late final int systemChannelFlags;
+
+ /// Channel where "PUBLIC" guilds display rules and/or guidelines
+ late final GuildChannel? rulesChannel;
+
+ /// The guild owner's ID
+ late final User? owner;
+
+ /// The guild's members.
+ late final Cache<Snowflake, Member> members;
+
+ /// The guild's channels.
+ late final ChannelCache channels;
+
+ /// The guild's roles.
+ late final Cache<Snowflake, Role> roles;
+
+ /// Guild custom emojis
+ late final Cache<Snowflake, GuildEmoji> emojis;
+
+ /// Boost level of guild
+ late final PremiumTier premiumTier;
+
+ /// The number of boosts this server currently has
+ late final int? premiumSubscriptionCount;
+
+ /// the preferred locale of a "PUBLIC" guild used
+ /// in server discovery and notices from Discord; defaults to "en-US"
+ late final String preferredLocale;
+
+ /// the id of the channel where admins and moderators
+ /// of "PUBLIC" guilds receive notices from Discord
+ late final GuildChannel? publicUpdatesChannel;
+
+ /// Permission of current(bot) user in this guild
+ Permissions? currentUserPermissions;
+
+ /// Users state cache
+ late final Cache<Snowflake, VoiceState> voiceStates;
+
+ /// Returns url to this guild.
+ String get url => "https://discordapp.com/channels/${this.id.toString()}";
+
+ Role get everyoneRole =>
+ roles.values.firstWhere((r) => r.name == "@everyone");
+
+ /// Returns member object for bot user
+ Member? get selfMember => members[client.self.id];
+
+ /// Upload limit for this guild in bytes
+ int get fileUploadLimit {
+ var megabyte = 1024 * 1024;
+
+ if(this.premiumTier == PremiumTier.tier2) {
+ return 50 * megabyte;
+ }
+
+ if(this.premiumTier == PremiumTier.tier3) {
+ return 100 * megabyte;
+ }
+
+ return 8 * megabyte;
+ }
+
+ Guild._new(Nyxx client, Map<String, dynamic> raw,
+ [this.available = true, bool guildCreate = false])
+ : super._new(Snowflake(raw['id']), client) {
+ if (!this.available) return;
+
+ this.name = raw['name'] as String;
+ this.region = raw['region'] as String;
+ this.afkTimeout = raw['afk_timeout'] as int;
+ this.mfaLevel = raw['mfa_level'] as int;
+ this.verificationLevel = raw['verification_level'] as int;
+ this.notificationLevel = raw['default_message_notifications'] as int;
+
+ this.icon = raw['icon'] as String?;
+ this.discoverySplash = raw['discoverySplash'] as String?;
+ this.splash = raw['splash'] as String?;
+ this.embedEnabled = raw['embed_enabled'] as bool?;
+
+ this.channels = ChannelCache._new();
+
+ if (raw['roles'] != null) {
+ this.roles = _SnowflakeCache<Role>();
+ raw['roles'].forEach((o) {
+ var role = Role._new(o as Map<String, dynamic>, this, client);
+ this.roles[role.id] = role;
+ });
+ }
+
+ this.emojis = _SnowflakeCache();
+ if (raw['emojis'] != null) {
+ raw['emojis'].forEach((dynamic o) {
+ var emoji = GuildEmoji._new(o as Map<String, dynamic>, this, client);
+ this.emojis[emoji.id] = emoji;
+ });
+ }
+
+ if (raw.containsKey('embed_channel_id'))
+ this.embedChannel =
+ client.channels[Snowflake(raw['embed_channel_id'] as String)]
+ as GuildChannel;
+
+ if (raw['system_channel_id'] != null) {
+ var snow = Snowflake(raw['system_channel_id'] as String);
+ if (this.channels.hasKey(snow))
+ this.systemChannel = this.channels[snow] as TextChannel;
+ }
+
+ this.features = (raw['features'] as List<dynamic>).cast<String>();
+
+ if (raw['permissions'] != null) {
+ this.currentUserPermissions =
+ Permissions.fromInt(raw['permissions'] as int);
+ }
+
+ if (raw['afk_channel_id'] != null) {
+ var snow = Snowflake(raw['afk_channel_id'] as String);
+ if (this.channels.hasKey(snow))
+ this.afkChannel = this.channels[snow] as VoiceChannel;
+ }
+
+ this.systemChannelFlags = raw['system_channel_flags'] as int;
+ this.premiumTier = PremiumTier.from(raw['premium_tier'] as int);
+ this.premiumSubscriptionCount = raw['premium_subscription_count'] as int?;
+ this.preferredLocale = raw['preferred_locale'] as String;
+
+ this.members = _SnowflakeCache();
+
+ if (!guildCreate) return;
+
+ raw['channels'].forEach((o) {
+ late GuildChannel channel;
+
+ if (o['type'] == 0 || o['type'] == 5 || o['type'] == 6)
+ channel = TextChannel._new(o as Map<String, dynamic>, this, client);
+ else if (o['type'] == 2)
+ channel = VoiceChannel._new(o as Map<String, dynamic>, this, client);
+ else if (o['type'] == 4)
+ channel = CategoryChannel._new(o as Map<String, dynamic>, this, client);
+
+ this.channels[channel.id] = channel;
+ client.channels[channel.id] = channel;
+ });
+
+ if (client._options.cacheMembers) {
+ raw['members'].forEach((o) {
+ final member = Member._standard(o as Map<String, dynamic>, this, client);
+ this.members[member.id] = member;
+ client.users[member.id] = member;
+ });
+ }
+
+ raw['presences'].forEach((o) {
+ var member = this.members[Snowflake(o['user']['id'] as String)];
+ if (member != null) {
+ member.status = ClientStatus._deserialize(o['client_status'] as Map<String, dynamic>);
+
+ if (o['game'] != null) {
+ member.presence = Activity._new(o['game'] as Map<String, dynamic>);
+ }
+ }
+ });
+
+ this.owner = this.members[Snowflake(raw['owner_id'] as String)];
+
+ this.voiceStates = _SnowflakeCache();
+ if (raw['voice_states'] != null) {
+ raw['voice_states'].forEach((o) {
+ var state = VoiceState._new(o as Map<String, dynamic>, client, this);
+
+ if (state.user != null)
+ this.voiceStates[state.user!.id] = state;
+ });
+ }
+
+ if(raw['rules_channel_id'] != null) {
+ this.rulesChannel = this.channels[Snowflake(raw['rules_channel_id'])] as GuildChannel?;
+ }
+
+ if(raw['public_updates_channel_id'] != null) {
+ this.publicUpdatesChannel = this.channels[Snowflake(raw['public_updates_channel_id'])] as GuildChannel?;
+ }
+ }
+
+ /// The guild's icon, represented as URL.
+ /// If guild doesn't have icon it returns null.
+ String? iconURL({String format = 'webp', int size = 128}) {
+ if (this.icon != null)
+ return 'https://cdn.${_Constants.cdnHost}/icons/${this.id}/${this.icon}.$format?size=$size';
+
+ return null;
+ }
+
+ /// URL to guild's splash.
+ /// If guild doesn't have splash it returns null.
+ String? splashURL({String format = 'webp', int size = 128}) {
+ if (this.splash != null)
+ return 'https://cdn.${_Constants.cdnHost}/splashes/${this.id}/${this.splash}.$format?size=$size';
+
+ return null;
+ }
+
+ /// URL to guild's splash.
+ /// If guild doesn't have splash it returns null.
+ String? discoveryURL({String format = 'webp', int size = 128}) {
+ if (this.splash != null)
+ return 'https://cdn.${_Constants.cdnHost}/discovery-splashes/${this.id}/${this.splash}.$format?size=$size';
+
+ return null;
+ }
+
+ /// Allows to download [Guild] widget aka advert png
+ /// Possible options for [style]: shield (default), banner1, banner2, banner3, banner4
+ String guildWidgetUrl([String style = "shield"]) {
+ return "http://cdn.${_Constants.cdnHost}/guilds/${this.id.toString()}/widget.png?style=${style}";
+ }
+
+ /// Returns a string representation of this object - Guild name.
+ @override
+ String toString() => this.name;
+
+ /// Gets Guild Emoji based on Id
+ ///
+ /// ```
+ /// var emoji = await guild.getEmoji(Snowflake("461449676218957824"));
+ /// ```
+ Future<GuildEmoji> getEmoji(Snowflake emojiId, [bool useCache = true]) async {
+ if (emojis.hasKey(emojiId) && useCache) return emojis[emojiId] as GuildEmoji;
+
+ var response = await client._http._execute(
+ BasicRequest._new("/guilds/$id/emojis/${emojiId.toString()}"));
+
+ if(response is HttpResponseSuccess) {
+ return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, this, client);
+ }
+
+ return Future.error(response);
+ }
+
+ Future<Invite> createInvite(
+ {int maxAge = 0,
+ int maxUses = 0,
+ bool temporary = false,
+ bool unique = false,
+ String? auditReason}) async {
+ var chan = this.channels.first as GuildChannel?;
+
+ if (chan == null) {
+ return Future.error("Cannot get any channel to create invite to");
+ }
+
+ return chan.createInvite(
+ maxUses: maxUses,
+ maxAge: maxAge,
+ temporary: temporary,
+ unique: unique,
+ auditReason: auditReason);
+ }
+
+ /// Gets a [Member] object. Caches fetched member if not cached.
+ ///
+ /// ```
+ /// var member = guild.getMember(user);
+ /// ```
+ Future<Member> getMember(User user) async => getMemberById(user.id);
+
+ /// Gets a [Member] object by id. Caches fetched member if not cached.
+ ///
+ /// ```
+ /// var member = guild.getMember(Snowflake('302359795648954380'));
+ /// ```
+ Future<Member> getMemberById(Snowflake id) async {
+ if (this.members.hasKey(id)) return this.members[id] as Member;
+
+ var response = await client._http._execute(
+ BasicRequest._new('/guilds/${this.id}/members/${id.toString()}'));
+
+ if(response is HttpResponseSuccess) {
+ return Member._standard(response.jsonBody as Map<String, dynamic>, this, client);
+ }
+
+ return Future.error(response);
+ }
+
@override
Future<void> dispose() async {
await channels.dispose();
Index: nyxx/lib/src/core/message/GuildEmoji.dart
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- nyxx/lib/src/core/message/GuildEmoji.dart (revision c17657e7566a5f411077c568f61ea18dcb1bdb5a)
+++ nyxx/lib/src/core/message/GuildEmoji.dart (date 1588628915000)
@@ -9,7 +9,7 @@
@override
/// Emoji guild
- late final Guild guild;
+ late final DataGuild guild;
@override
@@ -43,9 +43,9 @@
this.animated = raw['animated'] as bool? ?? false;
this.roles = [];
- if (raw['roles'] != null) {
+ if (raw['roles'] != null && this.guild is Guild) {
raw['roles'].forEach(
- (o) => this.roles.add(this.guild.roles[Snowflake(o as String)]));
+ (o) => this.roles.add((this.guild as Guild).roles[Snowflake(o as String)]));
}
this.partial = false;
Index: nyxx/lib/src/core/GuildEntity.dart
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- nyxx/lib/src/core/GuildEntity.dart (revision c17657e7566a5f411077c568f61ea18dcb1bdb5a)
+++ nyxx/lib/src/core/GuildEntity.dart (date 1588628915000)
@@ -2,5 +2,5 @@
/// Represents entity which bound to guild, eg. member, emoji, message, role.
abstract class GuildEntity {
- Guild? get guild;
+ DataGuild? get guild;
}

View file

@ -1,3 +1,22 @@
# Contributing
If you want to contribute, feel free to fork and make a PR. Please lint and run `dartfmt` before opening a PR.
And make sure to always make your PRs to `development` branch.
Nyxx is free and open-source project, and all contributions are welcome and highly appreciated.
However, please conform to the following guidelines when possible.
## Development cycle
All changes should be discussed beforehand either in issue or pull request on github
or in a discussion in our Discord channel with library regulars or other contributors.
All issues marked with 'help-needed' badge are free to be picked up by any member of the community.
### Pull Requests
Pull requests should be descriptive about changes that are made.
If adding new functionality or modifying existing, documentation should be added/modified to reflect changes.
## Coding style
We attempt to conform [Effective Dart Coding Style](https://dart.dev/guides/language/effective-dart/style) where possible.
However, code style rules are not enforcement and code should be readable and easy to maintain.
**One exception to rules above is line limit - we use 120 character line limit instead of 80 chars.**

View file

@ -3,29 +3,36 @@ part of nyxx.commander;
/// Helper class which describes context in which command is executed
class CommandContext {
/// Channel from where message come from
MessageChannel channel;
final MessageChannel channel;
/// Author of message
IMessageAuthor? author;
final IMessageAuthor? author;
/// Message that was sent
Message message;
final Message message;
/// Guild in which message was sent
Guild? guild;
final Guild? guild;
/// Returns author as guild member
Member? get member => guild?.members[author!.id];
IMember? get member => guild?.members[author!.id];
CommandContext._new(this.channel, this.author, this.guild, this.message);
/// Reference to client
Nyxx get client => channel.client;
/// Shard on which message was sent
int get shardId => this.guild != null ? this.guild!.shard.id : 0;
/// Substring by which command was matched
final String commandMatcher;
CommandContext._new(this.channel, this.author, this.guild, this.message, this.commandMatcher);
/// Reply to message. It allows to send regular message, Embed or both.
///
/// ```
/// /// Class body
/// @Command()
/// Future<void> getAv(CommandContext context, String message) async {
/// await context.reply(content: uset.avatarURL());
/// Future<void> getAv(CommandContext context) async {
/// await context.reply(content: context.user.avatarURL());
/// }
/// ```
Future<Message> reply(
@ -40,8 +47,7 @@ class CommandContext {
/// Reply to messages, then delete it when [duration] expires.
///
/// ```
/// @Command()
/// Future<void> getAv(CommandContext context, String message) async {
/// Future<void> getAv(CommandContext context) async {
/// await context.replyTemp(content: user.avatarURL());
/// }
/// ```
@ -56,8 +62,7 @@ class CommandContext {
/// Replies to message after delay specified with [duration]
/// ```
/// @Command()
/// Future<void> getAv(CommandContext context, String message) async {
/// Future<void> getAv(CommandContext context async {
/// await context.replyDelayed(Duration(seconds: 2), content: user.avatarURL());
/// }
/// ```
@ -77,18 +82,22 @@ class CommandContext {
builder: builder,
allowedMentions: allowedMentions));
/// Gather emojis of message in given time
/// Awaits for emoji under given [msg]
Future<Emoji> awaitEmoji(Message msg) async =>
(await this.client.onMessageReactionAdded.where((event) => event.message == msg).first).emoji;
/// Collects emojis within given [duration]. Returns empty map if no reaction received
///
/// ```
/// Future<void> getAv(CommandContext context, String message) async {
/// var msg = await context.replyDelayed(content: context.user.avatarURL());
/// var emojis = await context.collectEmojis(msg, Duration(seconds: 15));
/// Future<void> getAv(CommandContext context) async {
/// final msg = await context.replyDelayed(content: context.user.avatarURL());
/// final emojis = await context.awaitEmojis(msg, Duration(seconds: 15));
///
/// ...
/// }
/// ```
Future<Map<Emoji, int>?> collectEmojis(Message msg, Duration duration) => Future<Map<Emoji, int>?>(() async {
final collectedEmoji = <Emoji, int>{};
Future<Map<Emoji, int>> awaitEmojis(Message msg, Duration duration){
final collectedEmoji = <Emoji, int>{};
return Future<Map<Emoji, int>>(() async {
await for (final event in msg.client.onMessageReactionAdded.where((evnt) => evnt.message != null && evnt.message!.id == msg.id)) {
if (collectedEmoji.containsKey(event.emoji)) {
// TODO: NNBD: weird stuff
@ -104,7 +113,9 @@ class CommandContext {
}
return collectedEmoji;
}).timeout(duration, onTimeout: () => null);
}).timeout(duration, onTimeout: () => collectedEmoji);
}
/// Waits for first [TypingEvent] and returns it. If timed out returns null.
/// Can listen to specific user by specifying [user]
@ -113,9 +124,8 @@ class CommandContext {
/// Gets all context channel messages that satisfies [predicate].
///
/// ```
/// @Command()
/// Future<void> getAv(CommandContext context, String message) async {
/// var messages = await context.nextMessagesWhere((msg) => msg.content.startsWith("fuck"));
/// Future<void> getAv(CommandContext context) async {
/// final messages = await context.nextMessagesWhere((msg) => msg.content.startsWith("fuck"));
/// }
/// ```
Stream<MessageReceivedEvent> nextMessagesWhere(bool Function(MessageReceivedEvent msg) predicate, {int limit = 100}) => channel.onMessage.where(predicate).take(limit);
@ -123,15 +133,50 @@ class CommandContext {
/// Gets next [num] number of any messages sent within one context (same channel).
///
/// ```
/// @Command()
/// Future<void> getAv(CommandContext context, String message) async {
/// Future<void> getAv(CommandContext context) async {
/// // gets next 10 messages
/// var messages = await context.nextMessages(10);
/// final messages = await context.nextMessages(10);
/// }
/// ```
Stream<MessageReceivedEvent> nextMessages(int num) => channel.onMessage.take(num);
/// Returns stream of all code blocks in message
/// Starts typing loop and ends when [callback] resolves.
Future<T> enterTypingState<T>(Future<T> Function() callback) async {
this.channel.startTypingLoop();
final result = await callback();
this.channel.stopTypingLoop();
return result;
}
/// Returns list of words separated with space and/or text surrounded by quotes
/// Text: `hi this is "example stuff" which 'can be parsed'` will return
/// `List<String> [hi, this, is, example stuff, which, can be parsed]`
Iterable<String> getArguments() sync* {
final regex = RegExp('([A-Z0-9a-z]+)|["\']([^"]*)["\']');
final matches = regex.allMatches(this.message.content.replaceFirst(commandMatcher, ""));
for(final match in matches) {
final group1 = match.group(1);
yield group1 ?? match.group(2)!;
}
}
/// Returns list which content of quotes.
/// Text: `hi this is "example stuff" which 'can be parsed'` will return
/// `List<String> [example stuff, can be parsed]`
Iterable<String> getQuotedText() sync* {
final regex = RegExp('["\']([^"]*)["\']');
final matches = regex.allMatches(this.message.content.replaceFirst(commandMatcher, ""));
for(final match in matches) {
yield match.group(1)!;
}
}
/// Returns list of all code blocks in message
/// Language string `dart, java` will be ignored and not included
/// """
/// n> eval ```(dart)?

View file

@ -1,35 +1,90 @@
part of nyxx.commander;
/// Handles command execution - requires to implement [commandName] field which
/// returns name of command to match message content, and [commandHandler] callback
/// which is fired when command matches message content.
abstract class CommandHandler {
/// Executed before main [commandHandler] callback.
/// Base object for [CommandHandler] and [CommandGroup]
abstract class CommandEntity {
/// Executed before executing command.
/// Used to check if command can be executed in current context.
PassHandlerFunction? get beforeHandler => null;
/// Callback executed after [commandHandler].
CommandHandlerFunction? get afterHandler => null;
/// Callback executed after executing command
AfterHandlerFunction? get afterHandler => null;
/// Main command callback
CommandHandlerFunction get commandHandler;
/// Name of [CommandEntity]
String get name;
/// Command name
String get commandName;
/// Parent of entity
CommandEntity? get parent;
}
class _InternalCommandHandler implements CommandHandler {
/// Creates command group. Pass a [name] to crated command and commands added
/// via [registerSubCommand] will be subcommands og that group
class CommandGroup extends CommandEntity {
final List<CommandEntity> _commandEntities = [];
@override
final PassHandlerFunction? beforeHandler;
@override
final CommandHandlerFunction? afterHandler;
final AfterHandlerFunction? afterHandler;
/// Default [CommandHandler] for [CommandGroup] - it will be executed then no other command from group match
CommandHandler? defaultHandler;
@override
final CommandHandlerFunction commandHandler;
final String name;
@override
final String commandName;
CommandGroup? parent;
_InternalCommandHandler(this.commandName, this.commandHandler, {this.beforeHandler, this.afterHandler});
/// Creates command group. Pass a [name] to crated command and commands added
/// via [registerSubCommand] will be subcommands og that group
CommandGroup({this.name = "", this.defaultHandler, this.beforeHandler, this.afterHandler, this.parent});
/// Registers default command handler which will be executed if no subcommand is matched to message content
void registerDefaultCommand(CommandHandlerFunction commandHandler,
{PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) {
this.defaultHandler = BasicCommandHandler(this.name, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this);
}
/// Registers subcommand
void registerSubCommand(String name, CommandHandlerFunction commandHandler,
{PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) {
this._commandEntities.add(
BasicCommandHandler(name, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler, parent: this));
// TODO: That is not most efficient way
this._commandEntities.sort((a, b) => -a.name.compareTo(b.name));
}
/// Registers command as implemented [CommandEntity] class
void registerCommandGroup(CommandGroup commandGroup) => this._commandEntities.add(commandGroup..parent = this);
}
/// Handles command execution - requires to implement [name] field which
/// returns name of command to match message content, and [commandHandler] callback
/// which is fired when command matches message content.
abstract class CommandHandler extends CommandEntity {
/// Main command callback
CommandHandlerFunction get commandHandler;
}
/// Basic implementation of command handler. Used internally in library.
class BasicCommandHandler extends CommandHandler {
@override
final PassHandlerFunction? beforeHandler;
@override
final AfterHandlerFunction? afterHandler;
@override
CommandHandlerFunction commandHandler;
@override
final String name;
@override
CommandGroup? parent;
/// Basic implementation of command handler. Used internally in library.
BasicCommandHandler(this.name, this.commandHandler, {this.beforeHandler, this.afterHandler, this.parent});
}

View file

@ -2,44 +2,52 @@ part of nyxx.commander;
/// Used to determine if command can be executed in given environment.
/// Return true to allow executing command or false otherwise.
typedef PassHandlerFunction = FutureOr<bool> Function(CommandContext context, String message);
typedef PassHandlerFunction = FutureOr<bool> Function(CommandContext context);
/// Handler for executing command logic.
typedef CommandHandlerFunction = FutureOr<void> Function(CommandContext context, String message);
/// Handler for executing logic after executing command.
typedef AfterHandlerFunction = FutureOr<void> Function(CommandContext context);
/// Handler used to determine prefix for command in given environment.
/// Can be used to define different prefixes for different guild, users or dms.
/// Return String containing prefix or null if command cannot be executed.
typedef PrefixHandlerFunction = FutureOr<String?> Function(CommandContext context, String message);
typedef PrefixHandlerFunction = FutureOr<String?> Function(Message message);
/// Callback to customize logger output when command is executed.
typedef LoggerHandlerFunction = FutureOr<void> Function(CommandContext context, String commandName, Logger logger);
/// Callback called when command executions returns with [Exception] or [Error] ([exception] variable could be either).
typedef CommandExecutionError = FutureOr<void> Function(CommandContext context, dynamic exception);
/// Lightweight command framework. Doesn't use `dart:mirrors` and can be used in browser.
/// While constructing specify prefix which is string with prefix or
/// implement [PrefixHandlerFunction] for more fine control over where and in what conditions commands are executed.
///
/// Allows to specify callbacks which are executed before and after command - also on per command basis.
/// [BeforeHandlerFunction] callbacks are executed only command exists and is matched with message content.
/// [beforeCommandHandler] callbacks are executed only command exists and is matched with message content.
class Commander {
late final PrefixHandlerFunction _prefixHandler;
late final PassHandlerFunction? _beforeComandHandler;
late final CommandHandlerFunction? _afterHandlerFunction;
late final PassHandlerFunction? _beforeCommandHandler;
late final AfterHandlerFunction? _afterHandlerFunction;
late final LoggerHandlerFunction _loggerHandlerFunction;
late final CommandExecutionError? _commandExecutionError;
final List<CommandHandler> _commands = [];
final List<CommandEntity> _commandEntities = [];
final Logger _logger = Logger("Commander");
/// Either [prefix] or [prefixHandler] must be specified otherwise program will exit.
/// Allows to specify additional [beforeCommandHandler] executed before main command callback,
/// and [afterCommandCallback] executed after main command callback.
/// and [afterCommandHandler] executed after main command callback.
Commander(Nyxx client,
{String? prefix,
PrefixHandlerFunction? prefixHandler,
PassHandlerFunction? beforeCommandHandler,
CommandHandlerFunction? afterCommandHandler,
LoggerHandlerFunction? loggerHandlerFunction}) {
{String? prefix,
PrefixHandlerFunction? prefixHandler,
PassHandlerFunction? beforeCommandHandler,
AfterHandlerFunction? afterCommandHandler,
LoggerHandlerFunction? loggerHandlerFunction,
CommandExecutionError? commandExecutionError}) {
if (prefix == null && prefixHandler == null) {
_logger.shout("Commander cannot start without both prefix and prefixHandler");
exit(1);
@ -48,69 +56,132 @@ class Commander {
if (prefix == null) {
_prefixHandler = prefixHandler!;
} else {
_prefixHandler = (ctx, msg) => prefix;
_prefixHandler = (_) => prefix;
}
this._beforeComandHandler = beforeCommandHandler;
this._beforeCommandHandler = beforeCommandHandler;
this._afterHandlerFunction = afterCommandHandler;
this._commandExecutionError = commandExecutionError;
this._loggerHandlerFunction = loggerHandlerFunction ?? _defaultLogger;
client.onMessageReceived.listen(_handleMessage);
this._logger.info("Commander ready!");
}
Future<void> _handleMessage(MessageReceivedEvent event) async {
final prefix = await _prefixHandler(event.message);
if (prefix == null) {
return;
}
if(!event.message.content.startsWith(prefix)) {
return;
}
// Find matching command with given message content
final matchingCommand = _commandEntities._findMatchingCommand(event.message.content.replaceFirst(prefix, "").trim().split(" ")) as CommandHandler?;
if(matchingCommand == null) {
return;
}
final fullCommandName = getFullCommandName(matchingCommand);
// construct commandcontext
final context = CommandContext._new(event.message.channel, event.message.author,
event.message is GuildMessage ? (event.message as GuildMessage).guild : null, event.message, "$prefix$fullCommandName");
// Invoke before handler for commands
if (!(await _invokeBeforeHandler(matchingCommand, context))) {
return;
}
// Invoke before handler for commander
if(this._beforeCommandHandler != null && !(await this._beforeCommandHandler!(context))) {
return;
}
// Execute command
try {
await matchingCommand.commandHandler(context, event.message.content);
} on Exception catch (e) {
if(this._commandExecutionError != null) {
await _commandExecutionError!(context, e);
}
} on Error catch (e) {
if(this._commandExecutionError != null) {
await _commandExecutionError!(context, e);
}
}
// execute logger callback
_loggerHandlerFunction(context, fullCommandName, this._logger);
// invoke after handler of command
await _invokeAfterHandler(matchingCommand, context);
// Invoke after handler for commander
if (this._afterHandlerFunction != null) {
this._afterHandlerFunction!(context);
}
}
// Invokes command after handler and its parents
Future<void> _invokeAfterHandler(CommandEntity? commandEntity, CommandContext context) async {
if(commandEntity == null) {
return;
}
if(commandEntity.afterHandler != null) {
await commandEntity.afterHandler!(context);
}
if(commandEntity.parent != null) {
await _invokeAfterHandler(commandEntity.parent, context);
}
}
// Invokes command before handler and its parents. It will check for next before handlers if top handler returns true.
Future<bool> _invokeBeforeHandler(CommandEntity? commandEntity, CommandContext context) async {
if(commandEntity == null) {
return true;
}
if(commandEntity.beforeHandler == null) {
return _invokeBeforeHandler(commandEntity.parent, context);
}
if(await commandEntity.beforeHandler!(context)) {
return _invokeBeforeHandler(commandEntity.parent, context);
}
return false;
}
FutureOr<void> _defaultLogger(CommandContext ctx, String commandName, Logger logger) {
logger.info("Command [$commandName] executed by [${ctx.author!.tag}]");
}
Future<void> _handleMessage(MessageReceivedEvent event) async {
final context = CommandContext._new(event.message.channel, event.message.author,
event.message is GuildMessage ? (event.message as GuildMessage).guild : null, event.message);
final prefix = await _prefixHandler(context, event.message.content);
if (prefix == null) {
return;
}
// TODO: NNBD: try-catch in where
CommandHandler? matchingCommand;
try {
matchingCommand = _commands.firstWhere(
(element) => _isCommandMatching(element.commandName, event.message.content.replaceFirst(prefix, "")));
} on Error {
return;
}
if (this._beforeComandHandler != null && !await this._beforeComandHandler!(context, event.message.content)) {
return;
}
if (matchingCommand.beforeHandler != null &&
!await matchingCommand.beforeHandler!(context, event.message.content)) {
return;
}
await matchingCommand.commandHandler(context, event.message.content);
// execute logger callback
_loggerHandlerFunction(context, matchingCommand.commandName, this._logger);
if (matchingCommand.afterHandler != null) {
await matchingCommand.afterHandler!(context, event.message.content);
}
if (this._afterHandlerFunction != null) {
this._afterHandlerFunction!(context, event.message.content);
}
}
/// Registers command with given [commandName]. Allows to specify command specific before and after command execution callbacks
void registerCommand(String commandName, CommandHandlerFunction commandHandler,
{PassHandlerFunction? beforeHandler, CommandHandlerFunction? afterHandler}) {
this._commands.add(
_InternalCommandHandler(commandName, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler));
void registerCommand(String commandName, CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) {
this._commandEntities.add(BasicCommandHandler(commandName, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler));
// TODO: That is not most efficient way
this._commandEntities.sort((a, b) => -a.name.compareTo(b.name));
}
/// Registers command as implemented [CommandHandler] class
void registerCommandClass(CommandHandler commandHandler) => this._commands.add(commandHandler);
/// Registers command as implemented [CommandEntity] class
void registerCommandGroup(CommandGroup commandGroup) => this._commandEntities.add(commandGroup);
}
/// Full qualified command name with its parents names
String getFullCommandName(CommandEntity entity) {
var commandName = entity.name;
for(var e = entity.parent; e != null; e = e.parent) {
commandName = "${e.name} $commandName";
}
return commandName.trim();
}

View file

@ -1,18 +1,30 @@
part of nyxx.commander;
bool _isCommandMatching(String command, String message) {
final commandParts = command.split(" ");
final messageParts = message.split(" ");
// TODO: FIX. I think that is just awful but is does its job
extension _CommandMatcher on Iterable<CommandEntity> {
CommandEntity? _findMatchingCommand(Iterable<String> messageParts) {
for (final entity in this) {
if(entity is CommandGroup && entity.name == "") {
final e = entity._commandEntities._findMatchingCommand(messageParts);
if (commandParts.length > messageParts.length) {
return false;
}
if(e != null) {
return e;
}
}
for (var i = 0; i < commandParts.length; i++) {
if (commandParts[i] != messageParts[i]) {
return false;
if(entity is CommandGroup && entity.name == messageParts.first) {
final e = entity._commandEntities._findMatchingCommand(messageParts.skip(1));
if(e != null) {
return e;
}
}
if(entity is CommandHandler && entity.name == messageParts.first) {
return entity;
}
}
}
return true;
}
return null;
}
}

View file

@ -10,7 +10,7 @@ documentation: https://github.com/l7ssha/nyxx/wiki
issue_tracker: https://github.com/l7ssha/nyxx/issue
environment:
sdk: '>=2.9.0-2.0.dev <3.0.0'
sdk: '>=2.9.0-10.0.dev <3.0.0'
dependencies:
nyxx: "^1.0.0"

View file

@ -5,17 +5,15 @@ import "package:nyxx/nyxx.dart";
import "package:nyxx.commander/commander.dart";
void main() {
setupDefaultLogging();
final bot = Nyxx(Platform.environment["DISCORD_TOKEN"]!, ignoreExceptions: false);
bot.onMessageReceived.listen((event) async {
if (event.message.content == "Test 1") {
event.message.delete();
event.message.delete(); // ignore: unawaited_futures
}
if (event.message.content == "Test 2") {
event.message.delete();
event.message.delete(); // ignore: unawaited_futures
}
if (event.message.content == "Test 10") {
@ -27,22 +25,22 @@ void main() {
});
bot.onReady.listen((e) async {
final channel = bot.channels[Snowflake("422285619952222208")] as TextChannel;
final channel = bot.channels[Snowflake("422285619952222208")] as CachelessTextChannel;
await channel.send(content: "Testing Commander");
final msg1 = await channel.send(content: "test>test1");
msg1.delete();
msg1.delete(); // ignore: unawaited_futures
final msg2 = await channel.send(content: "test>test2 arg1");
msg2.delete();
msg2.delete(); // ignore: unawaited_futures
final msg3 = await channel.send(content: "test>test3");
msg3.delete();
msg3.delete(); // ignore: unawaited_futures
});
Commander(bot, prefix: "test>", beforeCommandHandler: (context, message) async {
if (message.endsWith("test3")) {
Commander(bot, prefix: "test>", beforeCommandHandler: (context) async {
if (context.message.content.endsWith("test3")) {
await context.channel.send(content: "Test 10");
return true;
}

View file

@ -0,0 +1,10 @@
library nyxx.extensions.emoji;
import "dart:convert";
import "package:nyxx/nyxx.dart";
import "package:w_transport/w_transport.dart" as w_transport;
import "package:w_transport/vm.dart" as w_transport show vmTransportPlatform;
part "src/emoji/EmojiDefinition.dart";
part "src/emoji/EmojiUtils.dart";

View file

@ -0,0 +1,3 @@
library nyxx.extensions.pagination;
export "src/pagination/pagination.dart";

View file

@ -0,0 +1 @@
export "src/scheduler/scheduler.dart";

View file

@ -0,0 +1,34 @@
part of nyxx.extensions.emoji;
/// Wrapper class around discords emojis data
class EmojiDefinition {
/// Name of emoji
late final String primaryName;
/// List of alternative names of emoji
late final Iterable<String> names;
/// Literal emoji
late final String rawEmoji;
/// List of utf32 code points
late final Iterable<int> codePoints;
/// Name of asset used in discords frontend for this emoji
late final String assetFileName;
/// Url of emoji picture
late final String assetUrl;
EmojiDefinition._new(Map<String, dynamic> raw) {
this.primaryName = raw["primaryName"] as String;
this.names = (raw["names"] as Iterable<dynamic>).cast();
this.rawEmoji = raw["surrogates"] as String;
this.codePoints = (raw["utf32codepoints"] as Iterable<dynamic>).cast();
this.assetFileName = raw["assetFileName"] as String;
this.assetUrl = raw["assetUrl"] as String;
}
/// Returns [UnicodeEmoji] object of this
UnicodeEmoji toEmoji() => UnicodeEmoji(rawEmoji);
}

View file

@ -0,0 +1,40 @@
part of nyxx.extensions.emoji;
List<EmojiDefinition> _emojisCache = [];
Future<Map<String, dynamic>> _downloadEmojiData() async {
final request = w_transport.JsonRequest(transportPlatform: w_transport.vmTransportPlatform)
..uri = emojiDataUri;
// ??? Had to trim 3 characters for some reason
final bodyString = (await request.send("GET")).body.asString().substring(3);
return jsonDecode(bodyString) as Map<String, dynamic>;
}
/// Emoji definitions uri
final Uri emojiDataUri = Uri.parse("https://static.emzi0767.com/misc/discordEmojiMap.json");
/// Returns emoji based on given [predicate]. Allows to cache results via [cache] parameter.
Future<EmojiDefinition> filterEmojiDefinitions(bool Function(EmojiDefinition) predicate, {bool cache = false}) async =>
(await getAllEmojiDefinitions(cache: cache)).firstWhere(predicate);
/// Returns all possible [EmojiDefinition]s. Allows to cache results via [cache] parameter.
Future<Iterable<EmojiDefinition>> getAllEmojiDefinitions({bool cache = false}) async {
if(_emojisCache.isNotEmpty) {
return Future.value(_emojisCache);
}
final rawData = await _downloadEmojiData();
final _emojis = [
for(final ed in rawData["emojiDefinitions"])
EmojiDefinition._new(ed as Map<String, dynamic>)
];
if(cache) {
_emojisCache = _emojis;
}
return _emojis;
}

View file

@ -0,0 +1,122 @@
import "dart:async" show Future, FutureOr, Stream;
import "package:nyxx/nyxx.dart" show Emoji, ITextChannel, Message, MessageBuilder, MessageChannel, MessageEditBuilder, MessageReactionEvent, Nyxx, UnicodeEmoji;
import "../../emoji.dart" show filterEmojiDefinitions;
import "../utils.dart" show StreamUtils, StringUtils;
/// Handles data and constructing data
abstract class IPaginationHandler {
/// Used to generate message based on given [page] number.
FutureOr<MessageEditBuilder> generatePage(int page);
/// Used to generate fist page of Paginated message.
FutureOr<MessageBuilder> generateInitialPage();
/// Return total number of pages
int get dataLength;
/// Emoji used to navigate to next page. Default: ""
FutureOr<Emoji> get nextEmoji async => (await filterEmojiDefinitions((emoji) => emoji.primaryName == "arrow_forward", cache: true)).toEmoji();
/// Emoji used to navigate to previous page. Default: ""
FutureOr<Emoji> get backEmoji async => (await filterEmojiDefinitions((emoji) => emoji.primaryName == "arrow_backward", cache: true)).toEmoji();
/// Emoji used to navigate to first page. Default: ""
FutureOr<Emoji> get firstEmoji async => (await filterEmojiDefinitions((emoji) => emoji.primaryName == "track_previous", cache: true)).toEmoji();
/// Emoji used to navigate to last page. Default: ""
FutureOr<Emoji> get lastEmoji async => (await filterEmojiDefinitions((emoji) => emoji.primaryName == "track_next", cache: true)).toEmoji();
}
/// Basic pagination handler based on [String]. Each entry in [pages] will be different page.
class BasicPaginationHandler extends IPaginationHandler {
/// Pages of paginated message
List<String> pages;
/// Generates new pagination from List of Strings. Each list element is single page.
BasicPaginationHandler(this.pages);
/// Generates pagination from String. It divides String into 250 char long pages.
factory BasicPaginationHandler.fromString(String str, MessageChannel channel) => BasicPaginationHandler(StringUtils.split(str, 250).toList());
/// Generates pagination from String but with user specified size of single page.
factory BasicPaginationHandler.fromStringLen(String str, int len, MessageChannel channel) => BasicPaginationHandler(StringUtils.split(str, len).toList());
/// Generates pagination from String but with user specified number of pages.
factory BasicPaginationHandler.fromStringEq(String str, int pieces, MessageChannel channel) => BasicPaginationHandler(StringUtils.splitEqually(str, pieces).toList());
@override
FutureOr<MessageEditBuilder> generatePage(int page) =>
MessageBuilder()..content = pages[page];
@override
FutureOr<MessageBuilder> generateInitialPage() =>
generatePage(0) as MessageBuilder;
@override
int get dataLength => pages.length;
}
/// Handles pagination interactivity. Allows to create paginated messages from List<String>
/// Factory constructors allows to create message from String directly.
///
/// Pagination is sent by [paginate] method. And returns [Message] instance of sent message.
///
/// ```
/// var pagination = new Pagination(["This is simple paginated", "data. Use it if you", "want to partition text by yourself"], ctx,channel);
/// // It generated 2 equal (possibly) pages.
/// var paginatedMessage = new Pagination.fromStringEq("This is text for pagination", 2);
/// ```
class Pagination<T extends IPaginationHandler> {
/// Channel where message will be sent
ITextChannel channel;
/// [IPaginationHandler] which will handle generating messages.
T paginationHandler;
///
Pagination(this.channel, this.paginationHandler);
/// Paginates a list of Strings - each String is a different page.
Future<Message> paginate(Nyxx client, {Duration timeout = const Duration(minutes: 2)}) async {
final nextEmoji = await paginationHandler.nextEmoji;
final backEmoji = await paginationHandler.backEmoji;
final firstEmoji = await paginationHandler.firstEmoji;
final lastEmoji = await paginationHandler.lastEmoji;
final msg = await channel.send(builder: await paginationHandler.generateInitialPage());
await msg.createReaction(firstEmoji);
await msg.createReaction(backEmoji);
await msg.createReaction(nextEmoji);
await msg.createReaction(lastEmoji);
await Future(() async {
var currPage = 0;
final group = StreamUtils.merge(
[client.onMessageReactionAdded, client.onMessageReactionsRemoved as Stream<MessageReactionEvent>]);
await for (final event in group) {
final emoji = (event as dynamic).emoji as UnicodeEmoji;
if (emoji == nextEmoji) {
if (currPage <= paginationHandler.dataLength - 2) {
++currPage;
await msg.edit(builder: await paginationHandler.generatePage(currPage));
}
} else if (emoji == backEmoji) {
if (currPage >= 1) {
--currPage;
await msg.edit(builder: await paginationHandler.generatePage(currPage));
}
} else if (emoji == firstEmoji) {
currPage = 0;
await msg.edit(builder: await paginationHandler.generatePage(currPage));
} else if (emoji == lastEmoji) {
currPage = paginationHandler.dataLength;
await msg.edit(builder: await paginationHandler.generatePage(currPage));
}
}
}).timeout(timeout);
return msg;
}
}

View file

@ -0,0 +1,27 @@
import "dart:async";
import "package:nyxx/nyxx.dart" show Nyxx;
/// Callback for [ScheduledEvent]. Is executed every period given in [ScheduledEvent]
typedef ScheduledEventCallback = Future<void> Function(Nyxx, Timer);
/// Creates and starts new periodic event. [callback] will be executed every given duration of time.
/// Event can be stopped via [stop] function.
class ScheduledEvent {
/// [Nyxx] instance
final Nyxx client;
/// Callback which will be run every given period of time
final ScheduledEventCallback callback;
late final Timer _timer;
/// Creates and starts new periodic event. [callback] will be executed every [duration] of time.
/// Event can be stopped via [stop] function.
ScheduledEvent(this.client, Duration duration, this.callback) {
_timer = Timer.periodic(duration, (timer) => callback(client, timer));
}
/// Stops [ScheduledEvent] if running.
void stop() => _timer.cancel();
}

View file

@ -1,6 +1,6 @@
part of nyxx.interactivity;
import "dart:async";
class _Utils {
class StreamUtils {
/// Merges list of stream into one stream
static Stream<T> merge<T>(List<Stream<T>> streams) {
var _open = streams.length;
@ -16,7 +16,9 @@ class _Utils {
}
return streamController.stream;
}
}
class StringUtils {
/// Splits string based on desired length
static Iterable<String> split(String str, int length) sync* {
var last = 0;
@ -33,20 +35,4 @@ class _Utils {
return split(str, len);
}
/// Partition list into chunks
static Iterable<List<T>> partition<T>(List<T> lst, int len) sync* {
for (var i = 0; i < lst.length; i += len) {
yield lst.sublist(i, i + len);
}
}
/// Divides list into equal pieces
static Stream<List<T>> chunk<T>(List<T> list, int chunkSize) async* {
final len = list.length;
for (var i = 0; i < len; i += chunkSize) {
final size = i + chunkSize;
yield list.sublist(i, size > len ? len : size);
}
}
}
}

View file

@ -0,0 +1 @@
export "src/utils.dart";

View file

@ -1,6 +1,6 @@
name: nyxx.interactivity
name: nyxx.extensions
version: 1.0.0
description: A Discord library for Dart.
description: Extensions for Nyxx library
authors:
- Szymon Uglis <szymon.uglis@tuta.io>
- Zach Vacura <admin@hackzzila.com>
@ -10,7 +10,7 @@ documentation: https://github.com/l7ssha/nyxx/wiki
issue_tracker: https://github.com/l7ssha/nyxx/issue
environment:
sdk: '>=2.9.0-2.0.dev <3.0.0'
sdk: '>=2.9.0-10.0.dev <3.0.0'
dependencies:
nyxx: "^1.0.0"

View file

@ -0,0 +1,29 @@
import 'dart:io';
import "package:nyxx.extensions/emoji.dart";
main() async {
print("Memory: ${(ProcessInfo.currentRss / 1024 / 1024).toStringAsFixed(2)} MB");
final stopwatch = Stopwatch()..start();
await filterEmojiDefinitions((emoji) => emoji.primaryName == "joy", cache: true);
print("TIME: ${stopwatch.elapsedMilliseconds} ms");
print("Memory: ${(ProcessInfo.currentRss / 1024 / 1024).toStringAsFixed(2)} MB");
stopwatch.reset();
await filterEmojiDefinitions((emoji) => emoji.primaryName == "cry", cache: true);
print("TIME: ${stopwatch.elapsedMilliseconds} ms");
print("Memory: ${(ProcessInfo.currentRss / 1024 / 1024).toStringAsFixed(2)} MB");
stopwatch.reset();
await filterEmojiDefinitions((emoji) => emoji.primaryName == "tired_face", cache: true);
print("TIME: ${stopwatch.elapsedMilliseconds} ms");
print("Memory: ${(ProcessInfo.currentRss / 1024 / 1024).toStringAsFixed(2)} MB");
}

View file

@ -1,11 +0,0 @@
library nyxx.interactivity;
import "dart:async";
import "package:nyxx/nyxx.dart";
part "src/utils/utils.dart";
part "src/utils/emojis.dart";
part "src/poll.dart";
part "src/pagination.dart";

View file

@ -1,75 +0,0 @@
part of nyxx.interactivity;
/// Handles pagination interactivity. Allows to create paginated messages from List<String>
/// Factory constructors allows to create message from String directly.
///
/// Pagination is sent by [paginate] method. And returns [Message] instance of sent message.
///
/// ```
/// var pagination = new Pagination(["This is simple paginated", "data. Use it if you", "want to partition text by yourself"], ctx,channel);
/// // It generated 2 equal (possibly) pages.
/// var paginatedMessage = new Pagination.fromStringEq("This is text for pagination", 2);
/// ```
class Pagination {
/// Pages of paginated message
List<String> pages;
/// Channel where message will be sent
MessageChannel channel;
/// Generates new pagination from List of Strings. Each list element is single page.
Pagination(this.pages, this.channel);
/// Generates pagination from String. It divides String into 250 char long pages.
factory Pagination.fromString(String str, MessageChannel channel) => Pagination(_Utils.split(str, 250).toList(), channel);
/// Generates pagination from String but with user specified size of single page.
factory Pagination.fromStringLen(String str, int len, MessageChannel channel) => Pagination(_Utils.split(str, len).toList(), channel);
/// Generates pagination from String but with user specified number of pages.
factory Pagination.fromStringEq(String str, int pieces, MessageChannel channel) => Pagination(_Utils.splitEqually(str, pieces).toList(), channel);
/// Paginates a list of Strings - each String is a different page.
Future<Message> paginate(Nyxx client, {Duration timeout = const Duration(minutes: 2)}) async {
final nextEmoji = EmojiUtils.getEmoji("arrow_forward")!;
final backEmoji = EmojiUtils.getEmoji("arrow_backward")!;
final firstEmoji = EmojiUtils.getEmoji("track_previous")!;
final lastEmoji = EmojiUtils.getEmoji("track_next")!;
final msg = await channel.send(content: pages[0]);
await msg.createReaction(firstEmoji);
await msg.createReaction(backEmoji);
await msg.createReaction(nextEmoji);
await msg.createReaction(lastEmoji);
await Future(() async {
var currPage = 0;
final group = _Utils.merge(
[client.onMessageReactionAdded, client.onMessageReactionsRemoved as Stream<MessageReactionEvent>]);
await for (final event in group) {
final emoji = (event as dynamic).emoji as UnicodeEmoji;
if (emoji == nextEmoji) {
if (currPage <= pages.length - 2) {
++currPage;
await msg.edit(content: pages[currPage]);
}
} else if (emoji == backEmoji) {
if (currPage >= 1) {
--currPage;
await msg.edit(content: pages[currPage]);
}
} else if (emoji == firstEmoji) {
await msg.edit(content: pages.first);
currPage = 0;
} else if (emoji == lastEmoji) {
await msg.edit(content: pages.last);
currPage = pages.length;
}
}
}).timeout(timeout);
return msg;
}
}

View file

@ -1,69 +0,0 @@
part of nyxx.interactivity;
/// Creates new poll, generates options and collects results. Returns `Map<Emoji, int` as result. [timeout] is set by default to 10 minutes
///
/// ```
/// Future<void> examplePoll(CommandContext ctx) async {
/// var results = createPoll(ctx.channel, "This is awesome poll", {UnicodeEmoji(""): "One option", UnicodeEmoji(""): "second option"});
/// }
/// ```
Future<Map<Emoji, int>> createPoll(TextChannel channel, String title, Map<Emoji, String> options,
{Duration timeout = const Duration(minutes: 10),
String? message,
bool delete = false,
dynamic Function(Map<Emoji, String> options, String message)? messageFactory}) async {
var toSend;
if (messageFactory == null) {
final buffer = StringBuffer();
buffer.writeln(title);
options.forEach((k, v) {
buffer.writeln("${k.format()} - $v");
});
if (message != null) buffer.writeln(message);
toSend = buffer.toString();
} else {
toSend = messageFactory(options, message!);
}
Message msg;
if (toSend is String) {
msg = await channel.send(content: toSend);
} else if (toSend is EmbedBuilder) {
msg = await channel.send(embed: toSend);
} else {
return Future.error("Cannot create poll");
}
for (final emoji in options.keys) {
await msg.createReaction(emoji);
}
final emojiCollection = <Emoji, int>{};
return Future<Map<Emoji, int>>(() async {
await for (final event in channel.client.onMessageReactionAdded.where((evnt) => evnt.message?.id == msg.id)) {
if (emojiCollection.containsKey(event.emoji)) {
// TODO: NNBD: weird stuff
var value = emojiCollection[event.emoji];
if (value != null) {
value += 1;
emojiCollection[event.emoji] = value;
}
} else {
emojiCollection[event.emoji] = 1;
}
}
return emojiCollection;
}).timeout(timeout, onTimeout: () async {
if (delete) await msg.delete();
return emojiCollection;
});
}

File diff suppressed because it is too large Load diff

38
nyxx/example/channel.dart Normal file
View file

@ -0,0 +1,38 @@
import "package:nyxx/nyxx.dart";
// Main function
void main() {
// Create new bot instance. Replace string with your token
final bot = Nyxx("<TOKEN>");
// Listen to ready event. Invoked when bot is connected to all shards. Note that cache can be empty or not incomplete.
bot.onReady.listen((ReadyEvent e) {
print("Ready!");
});
// Listen to all incoming messages
bot.onMessageReceived.listen((MessageReceivedEvent e) async {
// Check if message content equals "!embed"
if (e.message.content == "!create_channel") {
// Make sure that message was sent in guild not im dm, because we cant add roles in dms
if(e.message is! GuildMessage) {
return;
}
// Get guild object from message
final guild = (e.message as GuildMessage).guild!;
// Created text channel. Remember discord will lower the case of name and replace spaces with - and do other sanitization
final channel = await guild.createChannel("TEST CHANNEL", ChannelType.text) as GuildTextChannel;
// Send feedback
await e.message.channel.send(content: "Crated ${channel.mention}");
// Delete channel that we just created
await channel.delete();
// Send feedback
await e.message.channel.send(content: "Deleted ${channel.mention}");
}
});
}

View file

@ -0,0 +1,34 @@
import "package:nyxx/nyxx.dart";
// Main function
void main() {
// Create new bot instance. Replace string with your token
final bot = Nyxx("<TOKEN>");
// Listen to ready event. Invoked when bot is connected to all shards. Note that cache can be empty or not incomplete.
bot.onReady.listen((ReadyEvent e) {
print("Ready!");
});
// Listen to all incoming messages
bot.onMessageReceived.listen((MessageReceivedEvent e) async {
// Check if message content equals "!embed"
if (e.message.content == "!role") {
// Make sure that message was sent in guild not im dm, because we cant add roles in dms
if(e.message is! GuildMessage) {
return;
}
// Creating role with RoleBuilder. We have to cast `e.message` to GuildMessage because we want to access guild property
// and generic dont have that.
final role = await (e.message as GuildMessage).guild!.createRole(RoleBuilder("testRole")..color = DiscordColor.chartreuse);
// Cast message author to member because webhook can also be message author. And add role to user
await (e.message.author as CachelessMember).addRole(role);
// Send message with confirmation of given action
await e.message.channel.send(content: "Added [${role.name}] to user: [${e.message.author.tag}");
}
});
}

View file

@ -1,28 +1,26 @@
import 'package:nyxx/nyxx.dart';
//TODO: NNBD - Rewrite examples to be more idiomatic
import "package:nyxx/nyxx.dart";
// Main function
void main() {
// Create new bot instance
Nyxx bot = Nyxx("<TOKEN>");
// Create new bot instance. Replace string with your token
final bot = Nyxx("<TOKEN>");
// Listen to ready event. Invoked when bot started listening to events.
// Listen to ready event. Invoked when bot is connected to all shards. Note that cache can be empty or not incomplete.
bot.onReady.listen((ReadyEvent e) {
print("Ready!");
});
// Listen to all incoming messages via Dart Stream
// Listen to all incoming messages
bot.onMessageReceived.listen((MessageReceivedEvent e) {
// Check if message content equals "!embed"
if (e.message.content == "!embed") {
// Build embed with `..Builder` classes.
// Create embed with author and footer section.
var embed = EmbedBuilder()
final embed = EmbedBuilder()
..addField(name: "Example field title", content: "Example value")
..addField(builder: (field) {
field.content = "Hi";
field.name = "Example Filed";
field.name = "Example Field";
})
..addAuthor((author) {
author.name = e.message.author.username;
@ -31,9 +29,9 @@ void main() {
..addFooter((footer) {
footer.text = "Footer example, good";
})
..color = (e.message.author as Member).color;
..color = (e.message.author is CacheMember) ? (e.message.author as CacheMember).color : DiscordColor.black;
// Sent an embed
// Sent an embed to channel where message received was sent
e.message.channel.send(embed: embed);
}
});

29
nyxx/example/invite.dart Normal file
View file

@ -0,0 +1,29 @@
import "package:nyxx/nyxx.dart";
// Main function
void main() {
// Create new bot instance. Replace string with your token
final bot = Nyxx("<TOKEN>");
// Listen to ready event. Invoked when bot is connected to all shards. Note that cache can be empty or not incomplete.
bot.onReady.listen((ReadyEvent e) {
print("Ready!");
});
// Listen to all incoming messages
bot.onMessageReceived.listen((MessageReceivedEvent e) async {
// Check if message content equals "!embed"
if (e.message.content == "!create_channel") {
// Make sure that message was sent in guild not im dm, because we cant add roles in dms
if(e.message is! GuildMessage) {
return;
}
// Create default invite. We have to cast channel to access guild specific functionality.
final invite = await (e.message.channel as GuildTextChannel).createInvite();
// Send back invite url
await e.message.channel.send(content: invite.url);
}
});
}

View file

@ -1,56 +0,0 @@
import 'package:nyxx/nyxx.dart';
import 'dart:isolate';
import 'dart:io';
//TODO: NNBD - Rewrite examples to be more idiomatic
void setupBot(SendPort remotePort) {
/// Setup communication ports
var port = ReceivePort();
var sendPort = port.sendPort;
remotePort.send(sendPort);
// Create new bot instance
Nyxx bot = Nyxx("<TOKEN>");
// Listen to ready event. Invoked when bot started listening to events.
bot.onReady.listen((ReadyEvent e) {
print("Ready!");
});
// Listen to all incoming messages via Dart Stream
bot.onMessageReceived.listen((MessageReceivedEvent e) {
if (e.message.content == "!ping") {
e.message.channel.send(content: "Pong!");
}
});
port.listen((msg) async {
var exChannel = bot.channels[Snowflake("355365529369706509")] as TextChannel;
var m = msg.toString();
if (m.startsWith("SEND")) {
await exChannel.send(content: m.split(";").last);
}
});
}
// Main function
void main() async {
/// Create port
var recPort = ReceivePort();
/// spawn isolate
await Isolate.spawn(setupBot, recPort.sendPort);
var sendport = await recPort.first as SendPort;
/// Wait for user input
while (true) {
stdout.write("Send to channel >> ");
var msg = stdin.readLineSync();
sendport.send("SEND;$msg");
stdout.write("\n");
}
}

View file

@ -0,0 +1,61 @@
import "package:nyxx/nyxx.dart";
// Returns user that can be banned from message. Parses mention or raw id from message
SnowflakeEntity getUserToBan(GuildMessage message) {
// If mentions are not empty return first mention
if(message.mentions.isNotEmpty) {
return message.mentions.first;
}
// Otherwise split message by spaces then take lst part and parse it to snowflake and return as Snowflake entity
return SnowflakeEntity(message.content.split(" ").last.toSnowflake());
}
// Main function
void main() {
// Create new bot instance. Replace string with your token
final bot = Nyxx("<TOKEN>");
// Listen to ready event. Invoked when bot is connected to all shards. Note that cache can be empty or not incomplete.
bot.onReady.listen((ReadyEvent e) {
print("Ready!");
});
// Listen to all incoming messages
bot.onMessageReceived.listen((MessageReceivedEvent e) async {
// Check if message content equals "!embed"
if (e.message.content == "!ban") {
// Make sure that message was sent in guild not im dm, because we cant add roles in dms
if(e.message is! GuildMessage) {
return;
}
// Get user to ban
final userToBan = getUserToBan(e.message as GuildMessage);
// Ban user using variable initialized before
await (e.message as GuildMessage).guild!.ban(userToBan);
// Send feedback
await e.message.channel.send(content: "👍");
}
// Check if message content equals "!embed"
if (e.message.content == "!ban") {
// Make sure that message was sent in guild not im dm, because we cant add roles in dms
if(e.message is! GuildMessage) {
return;
}
// Get user to kick
final userToBan = getUserToBan(e.message as GuildMessage);
// Kick user
await (e.message as GuildMessage).guild!.kick(userToBan);
// Send feedback
await e.message.channel.send(content: "👍");
}
});
}

View file

@ -0,0 +1,42 @@
import "package:nyxx/nyxx.dart";
// Main function
void main() {
// Create new bot instance. Replace string with your token
final bot = Nyxx("<TOKEN>");
// Listen to ready event. Invoked when bot is connected to all shards. Note that cache can be empty or not incomplete.
bot.onReady.listen((ReadyEvent e) {
print("Ready!");
});
// Listen to all incoming messages
bot.onMessageReceived.listen((MessageReceivedEvent e) async {
// Check if message content equals "!embed"
if (e.message.content == "!addReadPerms") {
// Dont process message when not send in guild context
if(e.message is! GuildMessage) {
return;
}
// Get current channel
final messageChannel = e.message.channel as CacheGuildChannel;
// Get member from id
final member = await (e.message as GuildMessage).guild!.getMemberById(302359032612651009.toSnowflake());
// Get current member permissions in context of channel
final permissions = messageChannel.effectivePermissions(member as CacheMember);
// Get current member permissions as builder
final permissionsAsBuilder = permissions.toBuilder()..sendMessages = true;
// Get first channel override as builder and edit sendMessages property to allow sending messages for entities included in this override
final channelOverridesAsBuilder = messageChannel.permissionOverrides.first.toBuilder()..sendMessages = true;
// Create new channel permission override
await messageChannel.editChannelPermissions(PermissionsBuilder()..sendMessages = true, member);
}
});
}

View file

@ -1,20 +1,20 @@
import 'package:nyxx/nyxx.dart';
//TODO: NNBD - Rewrite examples to be more idiomatic
import "package:nyxx/nyxx.dart";
// Main function
void main() {
// Create new bot instance
Nyxx bot = Nyxx("<TOKEN>");
final bot = Nyxx("<TOKEN>");
// Listen to ready event. Invoked when bot started listening to events.
// Listen to ready event. Invoked when bot is connected to all shards. Note that cache can be empty or not incomplete.
bot.onReady.listen((e) {
print("Ready!");
});
// Listen to all incoming messages via Dart Stream
// Listen to all incoming messages
bot.onMessageReceived.listen((e) {
// Check if message content equals "!ping"
if (e.message.content == "!ping") {
// Send "Pong!" to channel where message was received
e.message.channel.send(content: "Pong!");
}
});

View file

@ -1,11 +1,11 @@
import 'package:nyxx/nyxx.dart';
import "dart:io";
import 'dart:io';
import "package:nyxx/nyxx.dart";
// Main function
void main() {
// Create new bot instance
Nyxx bot = Nyxx("<TOKEN>");
final bot = Nyxx("<TOKEN>");
// Listen to ready event. Invoked when bot started listening to events.
bot.onReady.listen((ReadyEvent e) {
@ -16,22 +16,23 @@ void main() {
bot.onMessageReceived.listen((MessageReceivedEvent e) {
// When receive specific message send new file to channel
if (e.message.content == "!give-me-file") {
// Send file via `sendFile()`. File path must be in list, so we have there `[]` syntax.
// First argument is path to file. When no additional arguments specified file is sent as is.
// File has to be in root project directory if path is relative.
// Files argument needs to be list of AttachmentBuilder object with
// path to file that you want to send. You can also use other
// AttachmentBuilder constructors to send File object or raw bytes
e.message.channel.send(files: [AttachmentBuilder.path("kitten.jpeg")]);
}
if (e.message.content == "!give-me-embed") {
// Check if message content equals "!givemeembed"
if (e.message.content == "!givemeembed") {
// Files can be used within embeds as custom images
var attachment = AttachmentBuilder.file(File("kitten.jpeg"));
final attachment = AttachmentBuilder.file(File("kitten.jpeg"));
// Use `attachUrl` property in embed to link uploaded file to thumbnail in that case
var embed = EmbedBuilder()
// use attachUrl getter from AttachmentBuildrer class to get reference to uploaded file
final embed = EmbedBuilder()
..title = "Example Title"
..thumbnailUrl = attachment.attachUrl;
// Sent all together
// Send everything we created before to channel where message was received.
e.message.channel.send(files: [attachment], embed: embed, content: "HEJKA!");
}
});

View file

@ -5,7 +5,6 @@ library nyxx;
import "dart:async";
import "dart:collection";
import "dart:convert";
import "dart:io";
@ -25,6 +24,15 @@ part "src/ClientOptions.dart";
// INTERNAL
part "src/internal/exceptions/MissingTokenError.dart";
part "src/internal/exceptions/EmbedBuilderArgumentException.dart";
part "src/internal/exceptions/InvalidShardException.dart";
part "src/internal/exceptions/InvalidSnowflakeException.dart";
part "src/internal/shard/Shard.dart";
part "src/internal/shard/ShardManager.dart";
part "src/internal/shard/shardHandler.dart";
part "src/internal/_Constants.dart";
part "src/internal/_EventController.dart";
part "src/internal/_WS.dart";
@ -44,10 +52,6 @@ part "src/internal/interfaces/Convertable.dart";
part "src/internal/interfaces/ISend.dart";
part "src/internal/interfaces/Mentionable.dart";
// ERROR
part "src/errors/SetupErrors.dart";
// EVENTS
part "src/events/MemberChunkEvent.dart";
@ -73,6 +77,7 @@ part "src/events/InviteEvents.dart";
part "src/utils/builders/Builder.dart";
part "src/utils/builders/PresenceBuilder.dart";
part "src/utils/builders/AttachmentBuilder.dart";
part "src/utils/builders/PermissionsBuilder.dart";
part "src/utils/builders/EmbedBuilder.dart";
@ -82,6 +87,8 @@ part "src/utils/builders/EmbedFooterBuilder.dart";
part "src/utils/builders/GuildBuilder.dart";
part "src/utils/builders/MessageBuilder.dart";
part "src/utils/extensions.dart";
// OBJECTS
part "src/core/AllowedMentions.dart";
@ -91,7 +98,6 @@ part "src/core/DiscordColor.dart";
part "src/core/SnowflakeEntity.dart";
part "src/core/Snowflake.dart";
part "src/Shard.dart";
part "src/core/guild/Webhook.dart";
part "src/core/voice/VoiceState.dart";
@ -111,6 +117,8 @@ part "src/core/channel/dm/GroupDMChannel.dart";
part "src/core/channel/dm/DMChannel.dart";
part "src/core/channel/Channel.dart";
part "src/core/channel/MessageChannel.dart";
part "src/core/channel/ITextChannel.dart";
part "src/core/channel/DummyTextChannel.dart";
part "src/core/embed/EmbedField.dart";
part "src/core/embed/EmbedAuthor.dart";
@ -122,6 +130,7 @@ part "src/core/embed/EmbedThumbnail.dart";
part "src/core/guild/ClientUser.dart";
part "src/core/guild/Guild.dart";
part "src/core/guild/GuildFeature.dart";
part "src/core/user/Presence.dart";
part "src/core/user/Member.dart";
part "src/core/guild/Status.dart";
@ -150,7 +159,6 @@ part "src/core/application/OAuth2Info.dart";
part "src/core/application/AppTeam.dart";
part "src/core/permissions/Permissions.dart";
part "src/core/permissions/AbstractPermissions.dart";
part "src/core/permissions/PermissionOverrides.dart";
part "src/core/permissions/PermissionsConstants.dart";

View file

@ -7,11 +7,8 @@ class ClientOptions {
/// **It means client won't send any of these. It doesn't mean filtering guild messages.**
AllowedMentions? allowedMentions;
/// The index of this shard
int shardIndex;
/// The total number of shards.
int shardCount;
int? shardCount;
/// The number of messages to cache for each channel.
int messageCacheSize;
@ -36,17 +33,28 @@ class ClientOptions {
/// If you do not specify a certain intent, you will not receive any of the gateway events that are batched into that group.
GatewayIntents? gatewayIntents;
/// Allows to receive compressed payloads from gateway
bool compressedGatewayPayloads;
/// Enables dispatching of guild subscription events (presence and typing events)
bool guildSubscriptions;
/// Initial bot presence
PresenceBuilder? initialPresence;
/// Makes a new `ClientOptions` object.
ClientOptions(
{this.allowedMentions,
this.shardIndex = 0,
this.shardCount = 1,
this.shardCount,
this.messageCacheSize = 400,
this.forceFetchMembers = false,
this.cacheMembers = true,
this.largeThreshold = 50,
this.ignoredEvents = const [],
this.gatewayIntents});
this.gatewayIntents,
this.compressedGatewayPayloads = true,
this.guildSubscriptions = true,
this.initialPresence });
}
/// When identifying to the gateway, you can specify an intents parameter which
@ -54,24 +62,57 @@ class ClientOptions {
/// If you do not specify a certain intent, you will not receive any of the gateway events that are batched into that group.
/// [Reference](https://discordapp.com/developers/docs/topics/gateway#gateway-intents)
class GatewayIntents {
/// Includes events: `GUILD_CREATE, GUILD_UPDATE, GUILD_DELETE, GUILD_ROLE_CREATE, GUILD_ROLE_UPDATE, GUILD_ROLE_DELETE, CHANNEL_DELETE, CHANNEL_CREATE, CHANNEL_UPDATE, CHANNEL_PINS_UPDATE`
bool guilds = false;
/// Includes events: `GUILD_MEMBER_ADD, GUILD_MEMBER_UPDATE, GUILD_MEMBER_REMOVE`
bool guildMembers = false;
/// Includes events: `GUILD_BAN_ADD, GUILD_BAN_REMOVE`
bool guildBans = false;
/// Includes event: `GUILD_EMOJIS_UPDATE`
bool guildEmojis = false;
/// Includes events: `GUILD_INTEGRATIONS_UPDATE`
bool guildIntegrations = false;
/// Includes events: `WEBHOOKS_UPDATE`
bool guildWebhooks = false;
/// Includes events: `INVITE_CREATE, INVITE_DELETE`
bool guildInvites = false;
/// Includes events: `VOICE_STATE_UPDATE`
bool guildVoiceState = false;
/// Includes events: `PRESENCE_UPDATE`
bool guildPresences = false;
/// Include events: `MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, MESSAGE_DELETE_BULK`
bool guildMessages = false;
/// Includes events: `MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI`
bool guildMessageReactions = false;
/// Includes events: `TYPING_START`
bool guildMessageTyping = false;
/// Includes events: `CHANNEL_CREATE, MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, CHANNEL_PINS_UPDATE`
bool directMessages = false;
/// Includes events: `MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI`
bool directMessageReactions = false;
/// Includes events: `TYPING_START`
bool directMessageTyping = false;
bool _all = false;
/// Constructs intens config object
GatewayIntents();
/// Return config with turned on all intents
GatewayIntents.all() : _all = true;
int _calculate() {
@ -88,8 +129,9 @@ class GatewayIntents {
if (guildIntegrations) value += 1 << 4;
if (guildWebhooks) value += 1 << 5;
if (guildInvites) value += 1 << 6;
if (guildVoiceState) value += 1 << 8;
if (guildPresences) value += 1 << 9;
if (guildVoiceState) value += 1 << 7;
if (guildPresences) value += 1 << 8;
if (guildMessages) value += 1 << 9;
if (guildMessageReactions) value += 1 << 10;
if (guildMessageTyping) value += 1 << 11;
if (directMessages) value += 1 << 12;

View file

@ -50,7 +50,7 @@ class Nyxx implements Disposable {
final String version = Constants.version;
/// Current client"s shard
late Shard shard;
late ShardManager shardManager;
/// Emitted when a shard is disconnected from the websocket.
late Stream<DisconnectEvent> onDisconnect;
@ -176,13 +176,33 @@ class Nyxx implements Disposable {
/// Creates and logs in a new client. If [ignoreExceptions] is true (by default is)
/// isolate will ignore all exceptions and continue to work.
Nyxx(this._token, {ClientOptions? options, bool ignoreExceptions = true}) {
Nyxx(this._token, {ClientOptions? options, bool ignoreExceptions = true, bool useDefaultLogger = true, Level? defaultLoggerLogLevel}) {
transport_vm.configureWTransportForVM();
if (_token.isEmpty) {
throw NoTokenError();
if(useDefaultLogger) {
Logger.root.level = defaultLoggerLogLevel ?? Level.ALL;
Logger.root.onRecord.listen((LogRecord rec) {
print("[${rec.time}] [${rec.level.name}] [${rec.loggerName}] ${rec.message}");
});
}
this._logger.info("Starting bot with pid: $pid");
if (_token.isEmpty) {
throw MissingTokenError();
}
if(!Platform.isWindows) {
ProcessSignal.sigterm.watch().forEach((event) async {
await this.dispose();
});
}
ProcessSignal.sigint.watch().forEach((event) async {
await this.dispose();
});
if (ignoreExceptions) {
Isolate.current.setErrorsFatal(false);
@ -203,7 +223,7 @@ class Nyxx implements Disposable {
this._events = _EventController(this);
this.onSelfMention = this
.onMessageReceived
.where((event) => event.message.mentions.any((mentionedUser) => mentionedUser == this.self));
.where((event) => event.message.mentions.contains(this.self));
this.onDmReceived = this
.onMessageReceived
.where((event) => event.message.channel is DMChannel || event.message.channel is GroupDMChannel);
@ -230,7 +250,9 @@ class Nyxx implements Disposable {
/// Returns guild with given [guildId]
Future<Guild> getGuild(Snowflake guildId, [bool useCache = true]) async {
if (this.guilds.hasKey(guildId) && useCache) return this.guilds[guildId]!;
if (this.guilds.hasKey(guildId) && useCache) {
return this.guilds[guildId]!;
}
final response = await _http._execute(BasicRequest._new("/guilds/$guildId"));
@ -248,7 +270,9 @@ class Nyxx implements Disposable {
/// var channel = await client.getChannel<TextChannel>(Snowflake("473853847115137024"));
/// ```
Future<T> getChannel<T extends Channel>(Snowflake id, [bool useCache = true]) async {
if (this.channels.hasKey(id) && useCache) return this.channels[id] as T;
if (this.channels.hasKey(id) && useCache) {
return this.channels[id] as T;
}
final response = await this._http._execute(BasicRequest._new("/channels/${id.toString()}"));
@ -257,25 +281,7 @@ class Nyxx implements Disposable {
}
final raw = (response as HttpResponseSuccess).jsonBody as Map<String, dynamic>;
switch (raw["type"] as int) {
case 1:
return DMChannel._new(raw, this) as T;
case 3:
return GroupDMChannel._new(raw, this) as T;
case 0:
case 5:
final guild = this.guilds[Snowflake(raw["guild_id"])];
return TextChannel._new(raw, guild!, this) as T;
case 2:
final guild = this.guilds[Snowflake(raw["guild_id"])];
return VoiceChannel._new(raw, guild!, this) as T;
case 4:
final guild = this.guilds[Snowflake(raw["guild_id"])];
return CategoryChannel._new(raw, guild!, this) as T;
default:
return Future.error("Cannot create channel of type [${raw["type"]}");
}
return Channel._deserialize(raw, this) as T;
}
/// Get user instance with specified id.
@ -286,7 +292,9 @@ class Nyxx implements Disposable {
/// var user = client.getUser(Snowflake("302359032612651009"));
/// ``
Future<User?> getUser(Snowflake id, [bool useCache = true]) async {
if (this.users.hasKey(id) && useCache) return this.users[id];
if (this.users.hasKey(id) && useCache) {
return this.users[id];
}
final response = await this._http._execute(BasicRequest._new("/users/${id.toString()}"));
@ -308,7 +316,7 @@ class Nyxx implements Disposable {
/// ```
Future<Guild> createGuild(GuildBuilder builder) async {
if (this.guilds.count >= 10) {
return Future.error("Guild cannot be created if bot is in 10 or more guilds");
return Future.error(ArgumentError("Guild cannot be created if bot is in 10 or more guilds"));
}
final response = await this._http._execute(BasicRequest._new("/guilds", method: "POST"));
@ -339,17 +347,17 @@ class Nyxx implements Disposable {
/// var inv = client.getInvite("YMgffU8");
/// ```
Future<Invite> getInvite(String code) async {
final r = await this._http._execute(BasicRequest._new("/invites/$code"));
final response = await this._http._execute(BasicRequest._new("/invites/$code"));
if (r is HttpResponseSuccess) {
return Invite._new(r.jsonBody as Map<String, dynamic>, this);
if (response is HttpResponseSuccess) {
return Invite._new(response.jsonBody as Map<String, dynamic>, this);
}
return Future.error(r);
return Future.error(response);
}
/// Returns number of shards
int get shards => this._options.shardCount;
int get shards => this.shardManager._shards.length;
/// Sets presence for bot.
///
@ -365,38 +373,21 @@ class Nyxx implements Disposable {
/// bot.setPresence(game: Activity.of("Super duper game", type: ActivityType.streaming, url: "https://twitch.tv/l7ssha"))
/// ```
/// `url` property in `Activity` can be only set when type is set to `streaming`
void setPresence({UserStatus? status, bool? afk, Activity? game, DateTime? since}) {
this.shard.setPresence(status: status, afk: afk, game: game, since: since);
void setPresence(PresenceBuilder presenceBuilder) {
this.shardManager.setPresence(presenceBuilder);
}
@override
Future<void> dispose() async {
await shard.dispose();
this._logger.info("Disposing and closing bot...");
await shardManager.dispose();
await this._events.dispose();
await guilds.dispose();
await users.dispose();
await guilds.dispose();
await this._events.dispose();
this._logger.info("Exiting...");
exit(0);
}
}
/// Sets up default logger
void setupDefaultLogging([Level? loglevel]) {
Logger.root.level = loglevel ?? Level.ALL;
Logger.root.onRecord.listen((LogRecord rec) {
var color = "";
if (rec.level == Level.WARNING) {
color = "\u001B[33m";
} else if (rec.level == Level.SEVERE) {
color = "\u001B[31m";
} else if (rec.level == Level.INFO) {
color = "\u001B[32m";
} else {
color = "\u001B[0m";
}
print("[${DateTime.now()}] "
"$color[${rec.level.name}] [${rec.loggerName}]\u001B[0m: "
"${rec.message}");
});
}
}

View file

@ -1,389 +0,0 @@
part of nyxx;
/// Discord gateways implement a method of user-controlled guild sharding which allows for splitting events across a number of gateway connections.
/// Guild sharding is entirely user controlled, and requires no state-sharing between separate connections to operate.
///
/// Shard is basically represents single websocket connection to gateway. Each shard can operate on up to 2500 guilds.
class Shard implements Disposable {
/// The shard id.
late final int id;
/// Whether or not the shard is ready.
bool connected = false;
/// Emitted when the shard is ready.
late Stream<Shard> onConnected;
/// Emitted when the shard encounters an error.
late Stream<Shard> onDisconnect;
/// Emitted when shard receives member chunk.
late Stream<MemberChunkEvent> onMemberChunk;
/// Number of events seen by shard
int get eventsSeen => _sequence;
final Logger _logger = Logger("Websocket");
late final _WS _ws;
bool _acked = false;
late Timer _heartbeatTimer;
transport.WebSocket? _socket;
StreamSubscription? _socketSubscription; // ignore: cancel_subscriptions
late int _sequence;
String? _sessionId;
late final StreamController<Shard> _onConnect;
late final StreamController<Shard> _onDisconnect;
late final StreamController<MemberChunkEvent> _onMemberChunk;
Shard._new(this._ws, this.id) {
this._onConnect = StreamController<Shard>.broadcast();
this.onConnected = this._onConnect.stream;
this._onDisconnect = StreamController<Shard>.broadcast();
this.onDisconnect = this._onDisconnect.stream;
this._onMemberChunk = StreamController.broadcast();
this.onMemberChunk = this._onMemberChunk.stream;
}
/// Allows to set presence for current shard.
void setPresence({UserStatus? status, bool? afk, Activity? game, DateTime? since}) {
final packet = <String, dynamic>{
"status": (status != null) ? status.toString() : UserStatus.online.toString(),
"afk": (afk != null) ? afk : false,
if (game != null)
"game": <String, dynamic>{
"name": game.name,
"type": game.type.value,
if (game.type == ActivityType.streaming) "url": game.url
},
"since": (since != null) ? since.millisecondsSinceEpoch : null
};
this.send(OPCodes.statusUpdate, packet);
}
/// Syncs all guilds
void guildSync() => this.send(OPCodes.guildSync, this._ws._client.guilds.keys.toList());
/// Sends WS data.
void send(int opCode, dynamic d) {
this._socket?.add(jsonEncode(<String, dynamic>{"op": opCode, "d": d}));
}
/// Allows to request members objects from gateway
/// [guild] can be either Snowflake or Iterable<Snowflake>
void requestMembers(/* Snowflake|Iterable<Snowflake> */ dynamic guild,
{String? query, Iterable<Snowflake>? userIds, int limit = 0, bool presences = false, String? nonce}) {
if (query != null && userIds != null) {
throw Exception("Both `query` and userIds cannot be specified.");
}
dynamic guildPayload;
if (guild is Snowflake) {
guildPayload = guild.toString();
} else if (guild is Iterable<Snowflake>) {
guildPayload = guild.map((e) => e.toString()).toList();
} else {
throw Exception("guild has to be either Snowflake or Iterable<Snowflake>");
}
final payload = <String, dynamic>{
"guild_id": guildPayload,
if (query != null) "query": query,
if (userIds != null) "user_ids": userIds.map((e) => e.toString()).toList(),
"limit": limit,
"presences": presences,
if (nonce != null) "nonce": nonce
};
this.send(OPCodes.requestGuildMember, payload);
}
// Attempts to connect to ws
void _connect([bool resume = false, bool init = false]) {
this.connected = false;
if (!init && resume) {
Future.delayed(const Duration(seconds: 3), () => _connect(true));
return;
}
transport.WebSocket.connect(Uri.parse("${this._ws.gateway}?v=6&encoding=json")).then((ws) {
_socket = ws;
this._socketSubscription = _socket!.listen((data) => this._handleMsg(_decodeBytes(data), resume),
onDone: this._handleErr, onError: (err) => this._handleErr);
}, onError: (_, __) => Future.delayed(const Duration(seconds: 6), () => this._connect()));
}
// Decodes zlib compresses string into string json
Map<String, dynamic> _decodeBytes(dynamic bytes) {
if (bytes is String) return jsonDecode(bytes) as Map<String, dynamic>;
final decoded = zlib.decoder.convert(bytes as List<int>);
final rawStr = utf8.decode(decoded);
return jsonDecode(rawStr) as Map<String, dynamic>;
}
void _heartbeat() {
if (this._socket?.closeCode != null) return;
if (!this._acked) _logger.warning("No ACK received");
this.send(OPCodes.heartbeat, _sequence);
this._acked = false;
}
Future<void> _handleMsg(Map<String, dynamic> msg, bool resume) async {
if (msg["op"] == OPCodes.dispatch && this._ws._client._options.ignoredEvents.contains(msg["t"] as String)) {
return;
}
if (msg["s"] != null) this._sequence = msg["s"] as int;
switch (msg["op"] as int) {
case OPCodes.heartbeatAck:
this._acked = true;
break;
case OPCodes.hello:
if (this._sessionId == null || !resume) {
final identifyMsg = <String, dynamic>{
"token": _ws._client._token,
"properties": <String, dynamic>{
"\$os": Platform.operatingSystem,
"\$browser": "nyxx",
"\$device": "nyxx",
},
"large_threshold": this._ws._client._options.largeThreshold,
"compress": true
};
if (_ws._client._options.gatewayIntents != null) {
identifyMsg["intents"] = _ws._client._options.gatewayIntents!._calculate();
}
identifyMsg["shard"] = <int>[this.id, _ws._client._options.shardCount];
this.send(OPCodes.identify, identifyMsg);
} else if (resume) {
this.send(OPCodes.resume,
<String, dynamic>{"token": _ws._client._token, "session_id": this._sessionId, "seq": this._sequence});
}
this._heartbeatTimer = Timer.periodic(
Duration(milliseconds: msg["d"]["heartbeat_interval"] as int), (Timer t) => this._heartbeat());
break;
case OPCodes.invalidSession:
_logger.severe("Invalid session. Reconnecting...");
_heartbeatTimer.cancel();
_ws._client._events.onDisconnect.add(DisconnectEvent._new(this, 9));
this._onDisconnect.add(this);
if (msg["d"] as bool) {
Future.delayed(const Duration(seconds: 3), () => _connect(true));
} else {
Future.delayed(const Duration(seconds: 6), _connect);
}
break;
case OPCodes.dispatch:
final j = msg["t"] as String;
switch (j) {
case "READY":
this._sessionId = msg["d"]["session_id"] as String;
_ws._client.self = ClientUser._new(msg["d"]["user"] as Map<String, dynamic>, _ws._client);
this.connected = true;
_logger.info("Shard connected");
this._onConnect.add(this);
if (!resume) {
await _ws.propagateReady();
}
break;
case "GUILD_MEMBERS_CHUNK":
this._onMemberChunk.add(MemberChunkEvent._new(msg, _ws._client));
break;
case "MESSAGE_REACTION_REMOVE_ALL":
_ws._client._events.onMessageReactionsRemoved.add(MessageReactionsRemovedEvent._new(msg, _ws._client));
break;
case "MESSAGE_REACTION_ADD":
MessageReactionAddedEvent._new(msg, _ws._client);
break;
case "MESSAGE_REACTION_REMOVE":
MessageReactionRemovedEvent._new(msg, _ws._client);
break;
case "MESSAGE_DELETE_BULK":
_ws._client._events.onMessageDeleteBulk.add(MessageDeleteBulkEvent._new(msg, _ws._client));
break;
case "CHANNEL_PINS_UPDATE":
_ws._client._events.onChannelPinsUpdate.add(ChannelPinsUpdateEvent._new(msg, _ws._client));
break;
case "VOICE_STATE_UPDATE":
_ws._client._events.onVoiceStateUpdate.add(VoiceStateUpdateEvent._new(msg, _ws._client));
break;
case "VOICE_SERVER_UPDATE":
_ws._client._events.onVoiceServerUpdate.add(VoiceServerUpdateEvent._new(msg, _ws._client));
break;
case "GUILD_EMOJIS_UPDATE":
_ws._client._events.onGuildEmojisUpdate.add(GuildEmojisUpdateEvent._new(msg, _ws._client));
break;
case "MESSAGE_CREATE":
_ws._client._events.onMessageReceived.add(MessageReceivedEvent._new(msg, _ws._client));
break;
case "MESSAGE_DELETE":
_ws._client._events.onMessageDelete.add(MessageDeleteEvent._new(msg, _ws._client));
break;
case "MESSAGE_UPDATE":
_ws._client._events.onMessageUpdate.add(MessageUpdateEvent._new(msg, _ws._client));
break;
case "GUILD_CREATE":
_ws._client._events.onGuildCreate.add(GuildCreateEvent._new(msg, this, _ws._client));
break;
case "GUILD_UPDATE":
_ws._client._events.onGuildUpdate.add(GuildUpdateEvent._new(msg, _ws._client));
break;
case "GUILD_DELETE":
_ws._client._events.onGuildDelete.add(GuildDeleteEvent._new(msg, this, _ws._client));
break;
case "GUILD_BAN_ADD":
_ws._client._events.onGuildBanAdd.add(GuildBanAddEvent._new(msg, _ws._client));
break;
case "GUILD_BAN_REMOVE":
_ws._client._events.onGuildBanRemove.add(GuildBanRemoveEvent._new(msg, _ws._client));
break;
case "GUILD_MEMBER_ADD":
_ws._client._events.onGuildMemberAdd.add(GuildMemberAddEvent._new(msg, _ws._client));
break;
case "GUILD_MEMBER_REMOVE":
_ws._client._events.onGuildMemberRemove.add(GuildMemberRemoveEvent._new(msg, _ws._client));
break;
case "GUILD_MEMBER_UPDATE":
_ws._client._events.onGuildMemberUpdate.add(GuildMemberUpdateEvent._new(msg, _ws._client));
break;
case "CHANNEL_CREATE":
_ws._client._events.onChannelCreate.add(ChannelCreateEvent._new(msg, _ws._client));
break;
case "CHANNEL_UPDATE":
_ws._client._events.onChannelUpdate.add(ChannelUpdateEvent._new(msg, _ws._client));
break;
case "CHANNEL_DELETE":
_ws._client._events.onChannelDelete.add(ChannelDeleteEvent._new(msg, _ws._client));
break;
case "TYPING_START":
_ws._client._events.onTyping.add(TypingEvent._new(msg, _ws._client));
break;
case "PRESENCE_UPDATE":
_ws._client._events.onPresenceUpdate.add(PresenceUpdateEvent._new(msg, _ws._client));
break;
case "GUILD_ROLE_CREATE":
_ws._client._events.onRoleCreate.add(RoleCreateEvent._new(msg, _ws._client));
break;
case "GUILD_ROLE_UPDATE":
_ws._client._events.onRoleUpdate.add(RoleUpdateEvent._new(msg, _ws._client));
break;
case "GUILD_ROLE_DELETE":
_ws._client._events.onRoleDelete.add(RoleDeleteEvent._new(msg, _ws._client));
break;
case "USER_UPDATE":
_ws._client._events.onUserUpdate.add(UserUpdateEvent._new(msg, _ws._client));
break;
case "INVITE_CREATE":
_ws._client._events.onInviteCreated.add(InviteCreatedEvent._new(msg, _ws._client));
break;
case "INVITE_DELETE":
_ws._client._events.onInviteDelete.add(InviteDeletedEvent._new(msg, _ws._client));
break;
case "MESSAGE_REACTION_REMOVE_EMOJI":
_ws._client._events.onMessageReactionRemoveEmoji
.add(MessageReactionRemoveEmojiEvent._new(msg, _ws._client));
break;
default:
print("UNKNOWN OPCODE: ${jsonEncode(msg)}");
}
break;
}
}
void _handleErr() {
this._heartbeatTimer.cancel();
_logger.severe(
"Shard disconnected. Error code: [${this._socket?.closeCode}] | Error message: [${this._socket?.closeReason}]");
this.dispose();
switch (this._socket?.closeCode) {
case 4004:
case 4010:
exit(1);
break;
case 4013:
_logger.shout("Cannot connect to gateway due intent value is invalid. "
"Check https://discordapp.com/developers/docs/topics/gateway#gateway-intents for more info.");
exit(1);
break;
case 4014:
_logger.shout("You sent a disallowed intent for a Gateway Intent. "
"You may have tried to specify an intent that you have not enabled or are not whitelisted for. "
"Check https://discordapp.com/developers/docs/topics/gateway#gateway-intents for more info.");
exit(1);
break;
case 4007:
case 4009:
Future.delayed(const Duration(seconds: 3), () => this._connect(true));
break;
default:
Future.delayed(const Duration(seconds: 6), () => _connect(false, true));
break;
}
_ws._client._events.onDisconnect.add(DisconnectEvent._new(this, this._socket?.closeCode!));
this._onDisconnect.add(this);
}
@override
Future<void> dispose() async {
await this._socketSubscription?.cancel();
await this._socket?.close(1000);
this._socket = null;
}
}

View file

@ -61,7 +61,7 @@ class AllowedMentions implements Builder {
if (_users.isNotEmpty) {
if (!_allowUsers) {
throw Exception(
throw ArgumentError(
"Invalid configuration of allowed mentions! Allowed `user` and blacklisted users at the same time!");
}
@ -70,7 +70,7 @@ class AllowedMentions implements Builder {
if (_roles.isNotEmpty) {
if (!_allowRoles) {
throw Exception(
throw ArgumentError(
"Invalid configuration of allowed mentions! Allowed `roles` and blacklisted roles at the same time!");
}

View file

@ -1,7 +1,9 @@
part of nyxx;
// All colors got from DiscordColor class from DSharp+.
// https://github.com/DSharpPlus/DSharpPlus/blob/a2f6eca7f5f675e83748b20b957ae8bdb8fd0cab/DSharpPlus/Entities/DiscordColor.Colors.cs
/// Wrapper for colors.
///
/// Simplifies creation and provides interface to interact with colors for nyxx.
class DiscordColor {
late final int _value;
@ -63,9 +65,6 @@ class DiscordColor {
@override
bool operator ==(other) => other is DiscordColor && other._value == this._value;
// All colors got from DiscordColor class from DSharp+.
// https://github.com/DSharpPlus/DSharpPlus/blob/a2f6eca7f5f675e83748b20b957ae8bdb8fd0cab/DSharpPlus/Entities/DiscordColor.Colors.cs
/// Color of null, literally null.
static const DiscordColor? none = null;

View file

@ -4,4 +4,7 @@ part of nyxx;
abstract class GuildEntity {
/// Reference to [Guild] object
Guild? get guild;
/// Id of [Guild]
Snowflake get guildId;
}

View file

@ -11,9 +11,6 @@ class Invite {
/// A mini channel object for the invite's channel.
late final Channel? channel;
/// Returns url invite
String get url => "https://discord.gg/$code";
/// User who created this invite
late final User? inviter;
@ -21,7 +18,10 @@ class Invite {
late final User? targetUser;
/// Reference to bot instance
Nyxx client;
final Nyxx client;
/// Returns url to invite
String get url => "https://discord.gg/$code";
Invite._new(Map<String, dynamic> raw, this.client) {
this.code = raw["code"] as String;
@ -30,6 +30,7 @@ class Invite {
this.guild = client.guilds[Snowflake(raw["guild"]["id"])];
}
// TODO: NNBD
if (raw["channel"] != null) {
this.channel = client.channels[Snowflake(raw["channel"]["id"])];
}
@ -41,7 +42,7 @@ class Invite {
if (raw["inviter"] != null) {
this.inviter = client.users[Snowflake(raw["inviter"]["id"])];
}
if (raw["target_user"] != null) {
this.targetUser = client.users[Snowflake(raw["target_user"]["id"])];
}

View file

@ -23,7 +23,11 @@ class Snowflake implements Comparable<Snowflake> {
if (id is int) {
_id = id;
} else {
_id = int.parse(id.toString());
try {
_id = int.parse(id.toString());
} on FormatException {
throw InvalidSnowflakeException._new(id);
}
}
}

View file

@ -1,19 +1,26 @@
part of nyxx;
class AppTeam extends SnowflakeEntity {
/// Hash of team icon
late final String? iconHash;
/// Id of Team owner
late final Snowflake ownerId;
/// List of members of team
late final List<AppTeamMember> members;
/// Returns instance of [AppTeamMember] of team owner
AppTeamMember get ownerMember => this.members.firstWhere((element) => element.user.id == this.ownerId);
AppTeam._new(Map<String, dynamic> raw) : super(Snowflake(raw["id"])) {
this.iconHash = raw["icon"] as String?;
this.ownerId = Snowflake(raw["owner_user_id"]);
this.members = [];
for (Map<String, dynamic> obj in raw["members"]) {
this.members.add(AppTeamMember._new(obj));
}
this.members = [
for (final rawMember in raw["members"])
AppTeamMember._new(rawMember as Map<String, dynamic>)
];
}
/// Returns url to team icon
@ -26,9 +33,8 @@ class AppTeam extends SnowflakeEntity {
}
}
/// Represent membership of user in [Team]
/// Represent membership of user in [AppTeam]
class AppTeamMember {
/// Basic information of user
late final AppTeamUser user;

View file

@ -14,20 +14,8 @@ class AuditLog {
late final Map<Snowflake, AuditLogEntry> entries;
/// Filters audit log by [users]
Iterable<AuditLogEntry> filterByUsers(List<User> users) =>
entries.values.where((entry) => users.contains(entry.user));
/// Filter audit log entries by type of change
Iterable<AuditLogEntry> filterByChangeType(List<ChangeKeyType> changeType) =>
entries.values.where((entry) => entry.changes.any((t) => changeType.contains(t.key)));
/// Filter audit log by type of entry
Iterable<AuditLogEntry> filterByEntryType(List<AuditLogEntryType> entryType) =>
entries.values.where((entry) => entryType.contains(entry.type));
/// Filter audit log by id of target
Iterable<AuditLogEntry> filterByTargetId(List<Snowflake> targetId) =>
entries.values.where((entry) => targetId.contains(entry.targetId));
Iterable<AuditLogEntry> filter(bool Function(AuditLogEntry) test) =>
entries.values.where(test);
AuditLog._new(Map<String, dynamic> raw, Nyxx client) {
webhooks = {};

View file

@ -23,54 +23,53 @@ class AuditLogEntry extends SnowflakeEntity {
String? reason;
AuditLogEntry._new(Map<String, dynamic> raw, Nyxx client) : super(Snowflake(raw["id"] as String)) {
targetId = raw["targetId"] as String;
this.targetId = raw["targetId"] as String;
changes = [
this.changes = [
if (raw["changes"] != null)
for (var o in raw["changes"]) AuditLogChange._new(o as Map<String, dynamic>)
];
user = client.users[Snowflake(raw["user_id"])];
type = AuditLogEntryType(raw["action_type"] as int);
this.user = client.users[Snowflake(raw["user_id"])];
this.type = AuditLogEntryType._create(raw["action_type"] as int);
if (raw["options"] != null) {
options = raw["options"] as String;
this.options = raw["options"] as String;
}
reason = raw["reason"] as String;
this.reason = raw["reason"] as String;
}
}
class AuditLogEntryType extends IEnum<int> {
static const AuditLogEntryType guildUpdate = AuditLogEntryType._of(1);
static const AuditLogEntryType channelCreate = AuditLogEntryType._of(10);
static const AuditLogEntryType channelUpdate = AuditLogEntryType._of(11);
static const AuditLogEntryType channelDelete = AuditLogEntryType._of(12);
static const AuditLogEntryType channelOverwriteCreate = AuditLogEntryType._of(13);
static const AuditLogEntryType channelOverwriteUpdate = AuditLogEntryType._of(14);
static const AuditLogEntryType channelOverwriteDelete = AuditLogEntryType._of(15);
static const AuditLogEntryType memberKick = AuditLogEntryType._of(20);
static const AuditLogEntryType memberPrune = AuditLogEntryType._of(21);
static const AuditLogEntryType memberBanAdd = AuditLogEntryType._of(22);
static const AuditLogEntryType memberBanRemove = AuditLogEntryType._of(23);
static const AuditLogEntryType memberUpdate = AuditLogEntryType._of(24);
static const AuditLogEntryType memberRoleUpdate = AuditLogEntryType._of(25);
static const AuditLogEntryType roleCreate = AuditLogEntryType._of(30);
static const AuditLogEntryType roleUpdate = AuditLogEntryType._of(31);
static const AuditLogEntryType roleDelete = AuditLogEntryType._of(32);
static const AuditLogEntryType inviteCreate = AuditLogEntryType._of(40);
static const AuditLogEntryType inviteUpdate = AuditLogEntryType._of(41);
static const AuditLogEntryType inviteDelete = AuditLogEntryType._of(42);
static const AuditLogEntryType webhookCreate = AuditLogEntryType._of(50);
static const AuditLogEntryType webhookUpdate = AuditLogEntryType._of(51);
static const AuditLogEntryType webhookDelete = AuditLogEntryType._of(52);
static const AuditLogEntryType emojiCreate = AuditLogEntryType._of(60);
static const AuditLogEntryType emojiUpdate = AuditLogEntryType._of(61);
static const AuditLogEntryType emojiDelete = AuditLogEntryType._of(62);
static const AuditLogEntryType messageDelete = AuditLogEntryType._of(72);
static const AuditLogEntryType guildUpdate = AuditLogEntryType._create(1);
static const AuditLogEntryType channelCreate = AuditLogEntryType._create(10);
static const AuditLogEntryType channelUpdate = AuditLogEntryType._create(11);
static const AuditLogEntryType channelDelete = AuditLogEntryType._create(12);
static const AuditLogEntryType channelOverwriteCreate = AuditLogEntryType._create(13);
static const AuditLogEntryType channelOverwriteUpdate = AuditLogEntryType._create(14);
static const AuditLogEntryType channelOverwriteDelete = AuditLogEntryType._create(15);
static const AuditLogEntryType memberKick = AuditLogEntryType._create(20);
static const AuditLogEntryType memberPrune = AuditLogEntryType._create(21);
static const AuditLogEntryType memberBanAdd = AuditLogEntryType._create(22);
static const AuditLogEntryType memberBanRemove = AuditLogEntryType._create(23);
static const AuditLogEntryType memberUpdate = AuditLogEntryType._create(24);
static const AuditLogEntryType memberRoleUpdate = AuditLogEntryType._create(25);
static const AuditLogEntryType roleCreate = AuditLogEntryType._create(30);
static const AuditLogEntryType roleUpdate = AuditLogEntryType._create(31);
static const AuditLogEntryType roleDelete = AuditLogEntryType._create(32);
static const AuditLogEntryType inviteCreate = AuditLogEntryType._create(40);
static const AuditLogEntryType inviteUpdate = AuditLogEntryType._create(41);
static const AuditLogEntryType inviteDelete = AuditLogEntryType._create(42);
static const AuditLogEntryType webhookCreate = AuditLogEntryType._create(50);
static const AuditLogEntryType webhookUpdate = AuditLogEntryType._create(51);
static const AuditLogEntryType webhookDelete = AuditLogEntryType._create(52);
static const AuditLogEntryType emojiCreate = AuditLogEntryType._create(60);
static const AuditLogEntryType emojiUpdate = AuditLogEntryType._create(61);
static const AuditLogEntryType emojiDelete = AuditLogEntryType._create(62);
static const AuditLogEntryType messageDelete = AuditLogEntryType._create(72);
const AuditLogEntryType._of(int value) : super(value);
AuditLogEntryType(int value) : super(value);
const AuditLogEntryType._create(int value) : super(value);
@override
bool operator ==(other) {

View file

@ -2,42 +2,55 @@ part of nyxx;
/// A channel.
/// Abstract base class that defines the base methods and/or properties for all Discord channel types.
abstract class Channel extends SnowflakeEntity {
class Channel extends SnowflakeEntity {
/// The channel's type.
/// https://discordapp.com/developers/docs/resources/channel#channel-object-channel-types
ChannelType type;
final ChannelType type;
/// Reference to client instance
Nyxx client;
final Nyxx client;
Channel._new(Map<String, dynamic> raw, int type, this.client)
: this.type = ChannelType._create(type),
super(Snowflake(raw["id"] as String));
super(Snowflake(raw["id"]));
factory Channel._deserialize(Map<String, dynamic> raw, Nyxx client) {
final type = raw["d"]["type"] as int;
factory Channel._deserialize(Map<String, dynamic> raw, Nyxx client, [Guild? guild]) {
final type = raw["type"] as int;
final guild = raw["d"]["guild_id"] != null ? client.guilds[Snowflake(raw["d"]["guild_id"])] : null;
Guild? channelGuild;
if(guild != null) {
channelGuild = guild;
} else if(raw["guild_id"] != null) {
channelGuild = client.guilds[Snowflake(raw["guild_id"])];
}
switch (type) {
case 1:
return DMChannel._new(raw["d"] as Map<String, dynamic>, client);
return DMChannel._new(raw, client);
break;
case 3:
return GroupDMChannel._new(raw["d"] as Map<String, dynamic>, client);
return GroupDMChannel._new(raw, client);
break;
case 0:
case 5:
return TextChannel._new(raw["d"] as Map<String, dynamic>, guild!, client);
if(channelGuild == null) {
return CachelessTextChannel._new(raw, Snowflake(raw["guild_id"]), client);
}
return CacheTextChannel._new(raw, channelGuild, client);
break;
case 2:
return VoiceChannel._new(raw["d"] as Map<String, dynamic>, guild!, client);
if(channelGuild == null) {
return CachelessVoiceChannel._new(raw, Snowflake(raw["guild_id"]), client);
}
return CacheVoiceChannel._new(raw, channelGuild, client);
break;
case 4:
return CategoryChannel._new(raw["d"] as Map<String, dynamic>, guild!, client);
return CategoryChannel._new(raw, channelGuild == null ? Snowflake(raw["guild_id"]) : channelGuild.id, client);
break;
default:
return _InternalChannel._new(raw["d"] as Map<String, dynamic>, type, client);
return _InternalChannel._new(raw, type, client);
}
}

View file

@ -0,0 +1,5 @@
part of nyxx;
class DummyTextChannel extends Channel with MessageChannel, ISend implements ITextChannel {
DummyTextChannel._new(Map<String, dynamic> raw, int type, Nyxx client) : super._new(raw, type, client);
}

View file

@ -0,0 +1,81 @@
part of nyxx;
/// Represents text channel. Can be either [CachelessTextChannel] or [DMChannel] or [GroupDMChannel]
abstract class ITextChannel implements Channel, MessageChannel {
/// Returns message with given [id]. Allows to force fetch message from api
/// with [ignoreCache] property. By default it checks if message is in cache and fetches from api if not.
@override
Future<Message?> getMessage(Snowflake id, {bool ignoreCache = false});
/// Sends message to channel. Performs `toString()` on thing passed to [content]. Allows to send embeds with [embed] field.
///
/// ```
/// await channel.send(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()}");
/// ```
/// Embeds can be sent very easily:
/// ```
/// var embed = EmbedBuilder()
/// ..title = "Example Title"
/// ..addField(name: "Memory usage", value: "${ProcessInfo.currentRss / 1024 / 1024}MB");
///
/// await chan.send(embed: embed);
/// ```
///
/// Method also allows to send file and optional [content] with [embed].
/// Use `expandAttachment(String file)` method to expand file names in embed
///
/// ```
/// await channel.send(files: [new File("kitten.png"), new File("kitten.jpg")], content: "Kittens ^-^"]);
/// ```
/// ```
/// var embed = new nyxx.EmbedBuilder()
/// ..title = "Example Title"
/// ..thumbnailUrl = "${attach("kitten.jpg")}";
///
/// channel.send(files: [new File("kitten.jpg")], embed: embed, content: "HEJKA!");
/// ```
@override
Future<Message> send(
{dynamic content,
List<AttachmentBuilder>? files,
EmbedBuilder? embed,
bool? tts,
AllowedMentions? allowedMentions,
MessageBuilder? builder});
/// Starts typing.
@override
Future<void> startTyping();
/// Loops `startTyping` until `stopTypingLoop` is called.
@override
void startTypingLoop();
/// Stops a typing loop if one is running.
@override
void stopTypingLoop();
/// Bulk removes many messages by its ids. [messagesIds] is list of messages ids to delete.
///
/// ```
/// var toDelete = chan.messages.keys.take(5);
/// await chan.bulkRemoveMessages(toDelete);
/// ```
@override
Future<void> bulkRemoveMessages(Iterable<Message> messagesIds);
/// Gets several [Message] objects from API. Only one of [after], [before], [around] can be specified,
/// otherwise, it will throw.
///
/// ```
/// var messages = await chan.getMessages(limit: 100, after: Snowflake("222078108977594368"));
/// ```
@override
Stream<Message> getMessages({int limit = 50, Snowflake? after, Snowflake? before, Snowflake? around});
}

View file

@ -1,6 +1,6 @@
part of nyxx;
/// Provides abstraction of messages for [TextChannel], [DMChannel] and [GroupDMChannel].
/// Provides abstraction of messages for [CachelessTextChannel], [DMChannel] and [GroupDMChannel].
/// Implements iterator which allows to use message object in for loops to access
/// messages sequentially.
///
@ -11,34 +11,35 @@ part of nyxx;
/// print(message.author.id);
/// }
/// ```
class MessageChannel extends Channel with IterableMixin<Message>, ISend, Disposable {
Timer? _typing;
abstract class MessageChannel implements Channel, ISend, Disposable {
/// Reference to client
@override
Nyxx get client;
/// Sent when a new message is received.
late final Stream<MessageReceivedEvent> onMessage;
late final Stream<MessageReceivedEvent> onMessage = client.onMessageReceived.where((event) => event.message.channel == this);
/// Emitted when user starts typing.
late final Stream<TypingEvent> onTyping;
late final Stream<TypingEvent> onTyping = client.onTyping.where((event) => event.channel == this);
/// Emitted when channel pins are updated.
late final Stream<ChannelPinsUpdateEvent> pinsUpdated = client.onChannelPinsUpdate.where((event) => event.channel == this);
/// A collection of messages sent to this channel.
late final MessageCache messages;
late final MessageCache messages = MessageCache._new(client._options.messageCacheSize);
/// File upload limit for channel
// Used to create infinite typing loop
Timer? _typing;
/// File upload limit for channel in bytes. If channel is [CachelessGuildChannel] returns default value.
int get fileUploadLimit {
if (this is GuildChannel) {
return (this as GuildChannel).guild.fileUploadLimit;
if (this is CacheGuildChannel) {
return (this as CacheGuildChannel).guild.fileUploadLimit;
}
return 8 * 1024 * 1024;
}
MessageChannel._new(Map<String, dynamic> raw, int type, Nyxx client) : super._new(raw, type, client) {
this.messages = MessageCache._new(client._options.messageCacheSize);
onTyping = client.onTyping.where((event) => event.channel == this);
onMessage = client.onMessageReceived.where((event) => event.message.channel == this);
}
/// Returns message with given [id]. Allows to force fetch message from api
/// with [ignoreCache] property. By default it checks if message is in cache and fetches from api if not.
Future<Message?> getMessage(Snowflake id, {bool ignoreCache = false}) async {
@ -48,16 +49,14 @@ class MessageChannel extends Channel with IterableMixin<Message>, ISend, Disposa
if (response is HttpResponseError) {
return Future.error(response);
}
var msg = Message._deserialize((response as HttpResponseSuccess).jsonBody as Map<String, dynamic>, client);
return messages._cacheMessage(msg);
return messages._cacheMessage(
Message._deserialize((response as HttpResponseSuccess).jsonBody as Map<String, dynamic>, client));
}
return messages[id];
}
@override
/// Sends message to channel. Performs `toString()` on thing passed to [content]. Allows to send embeds with [embed] field.
///
/// ```
@ -93,6 +92,7 @@ class MessageChannel extends Channel with IterableMixin<Message>, ISend, Disposa
/// await e.message.channel
/// .send(files: [new File("kitten.jpg")], embed: embed, content: "HEJKA!");
/// ```
@override
Future<Message> send(
{dynamic content,
List<AttachmentBuilder>? files,
@ -121,7 +121,7 @@ class MessageChannel extends Channel with IterableMixin<Message>, ISend, Disposa
if (files != null && files.isNotEmpty) {
for (final file in files) {
if (file._bytes.length > fileUploadLimit) {
return Future.error("File with name: [${file._name}] is too big!");
return Future.error(ArgumentError("File with name: [${file._name}] is too big!"));
}
}
@ -134,9 +134,9 @@ class MessageChannel extends Channel with IterableMixin<Message>, ISend, Disposa
if (response is HttpResponseSuccess) {
return Message._deserialize(response.jsonBody as Map<String, dynamic>, client);
} else {
return Future.error(response);
}
return Future.error(response);
}
/// Starts typing.
@ -191,7 +191,7 @@ class MessageChannel extends Channel with IterableMixin<Message>, ISend, Disposa
}
}
@override
/// Returns iterator for messages cache
Iterator<Message> get iterator => messages.values.iterator;
@override

View file

@ -1,7 +1,7 @@
part of nyxx;
/// Represents channel with another user.
class DMChannel extends MessageChannel {
class DMChannel extends Channel with MessageChannel, ISend implements ITextChannel {
/// The recipient.
late User recipient;

View file

@ -1,7 +1,7 @@
part of nyxx;
/// Represents group DM channel.
class GroupDMChannel extends MessageChannel {
class GroupDMChannel extends Channel with MessageChannel, ISend implements ITextChannel {
/// The recipients of channel.
late final List<User> recipients;

View file

@ -1,8 +1,6 @@
part of nyxx;
/// Represents guild group channel.
class CategoryChannel extends Channel with GuildChannel {
CategoryChannel._new(Map<String, dynamic> raw, Guild guild, Nyxx client) : super._new(raw, 4, client) {
_initialize(raw, guild);
}
class CategoryChannel extends CachelessGuildChannel {
CategoryChannel._new(Map<String, dynamic> raw, Snowflake guildId, Nyxx client) : super._new(raw, 4, guildId, client);
}

View file

@ -1,57 +1,208 @@
part of nyxx;
/// Represents channel which is part of guild.
mixin GuildChannel implements Channel, GuildEntity {
/// Can be represented by [CachelessGuildChannel] or [CacheGuildChannel]
abstract class IGuildChannel extends Channel {
/// The channel"s name.
late String name;
String get name;
/// Relative position of channel in context of channel list
int get position;
/// Id of [Guild] that the channel is in.
Snowflake get guildId;
/// Id of parent channel
Snowflake? get parentChannelId;
/// Indicates if channel is nsfw
bool get isNsfw;
/// Returns list of [CacheMember] objects who can see this channel
List<PermissionsOverrides> get permissionOverrides;
IGuildChannel._new(Map<String, dynamic> raw, int type, Nyxx client) : super._new(raw, type, client);
/// Fetches and returns all channel"s [Invite]s
///
/// ```
/// var invites = await chan.getChannelInvites();
/// ```
Stream<InviteWithMeta> getChannelInvites();
/// Allows to set or edit permissions for channel. [id] can be either User or Role
/// Throws if [id] isn't [User] or [Role]
Future<void> editChannelPermissions(PermissionsBuilder perms, SnowflakeEntity id, {String? auditReason});
/// Allows to edit or set channel permission overrides.
Future<void> editChannelPermissionOverrides(PermissionOverrideBuilder permissionBuilder, {String? auditReason});
/// Deletes permission overwrite for given User or Role [id]
/// Throws if [id] isn't [User] or [Role]
Future<void> deleteChannelPermission(SnowflakeEntity id, {String? auditReason});
/// Creates new [Invite] for [Channel] and returns it"s instance
///
/// ```
/// var invite = await channel.createInvite(maxUses: 2137);
/// ```
Future<Invite> createInvite({int? maxAge, int? maxUses, bool? temporary, bool? unique, String? auditReason});
}
/// Guild channel which does not have access to cache.
abstract class CachelessGuildChannel extends IGuildChannel {
/// The channel"s name.
@override
late final String name;
/// The channel's position in the channel list.
@override
late final int position;
/// Id of [Guild] that the channel is in.
@override
final Snowflake guildId;
/// Id of parent channel
@override
late final Snowflake? parentChannelId;
/// Indicates if channel is nsfw
@override
late final bool isNsfw;
/// Returns list of [CacheMember] objects who can see this channel
@override
late final List<PermissionsOverrides> permissionOverrides;
CachelessGuildChannel._new(Map<String, dynamic> raw, int type, this.guildId, Nyxx client) : super._new(raw, type, client) {
this.name = raw["name"] as String;
this.position = raw["position"] as int;
this.parentChannelId = raw["parent_id"] != null ? Snowflake(raw["parent_id"]) : null;
this.isNsfw = raw["nsfw"] as bool? ?? false;
this.permissionOverrides = [
if (raw["permission_overwrites"] != null)
for (var obj in raw["permission_overwrites"])
PermissionsOverrides._new(obj as Map<String, dynamic>)
];
}
/// Fetches and returns all channel"s [Invite]s
///
/// ```
/// var invites = await chan.getChannelInvites();
/// ```
@override
Stream<InviteWithMeta> getChannelInvites() async* {
final response = await client._http._execute(BasicRequest._new("/channels/$id/invites"));
if (response is HttpResponseError) {
yield* Stream.error(response);
}
final bodyValues = (response as HttpResponseSuccess).jsonBody.values.first;
for (final val in bodyValues as Iterable<Map<String, dynamic>>) {
yield InviteWithMeta._new(val, client);
}
}
/// Allows to set permissions for channel. [entity] can be either [User] or [Role]
/// Throws if [entity] isn't [User] or [Role]
@override
Future<void> editChannelPermissions(PermissionsBuilder perms, SnowflakeEntity entity, {String? auditReason}) async {
if (entity is! IRole && entity is! User) {
return Future.error(ArgumentError("The `id` property must be either Role or User"));
}
final permSet = perms._build();
await client._http._execute(BasicRequest._new("/channels/${this.id}/permissions/${entity.id.toString()}",
method: "PUT", body: {
"type" : entity is IRole ? "role" : "member",
"allow" : permSet.allow,
"deny" : permSet.deny
}, auditLog: auditReason));
}
@override
/// Allows to edit or set channel permission overrides.
Future<void> editChannelPermissionOverrides(PermissionOverrideBuilder permissionBuilder, {String? auditReason}) async {
final permSet = permissionBuilder._build();
await client._http._execute(BasicRequest._new("/channels/${this.id}/permissions/${permissionBuilder.id.toString()}",
method: "PUT", body: {
"type" : permissionBuilder.type,
"allow" : permSet.allow,
"deny" : permSet.deny
}, auditLog: auditReason));
}
/// Deletes permission overwrite for given User or Role [id]
/// Throws if [id] isn't [User] or [Role]
@override
Future<void> deleteChannelPermission(SnowflakeEntity id, {String? auditReason}) async {
if (id is! Role && id is! User) {
throw ArgumentError("`id` property must be either Role or User");
}
await client._http
._execute(BasicRequest._new("/channels/${this.id}/permissions/$id", method: "PUT", auditLog: auditReason));
}
/// Creates new [Invite] for [Channel] and returns it's instance
///
/// ```
/// final invite = await channel.createInvite(maxUses: 2137);
/// ```
@override
Future<Invite> createInvite({int? maxAge, int? maxUses, bool? temporary, bool? unique, String? auditReason}) async {
final body = {
if (maxAge != null) "max_age": maxAge,
if (maxUses != null) "max_uses": maxUses,
if (temporary != null) "temporary": temporary,
if (unique != null) "unique": unique,
};
final response = await client._http
._execute(BasicRequest._new("/channels/$id/invites", method: "POST", body: body, auditLog: auditReason));
if (response is HttpResponseError) {
return Future.error(response);
}
return InviteWithMeta._new((response as HttpResponseSuccess).jsonBody as Map<String, dynamic>, client);
}
}
/// Guild channel which does have access to cache.
abstract class CacheGuildChannel extends CachelessGuildChannel {
/// The guild that the channel is in.
late Guild guild;
/// The channel"s position in the channel list.
late int position;
/// Parent channel id
CategoryChannel? parentChannel;
/// Indicates if channel is nsfw
late bool nsfw;
/// Returns list of [CacheMember] objects who can see this channel
Iterable<IMember> get users => this.guild.members.values.where((member) => member is CacheMember && this.effectivePermissions(member).hasPermission(PermissionsConstants.viewChannel));
/// Permissions overwrites for channel.
late final List<PermissionsOverrides> permissions;
/// Returns list of [Member] objects who can see this channel
Iterable<Member> get users => this
.guild
.members
.values
.where((member) => this.effectivePermissions(member).hasPermission(PermissionsConstants.viewChannel));
// Initializes Guild channel
void _initialize(Map<String, dynamic> raw, Guild guild) {
this.name = raw["name"] as String;
this.position = raw["position"] as int;
this.guild = guild;
if (raw["parent_id"] != null) {
this.parentChannel = client.channels[Snowflake(raw["parent_id"])] as CategoryChannel?;
CacheGuildChannel._new(Map<String, dynamic> raw, int type, this.guild, Nyxx client) : super._new(raw, type, guild.id, client) {
if(this.parentChannelId != null) {
this.parentChannel = guild.channels[this.parentChannelId!] as CategoryChannel?;
}
this.nsfw = raw["nsfw"] as bool? ?? false;
this.permissions = [
if (raw["permission_overwrites"] != null)
for (var obj in raw["permission_overwrites"]) PermissionsOverrides._new(obj as Map<String, dynamic>)
];
}
/// Returns effective permissions for [member] to this channel including channel overrides.
Permissions effectivePermissions(Member member) {
if (member.guild != this.guild) return Permissions.empty();
Permissions effectivePermissions(CacheMember member) {
if (member.guild != this.guild) {
return Permissions.empty();
}
if (member.guild.owner == member) return Permissions.fromInt(PermissionsConstants.allPermissions);
if (member.guild.owner == member) {
return Permissions.fromInt(PermissionsConstants.allPermissions);
}
var rawMemberPerms = member.effectivePermissions.raw;
@ -69,91 +220,29 @@ mixin GuildChannel implements Channel, GuildEntity {
/// Returns effective permissions for [role] to this channel including channel overrides.
Permissions effectivePermissionForRole(Role role) {
if (role.guild != this.guild) return Permissions.empty();
if (role.guild != this.guild) {
return Permissions.empty();
}
var permissions = role.permissions.raw | guild.everyoneRole.permissions.raw;
var permissions = role.permissions.raw | (guild.everyoneRole as Role).permissions.raw;
// TODO: NNBD: try-catch in where
try {
final overEveryone = this.permissions.firstWhere((f) => f.id == guild.everyoneRole.id);
final overEveryone = this.permissionOverrides.firstWhere((f) => f.id == guild.everyoneRole.id);
permissions &= ~overEveryone.deny;
permissions |= overEveryone.allow;
// ignore: avoid_catches_without_on_clauses, empty_catches
} on Error {}
// ignore: avoid_catches_without_on_clauses, empty_catches
} on Exception {}
try {
final overRole = this.permissions.firstWhere((f) => f.id == role.id);
final overRole = this.permissionOverrides.firstWhere((f) => f.id == role.id);
permissions &= ~overRole.deny;
permissions |= overRole.allow;
// ignore: avoid_catches_without_on_clauses, empty_catches
} on Error {}
// ignore: avoid_catches_without_on_clauses, empty_catches
} on Exception {}
return Permissions.fromInt(permissions);
}
/// Creates new [Invite] for [Channel] and returns it"s instance
///
/// ```
/// var inv = await chan.createInvite(maxUses: 2137);
/// ```
Future<Invite> createInvite({int? maxAge, int? maxUses, bool? temporary, bool? unique, String? auditReason}) async {
final body = {
if (maxAge != null) "max_age": maxAge,
if (maxAge != null) "max_uses": maxUses,
if (maxAge != null) "temporary": temporary,
if (maxAge != null) "unique": unique,
};
final response = await client._http
._execute(BasicRequest._new("/channels/$id/invites", method: "POST", body: body, auditLog: auditReason));
if (response is HttpResponseError) {
return Future.error(response);
}
return InviteWithMeta._new((response as HttpResponseSuccess).jsonBody as Map<String, dynamic>, client);
}
/// Fetches and returns all channel"s [Invite]s
///
/// ```
/// var invites = await chan.getChannelInvites();
/// ```
Stream<InviteWithMeta> getChannelInvites() async* {
final response = await client._http._execute(BasicRequest._new("/channels/$id/invites"));
if (response is HttpResponseError) {
yield* Stream.error(response);
}
final bodyValues = (response as HttpResponseSuccess).jsonBody.values.first;
for (final val in bodyValues as Iterable<Map<String, dynamic>>) {
yield InviteWithMeta._new(val, client);
}
}
/// Allows to set permissions for channel. [id] can be either User or Role
/// Throws if [id] isn't [User] or [Role]
Future<void> editChannelPermission(PermissionsBuilder perms, SnowflakeEntity id, {String? auditReason}) {
if (id is! Role || id is! User) {
throw Exception("The `id` property must be either Role or User");
}
return client._http._execute(BasicRequest._new("/channels/${this.id}/permissions/${id.toString()}",
method: "PUT", body: perms._build()._build(), auditLog: auditReason));
}
/// Deletes permission overwrite for given User or Role [id]
/// Throws if [id] isn't [User] or [Role]
Future<void> deleteChannelPermission(SnowflakeEntity id, {String? auditReason}) async {
if (id is! Role || id is! User) {
throw Exception("`id` property must be either Role or User");
}
return client._http
._execute(BasicRequest._new("/channels/${this.id}/permissions/$id", method: "PUT", auditLog: auditReason));
}
}

View file

@ -1,15 +1,9 @@
part of nyxx;
/// [TextChannel] represents single text channel on [Guild].
/// Inhertits from [MessageChannel] and mixes [GuildChannel].
class TextChannel extends MessageChannel with GuildChannel implements Mentionable {
/// Emitted when channel pins are updated.
late final Stream<ChannelPinsUpdateEvent> pinsUpdated;
/// [ITextChannel] in context of [Guild].
abstract class GuildTextChannel implements Channel, CachelessGuildChannel, ITextChannel {
/// The channel's topic.
String? topic;
@override
late final String? topic;
/// The channel's mention string.
String get mention => "<#${this.id}>";
@ -18,31 +12,28 @@ class TextChannel extends MessageChannel with GuildChannel implements Mentionabl
late final int slowModeThreshold;
/// Returns url to this channel.
String get url => "https://discordapp.com/channels/${this.guild.id.toString()}"
String get url => "https://discordapp.com/channels/${this.guildId.toString()}"
"/${this.id.toString()}";
TextChannel._new(Map<String, dynamic> raw, Guild guild, Nyxx client) : super._new(raw, 0, client) {
_initialize(raw, guild);
/* Constructor????? */
void _initialize(Map<String, dynamic> raw, Snowflake guildId, Nyxx client) {
this.topic = raw["topic"] as String?;
this.slowModeThreshold = raw["rate_limit_per_user"] as int? ?? 0;
pinsUpdated = client.onChannelPinsUpdate.where((event) => event.channel == this);
}
/// Edits the channel.
Future<TextChannel> edit({String? name, String? topic, int? position, int? slowModeTreshold}) async {
Future<GuildTextChannel> edit({String? name, String? topic, int? position, int? slowModeThreshold}) async {
final body = <String, dynamic>{
if (name != null) "name": name,
if (topic != null) "topic": topic,
if (position != null) "position": position,
if (slowModeTreshold != null) "rate_limit_per_user": slowModeTreshold,
if (slowModeThreshold != null) "rate_limit_per_user": slowModeThreshold,
};
final response = await client._http._execute(BasicRequest._new("/channels/${this.id}", method: "PATCH", body: body));
if (response is HttpResponseSuccess) {
return TextChannel._new(response.jsonBody as Map<String, dynamic>, this.guild, client);
return CachelessTextChannel._new(response.jsonBody as Map<String, dynamic>, guildId, client);
}
return Future.error(response);
@ -65,11 +56,11 @@ class TextChannel extends MessageChannel with GuildChannel implements Mentionabl
/// Valid file types for [avatarFile] are jpeg, gif and png.
///
/// ```
/// var webhook = await chan.createWebhook("!a Send nudes kek6407");
/// final webhook = await channnel.createWebhook("!a Send nudes kek6407");
/// ```
Future<Webhook> createWebhook(String name, {File? avatarFile, String auditReason = ""}) async {
Future<Webhook> createWebhook(String name, {File? avatarFile, String? auditReason}) async {
if (name.isEmpty || name.length > 80) {
return Future.error("Webhook's name cannot be shorter than 1 character and longer than 80 characters");
return Future.error(ArgumentError("Webhook name cannot be shorter than 1 character and longer than 80 characters"));
}
final body = <String, dynamic>{"name": name};
@ -91,7 +82,7 @@ class TextChannel extends MessageChannel with GuildChannel implements Mentionabl
return Future.error(response);
}
/// Returns pinned [Message]s for [Channel].
/// Returns pinned [Message]s for channel.
Stream<Message> getPinnedMessages() async* {
final response = await client._http._execute(BasicRequest._new("/channels/$id/pins"));
@ -109,3 +100,21 @@ class TextChannel extends MessageChannel with GuildChannel implements Mentionabl
/// Returns mention to channel
String toString() => this.mention;
}
/// [CachelessTextChannel] represents single text channel on [Guild].
/// Inhertits from [MessageChannel] and mixes [CacheGuildChannel].
class CachelessTextChannel extends CachelessGuildChannel with GuildTextChannel, MessageChannel, ISend implements Mentionable, ITextChannel {
CachelessTextChannel._new(Map<String, dynamic> raw, Snowflake guildId, Nyxx client) : super._new(raw, 0, guildId, client) {
_initialize(raw, guildId, client);
}
}
/// [CachelessTextChannel] represents single text channel on [Guild].
/// Inhertits from [MessageChannel] and mixes [CacheGuildChannel].
class CacheTextChannel extends CacheGuildChannel with GuildTextChannel, MessageChannel, ISend implements Mentionable, ITextChannel {
CacheTextChannel._new(Map<String, dynamic> raw, Guild guild, Nyxx client) : super._new(raw, 0, guild, client) {
_initialize(raw, guild.id, client);
}
}

View file

@ -1,24 +1,17 @@
part of nyxx;
/// Represents VoiceChannel within [Guild]
class VoiceChannel extends Channel with GuildChannel {
abstract class VoiceChannel implements Channel {
/// The channel's bitrate.
int? bitrate;
late final int? bitrate;
/// The channel's user limit.
int? userLimit;
VoiceChannel._new(Map<String, dynamic> raw, Guild guild, Nyxx client) : super._new(raw, 2, client) {
_initialize(raw, guild);
late final int? userLimit;
void _initialize(Map<String, dynamic> raw) {
this.bitrate = raw["bitrate"] as int?;
this.userLimit = raw["user_limit"] as int?;
}
/// Allows to get [VoiceState]s of users connected to this channel
Iterable<VoiceState> get connectedUsers => this.guild.voiceStates.values.where((e) => e.channel?.id == this.id);
/// Edits the channel.
Future<VoiceChannel> edit({String? name, int? bitrate, int? position, int? userLimit, String? auditReason}) async {
final body = <String, dynamic>{
if (name != null) "name": name,
@ -31,9 +24,51 @@ class VoiceChannel extends Channel with GuildChannel {
._execute(BasicRequest._new("/channels/${this.id}", method: "PATCH", body: body, auditLog: auditReason));
if (response is HttpResponseSuccess) {
return VoiceChannel._new(response.jsonBody as Map<String, dynamic>, this.guild, client);
if(this is CacheGuildChannel) {
return CacheVoiceChannel._new(response.jsonBody as Map<String, dynamic>, (this as CacheGuildChannel).guild, client);
}
return CachelessVoiceChannel._new(response.jsonBody as Map<String, dynamic>, (this as CachelessVoiceChannel).guildId, client);
}
return Future.error(response);
}
}
class CachelessVoiceChannel extends CachelessGuildChannel with VoiceChannel {
CachelessVoiceChannel._new(Map<String, dynamic> raw, Snowflake guildId, Nyxx client) : super._new(raw, 2, guildId, client) {
_initialize(raw);
}
/// Connects client to channel
void connect({bool selfMute = false, bool selfDeafen = false}) {
try {
final shard = this.client.shardManager.shards.firstWhere((element) => element.guilds.contains(this.guildId));
shard.changeVoiceState(this.guildId, this.id, selfMute: selfMute, selfDeafen: selfDeafen);
} on Error {
throw InvalidShardException._new("Cannot find shard for this channel!");
}
}
/// Disconnects use from channel.
void disconnect() {
try {
final shard = this.client.shardManager.shards.firstWhere((element) => element.guilds.contains(this.guildId));
shard.changeVoiceState(this.guildId, null);
} on Error {
throw InvalidShardException._new("Cannot find shard for this channel!");
}
}
}
/// Represents VoiceChannel within [Guild]
class CacheVoiceChannel extends CacheGuildChannel with VoiceChannel {
/// Allows to get [VoiceState]s of users connected to this channel
Iterable<VoiceState> get connectedUsers => this.guild.voiceStates.values.where((e) => e.channel?.id == this.id);
CacheVoiceChannel._new(Map<String, dynamic> raw, Guild guild, Nyxx client) : super._new(raw, 2, guild, client) {
_initialize(raw);
}
}

View file

@ -129,7 +129,7 @@ class Embed implements Convertable<EmbedBuilder> {
..thumbnailUrl = this.thumbnail?.url
..imageUrl = this.image?.url
..author = this.author?.toBuilder()
.._fields = this.fields.map((field) => field.toBuilder()).toList();
..fields = this.fields.map((field) => field.toBuilder()).toList();
@override
bool operator ==(other) => other is EmbedVideo ? other.url == this.url : false;

View file

@ -13,9 +13,9 @@ class ClientUser extends User {
this.mfa = data["mfa_enabled"] as bool;
}
/// Allows to get [Member] objects for all guilds for bot user.
Map<Guild, Member> getMembership() {
final membershipCollection = <Guild, Member>{};
/// Allows to get [CacheMember] objects for all guilds for bot user.
Map<Guild, IMember> getMembership() {
final membershipCollection = <Guild, IMember>{};
for (final guild in client.guilds.values) {
final member = guild.members[this.id];
@ -31,7 +31,7 @@ class ClientUser extends User {
/// Edits current user. This changes user's username - not per guild nickname.
Future<User> edit({String? username, File? avatar, String? encodedAvatar}) async {
if (username == null && (avatar == null || encodedAvatar == null)) {
return Future.error("Cannot edit user with null values");
return Future.error(ArgumentError("Cannot edit user with null null arguments"));
}
final body = <String, dynamic>{

View file

@ -5,7 +5,7 @@ part of nyxx;
///
/// ---------
///
/// [channels] property is Map of [Channel]s but it can be cast to specific Channel subclasses. Example with getting all [TextChannel]s in [Guild]:
/// [channels] property is Map of [Channel]s but it can be cast to specific Channel subclasses. Example with getting all [CachelessTextChannel]s in [Guild]:
/// ```
/// var textChannels = channels.where((channel) => channel is MessageChannel) as List<TextChannel>;
/// ```
@ -27,19 +27,19 @@ class Guild extends SnowflakeEntity implements Disposable {
late String? discoverySplash;
/// System channel where system messages are sent
late final TextChannel? systemChannel;
late final CachelessTextChannel? systemChannel;
/// enabled guild features
late final List<String> features;
late final Iterable<GuildFeature> features;
/// The guild's afk channel ID, null if not set.
late VoiceChannel? afkChannel;
/// The guild's voice region.
late String region;
/// The channel ID for the guild's widget if enabled.
late final GuildChannel? embedChannel;
late final CacheGuildChannel? embedChannel;
/// The guild's AFK timeout.
late final int afkTimeout;
@ -63,22 +63,22 @@ class Guild extends SnowflakeEntity implements Disposable {
late final int systemChannelFlags;
/// Channel where "PUBLIC" guilds display rules and/or guidelines
late final GuildChannel? rulesChannel;
late final IGuildChannel? rulesChannel;
/// The guild owner's ID
late final User? owner;
/// The guild's members.
late final Cache<Snowflake, Member> members;
late final Cache<Snowflake, IMember> members;
/// The guild's channels.
late final ChannelCache channels;
/// The guild's roles.
late final Cache<Snowflake, Role> roles;
late final Cache<Snowflake, IRole> roles;
/// Guild custom emojis
late final Cache<Snowflake, GuildEmoji> emojis;
late final Cache<Snowflake, IGuildEmoji> emojis;
/// Boost level of guild
late final PremiumTier premiumTier;
@ -92,7 +92,7 @@ class Guild extends SnowflakeEntity implements Disposable {
/// the id of the channel where admins and moderators
/// of "PUBLIC" guilds receive notices from Discord
late final GuildChannel? publicUpdatesChannel;
late final IGuildChannel? publicUpdatesChannel;
/// Permission of current(bot) user in this guild
Permissions? currentUserPermissions;
@ -104,12 +104,12 @@ class Guild extends SnowflakeEntity implements Disposable {
String get url => "https://discordapp.com/channels/${this.id.toString()}";
/// Getter for @everyone role
Role get everyoneRole => roles.values.firstWhere((r) => r.name == "@everyone");
IRole get everyoneRole => roles.values.firstWhere((r) => (r as Role).name == "@everyone");
/// Returns member object for bot user
Member? get selfMember => members[client.self.id];
IMember? get selfMember => members[client.self.id];
/// Upload limit for this guild in bytes
/// File upload limit for channel in bytes.
int get fileUploadLimit {
const megabyte = 1024 * 1024;
@ -124,6 +124,9 @@ class Guild extends SnowflakeEntity implements Disposable {
return 8 * megabyte;
}
/// Returns this guilds shard
Shard get shard => client.shardManager.shards.firstWhere((_shard) => _shard.guilds.contains(this.id));
Guild._new(this.client, Map<String, dynamic> raw, [this.available = true, bool guildCreate = false])
: super(Snowflake(raw["id"] as String)) {
if (!this.available) return;
@ -145,7 +148,7 @@ class Guild extends SnowflakeEntity implements Disposable {
if (raw["roles"] != null) {
this.roles = _SnowflakeCache<Role>();
raw["roles"].forEach((o) {
final role = Role._new(o as Map<String, dynamic>, this, client);
final role = Role._new(o as Map<String, dynamic>, this.id, client);
this.roles[role.id] = role;
});
}
@ -153,23 +156,23 @@ class Guild extends SnowflakeEntity implements Disposable {
this.emojis = _SnowflakeCache();
if (raw["emojis"] != null) {
raw["emojis"].forEach((dynamic o) {
final emoji = GuildEmoji._new(o as Map<String, dynamic>, this, client);
final emoji = GuildEmoji._new(o as Map<String, dynamic>, this.id, client);
this.emojis[emoji.id] = emoji;
});
}
if (raw.containsKey("embed_channel_id")) {
this.embedChannel = client.channels[Snowflake(raw["embed_channel_id"])] as GuildChannel;
this.embedChannel = client.channels[Snowflake(raw["embed_channel_id"])] as CacheGuildChannel;
}
if (raw["system_channel_id"] != null) {
final id = Snowflake(raw["system_channel_id"] as String);
if (this.channels.hasKey(id)) {
this.systemChannel = this.channels[id] as TextChannel;
this.systemChannel = this.channels[id] as CachelessTextChannel;
}
}
this.features = (raw["features"] as List<dynamic>).cast<String>();
this.features = (raw["features"] as List<dynamic>).map((e) => GuildFeature.from(e.toString()));
if (raw["permissions"] != null) {
this.currentUserPermissions = Permissions.fromInt(raw["permissions"] as int);
@ -178,7 +181,7 @@ class Guild extends SnowflakeEntity implements Disposable {
if (raw["afk_channel_id"] != null) {
final id = Snowflake(raw["afk_channel_id"] as String);
if (this.channels.hasKey(id)) {
this.afkChannel = this.channels[id] as VoiceChannel;
this.afkChannel = this.channels[id] as CacheVoiceChannel;
}
}
@ -192,15 +195,7 @@ class Guild extends SnowflakeEntity implements Disposable {
if (!guildCreate) return;
raw["channels"].forEach((o) {
late GuildChannel channel;
if (o["type"] == 0 || o["type"] == 5 || o["type"] == 6) {
channel = TextChannel._new(o as Map<String, dynamic>, this, client);
} else if (o["type"] == 2) {
channel = VoiceChannel._new(o as Map<String, dynamic>, this, client);
} else if (o["type"] == 4) {
channel = CategoryChannel._new(o as Map<String, dynamic>, this, client);
}
final channel = Channel._deserialize(o as Map<String, dynamic>, this.client, this);
this.channels[channel.id] = channel;
client.channels[channel.id] = channel;
@ -208,7 +203,7 @@ class Guild extends SnowflakeEntity implements Disposable {
if (client._options.cacheMembers) {
raw["members"].forEach((o) {
final member = Member._standard(o as Map<String, dynamic>, this, client);
final member = CacheMember._standard(o as Map<String, dynamic>, this, client);
this.members[member.id] = member;
client.users[member.id] = member;
});
@ -237,11 +232,11 @@ class Guild extends SnowflakeEntity implements Disposable {
}
if (raw["rules_channel_id"] != null) {
this.rulesChannel = this.channels[Snowflake(raw["rules_channel_id"])] as GuildChannel?;
this.rulesChannel = this.channels[Snowflake(raw["rules_channel_id"])] as IGuildChannel;
}
if (raw["public_updates_channel_id"] != null) {
this.publicUpdatesChannel = this.channels[Snowflake(raw["public_updates_channel_id"])] as GuildChannel?;
this.publicUpdatesChannel = this.channels[Snowflake(raw["public_updates_channel_id"])] as IGuildChannel?;
}
}
@ -293,7 +288,7 @@ class Guild extends SnowflakeEntity implements Disposable {
final response = await client._http._execute(BasicRequest._new("/guilds/$id/emojis/${emojiId.toString()}"));
if (response is HttpResponseSuccess) {
return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, this, client);
return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, this.id, client);
}
return Future.error(response);
@ -308,11 +303,11 @@ class Guild extends SnowflakeEntity implements Disposable {
/// ```
Future<GuildEmoji> createEmoji(String name, {List<Role>? roles, File? image, List<int>? imageBytes}) async {
if (image != null && await image.length() > 256000) {
return Future.error("Emojis and animated emojis have a maximum file size of 256kb.");
return Future.error(ArgumentError("Emojis and animated emojis have a maximum file size of 256kb."));
}
if (image == null && imageBytes == null) {
return Future.error("Both imageData and file fields cannot be null");
return Future.error(ArgumentError("Both imageData and file fields cannot be null"));
}
final body = <String, dynamic>{
@ -325,7 +320,7 @@ class Guild extends SnowflakeEntity implements Disposable {
._execute(BasicRequest._new("/guilds/${this.id.toString()}/emojis", method: "POST", body: body));
if (response is HttpResponseSuccess) {
return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, this, client);
return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, this.id, client);
}
return Future.error(response);
@ -351,8 +346,8 @@ class Guild extends SnowflakeEntity implements Disposable {
final response = await client._http._execute(BasicRequest._new("/guilds/$id/prune",
method: "POST",
auditLog: auditReason,
queryParams: {
"days": days.toString(),
queryParams: { "days": days.toString() },
body: {
if (includeRoles != null) "include_roles": includeRoles.map((e) => e.id.toString())
}));
@ -393,7 +388,7 @@ class Guild extends SnowflakeEntity implements Disposable {
}
/// Change guild owner.
Future<Guild> changeOwner(Member member, {String? auditReason}) async {
Future<Guild> changeOwner(CacheMember member, {String? auditReason}) async {
final response = await client._http._execute(
BasicRequest._new("/guilds/$id", method: "PATCH", auditLog: auditReason, body: {"owner_id": member.id}));
@ -410,10 +405,10 @@ class Guild extends SnowflakeEntity implements Disposable {
/// Creates invite in first channel possible
Future<Invite> createInvite({int maxAge = 0, int maxUses = 0, bool temporary = false, bool unique = false, String? auditReason}) async {
final channel = this.channels.first as GuildChannel?;
final channel = this.channels.first as CacheGuildChannel?;
if (channel == null) {
return Future.error("Cannot get any channel to create invite to");
return Future.error(ArgumentError("Cannot get any channel to create invite to"));
}
return channel.createInvite(
@ -494,13 +489,13 @@ class Guild extends SnowflakeEntity implements Disposable {
BasicRequest._new("/guilds/$id/roles", method: "POST", auditLog: auditReason, body: roleBuilder._build()));
if (response is HttpResponseSuccess) {
return Role._new(response.jsonBody as Map<String, dynamic>, this, client);
return Role._new(response.jsonBody as Map<String, dynamic>, this.id, client);
}
return Future.error(response);
}
/// Adds [Role] to [Member]
/// Adds [Role] to [CacheMember]
///
/// ```
/// var role = guild.roles.values.first;
@ -508,10 +503,10 @@ class Guild extends SnowflakeEntity implements Disposable {
///
/// await guild.addRoleToMember(member, role);
/// ```
Future<void> addRoleToMember(Member user, Role role) async =>
Future<void> addRoleToMember(CacheMember user, Role role) async =>
client._http._execute(BasicRequest._new("/guilds/$id/members/${user.id}/roles/${role.id}", method: "PUT"));
/// Returns list of available [VoiceChannel]s
/// Returns list of available [CacheVoiceChannel]s
Stream<VoiceRegion> getVoiceRegions() async* {
final response = await client._http._execute(BasicRequest._new("/guilds/$id/regions"));
@ -530,7 +525,7 @@ class Guild extends SnowflakeEntity implements Disposable {
/// ```
/// var chan = await guild.createChannel("Super duper channel", ChannelType.text, nsfw: true);
/// ```
Future<GuildChannel> createChannel(String name, ChannelType type,
Future<CacheGuildChannel> createChannel(String name, ChannelType type,
{int? bitrate,
String? topic,
CategoryChannel? parent,
@ -540,11 +535,11 @@ class Guild extends SnowflakeEntity implements Disposable {
String? auditReason}) async {
// Checks to avoid API panic
if (type == ChannelType.dm || type == ChannelType.groupDm) {
return Future.error("Cannot create DM channel.");
return Future.error(ArgumentError("Cannot create DM channel."));
}
if (type == ChannelType.category && parent != null) {
return Future.error("Cannot create Category Channel which have parent channel.");
return Future.error(ArgumentError("Cannot create Category Channel which have parent channel."));
}
// Construct body
@ -566,18 +561,9 @@ class Guild extends SnowflakeEntity implements Disposable {
return Future.error(response);
}
final raw = (response as HttpResponseSuccess).jsonBody;
final raw = (response as HttpResponseSuccess).jsonBody as Map<String, dynamic>;
switch (type._value) {
case 0:
return TextChannel._new(raw as Map<String, dynamic>, this, client);
case 4:
return CategoryChannel._new(raw as Map<String, dynamic>, this, client);
case 2:
return VoiceChannel._new(raw as Map<String, dynamic>, this, client);
default:
return Future.error("Cannot create DM channel.");
}
return Channel._deserialize(raw, this.client, this) as CacheGuildChannel;
}
/// Moves channel. Allows to move channel by absolute about with [absolute] or relatively with [relative] parameter.
@ -586,7 +572,7 @@ class Guild extends SnowflakeEntity implements Disposable {
/// // This moves channel 2 places up
/// await guild.moveChannel(chan, relative: -2);
/// ```
Future<void> moveChannel(GuildChannel channel, {int? absolute, int? relative, String? auditReason}) async {
Future<void> moveChannel(CacheGuildChannel channel, {int? absolute, int? relative, String? auditReason}) async {
var newPosition = 0;
if (relative != null) {
@ -594,10 +580,10 @@ class Guild extends SnowflakeEntity implements Disposable {
} else if (absolute != null) {
newPosition = absolute;
} else {
return Future.error("Cannot move channel by zero places");
return Future.error(ArgumentError("Cannot move channel by zero places"));
}
return client._http._execute(BasicRequest._new("/guilds/${this.id}/channels",
await client._http._execute(BasicRequest._new("/guilds/${this.id}/channels",
method: "PATCH", auditLog: auditReason, body: {"id": channel.id.toString(), "position": newPosition}));
}
@ -606,8 +592,8 @@ class Guild extends SnowflakeEntity implements Disposable {
///
/// await guild.ban(member);
/// ```
Future<void> ban(Member member, {int deleteMessageDays = 0, String? auditReason}) async =>
client._http._execute(BasicRequest._new("/guilds/${this.id}/bans/${member.id.toString()}",
Future<void> ban(SnowflakeEntity user, {int deleteMessageDays = 0, String? auditReason}) async =>
client._http._execute(BasicRequest._new("/guilds/${this.id}/bans/${user.id.toString()}",
method: "PUT", auditLog: auditReason, body: {"delete-message-days": deleteMessageDays}));
/// Kicks user from guild. Member is removed from guild and he is able to rejoin
@ -615,8 +601,8 @@ class Guild extends SnowflakeEntity implements Disposable {
/// ```
/// await guild.kick(member);
/// ```
Future<void> kick(Member member, {String? auditReason}) async =>
client._http._execute(BasicRequest._new("/guilds/${this.id.toString()}/members/${member.id.toString()}",
Future<void> kick(SnowflakeEntity user, {String? auditReason}) async =>
client._http._execute(BasicRequest._new("/guilds/${this.id.toString()}/members/${user.id.toString()}",
method: "DELTE", auditLog: auditReason));
/// Unbans a user by ID.
@ -628,7 +614,7 @@ class Guild extends SnowflakeEntity implements Disposable {
{String? name,
int? verificationLevel,
int? notificationLevel,
VoiceChannel? afkChannel,
CacheVoiceChannel? afkChannel,
int? afkTimeout,
String? icon,
String? auditReason}) async {
@ -651,27 +637,27 @@ class Guild extends SnowflakeEntity implements Disposable {
return Future.error(response);
}
/// Gets a [Member] object. Caches fetched member if not cached.
/// Gets a [CacheMember] object. Caches fetched member if not cached.
///
/// ```
/// var member = guild.getMember(user);
/// ```
Future<Member> getMember(User user) async => getMemberById(user.id);
Future<IMember> getMember(User user) async => getMemberById(user.id);
/// Gets a [Member] object by id. Caches fetched member if not cached.
/// Gets a [CacheMember] object by id. Caches fetched member if not cached.
///
/// ```
/// var member = guild.getMember(Snowflake("302359795648954380"));
/// ```
Future<Member> getMemberById(Snowflake id) async {
Future<IMember> getMemberById(Snowflake id) async {
if (this.members.hasKey(id)) {
return this.members[id] as Member;
return this.members[id]!;
}
final response = await client._http._execute(BasicRequest._new("/guilds/${this.id}/members/${id.toString()}"));
if (response is HttpResponseSuccess) {
return Member._standard(response.jsonBody as Map<String, dynamic>, this, client);
return CacheMember._standard(response.jsonBody as Map<String, dynamic>, this, client);
}
return Future.error(response);
@ -680,7 +666,7 @@ class Guild extends SnowflakeEntity implements Disposable {
/// Allows to fetch guild members. In future will be restricted with `Priviliged Intents`.
/// [after] is used to continue from specified user id.
/// By default limits to one user - use [limit] paramter to change that behavior.
Stream<Member> getMembers({int limit = 1, Snowflake? after}) async* {
Stream<CacheMember> getMembers({int limit = 1, Snowflake? after}) async* {
final request = this.client._http._execute(BasicRequest._new("/guilds/${this.id.toString()}/members",
queryParams: {"limit": limit.toString(), if (after != null) "after": after.toString()}));
@ -689,13 +675,13 @@ class Guild extends SnowflakeEntity implements Disposable {
}
for (final rawMember in (request as HttpResponseSuccess).jsonBody as List<dynamic>) {
yield Member._standard(rawMember as Map<String, dynamic>, this, client);
yield CacheMember._standard(rawMember as Map<String, dynamic>, this, client);
}
}
/// Returns a [Stream] of [Member] objects whose username or nickname starts with a provided string.
/// Returns a [Stream] of [CacheMember] objects whose username or nickname starts with a provided string.
/// By default limits to one entry - can be changed with [limit] parameter.
Stream<Member> searchMembers(String query, {int limit = 1}) async* {
Stream<CacheMember> searchMembers(String query, {int limit = 1}) async* {
final response = await client._http._execute(BasicRequest._new("/guilds/${this.id}/members/search",
queryParams: {"query": query, "limit": limit.toString()}));
@ -704,25 +690,24 @@ class Guild extends SnowflakeEntity implements Disposable {
}
for (final Map<String, dynamic> member in (response as HttpResponseSuccess).jsonBody) {
yield Member._standard(member, this, client);
yield CacheMember._standard(member, this, client);
}
}
/// Returns a [Stream] of [Member] objects whose username or nickname starts with a provided string.
/// Returns a [Stream] of [CacheMember] objects whose username or nickname starts with a provided string.
/// By default limits to one entry - can be changed with [limit] parameter.
Stream<Member> searchMembersGateway(String query, {int limit = 0}) async* {
Stream<IMember> searchMembersGateway(String query, {int limit = 0}) async* {
final nonce = "$query${id.toString()}";
this.shard.requestMembers(this.id, query: query, limit: limit, nonce: nonce);
this.client.shard.requestMembers(this.id, query: query, limit: limit, nonce: nonce);
final first = (await this.client.shard.onMemberChunk.take(1).toList()).first;
final first = (await this.shard.onMemberChunk.take(1).toList()).first;
for (final member in first.members) {
yield member;
}
if (first.chunkCount > 1) {
await for (final event in this.client.shard.onMemberChunk.where((event) => event.nonce == nonce).take(first.chunkCount - 1)) {
await for (final event in this.shard.onMemberChunk.where((event) => event.nonce == nonce).take(first.chunkCount - 1)) {
for (final member in event.members) {
yield member;
}

View file

@ -0,0 +1,52 @@
part of nyxx;
/// Guild features
class GuildFeature extends IEnum<String> {
/// Guild has access to set an invite splash background
static const GuildFeature inviteSplash = GuildFeature._create("INVITE_SPLASH");
/// Guild has access to set 384kbps bitrate in voice (previously VIP voice servers)
static const GuildFeature vipRegions = GuildFeature._create("VIP_REGIONS");
/// Guild has access to set a vanity URL
static const GuildFeature vanityUrl = GuildFeature._create("VANITY_URL");
/// Guild is verified
static const GuildFeature verified = GuildFeature._create("VERIFIED");
/// Guild is partnered
static const GuildFeature partnered = GuildFeature._create("PARTNERED");
/// Guild has access to use commerce features (i.e. create store channels)
static const GuildFeature commerce = GuildFeature._create("COMMERCE");
/// Guild has access to create news channels
static const GuildFeature news = GuildFeature._create("NEWS");
/// Guild is able to be discovered in the directory
static const GuildFeature discoverable = GuildFeature._create("DISCOVERABLE");
/// Guild has access to set an animated guild icon
static const GuildFeature animatedIcon = GuildFeature._create("ANIMATED_ICON");
/// Guild has access to set a guild banner image
static const GuildFeature banner = GuildFeature._create("BANNER");
/// Guild cannot be public
static const GuildFeature publicDisabled = GuildFeature._create("PUBLIC_DISABLED");
/// Guild has enabled the welcome screen
static const GuildFeature welcomeScreenEnabled = GuildFeature._create("WELCOME_SCREEN_ENABLED");
const GuildFeature._create(String? value) : super(value ?? "");
GuildFeature.from(String? value) : super(value ?? "");
@override
bool operator ==(other) {
if (other is String) {
return other == _value;
}
return super == other;
}
}

View file

@ -19,7 +19,7 @@ class GuildPreview extends SnowflakeEntity {
late final List<Emoji> emojis;
/// List of guild's features
late final List<String> features;
late final Iterable<GuildFeature> features;
/// Approximate number of members in this guild
late final int approxMemberCount;
@ -47,7 +47,7 @@ class GuildPreview extends SnowflakeEntity {
this.emojis = [for (var rawEmoji in raw["emojis"]) Emoji._deserialize(rawEmoji as Map<String, dynamic>)];
this.features = (raw["features"] as List<dynamic>).map((e) => e.toString()).toList();
this.features = (raw["features"] as List<dynamic>).map((e) => GuildFeature.from(e.toString()));
this.approxMemberCount = raw["approximate_member_count"] as int;
this.approxOnlineMembers = raw["approximate_presence_count"] as int;

View file

@ -1,7 +1,41 @@
part of nyxx;
/// Represents [Guild] role.
/// Interface allows basic operations on member but does not guarantee data to be valid or available
class IRole extends SnowflakeEntity {
/// Reference to client
final Nyxx client;
/// Id of role's [Guild]
final Snowflake guildId;
IRole._new(Snowflake id, this.guildId, this.client) : super(id);
/// Edits the role.
Future<Role> edit(RoleBuilder role, {String? auditReason}) async {
final response = await client._http._execute(BasicRequest._new("/guilds/${this.guildId}/roles/$id",
method: "PATCH", body: role._build(), auditLog: auditReason));
if (response is HttpResponseSuccess) {
return Role._new(response.jsonBody as Map<String, dynamic>, this.guildId, client);
}
return Future.error(response);
}
/// Deletes the role.
Future<void> delete({String? auditReason}) =>
client._http
._execute(BasicRequest._new("/guilds/${this.guildId}/roles/$id", method: "DELETE", auditLog: auditReason));
/// Adds role to user.
Future<void> addToUser(User user, {String? auditReason}) =>
client._http._execute(
BasicRequest._new("/guilds/${this.guildId}/members/${user.id}/roles/$id", method: "PUT", auditLog: auditReason));
}
/// Represents a Discord guild role, which is used to assign priority, permissions, and a color to guild members
class Role extends SnowflakeEntity implements Mentionable, GuildEntity {
class Role extends IRole implements Mentionable, GuildEntity {
/// The role's name.
late final String name;
@ -22,24 +56,19 @@ class Role extends SnowflakeEntity implements Mentionable, GuildEntity {
/// The role's guild.
@override
late final Guild guild;
late final Guild? guild;
/// The role's permissions.
late final Permissions permissions;
/// Returns all members which have this role assigned
Iterable<Member> get members => guild.members.find((m) => m.roles.contains(this));
@override
/// Mention of role. If role cannot be mentioned it returns name of role (@name)
@override
String get mention => mentionable ? "<@&${this.id}>" : "@$name";
/// Additional role data like if role is managed by integration or role is from server boosting.
late final RoleTags? roleTags;
/// Reference to [Nyxx] instance
Nyxx client;
Role._new(Map<String, dynamic> raw, this.guild, this.client) : super(Snowflake(raw["id"] as String)) {
Role._new(Map<String, dynamic> raw, Snowflake guildId, Nyxx client) : super._new(Snowflake(raw["id"]), guildId, client) {
this.name = raw["name"] as String;
this.position = raw["position"] as int;
this.hoist = raw["hoist"] as bool;
@ -47,31 +76,38 @@ class Role extends SnowflakeEntity implements Mentionable, GuildEntity {
this.mentionable = raw["mentionable"] as bool? ?? false;
this.permissions = Permissions.fromInt(raw["permissions"] as int);
this.color = DiscordColor.fromInt(raw["color"] as int);
}
/// Edits the role.
Future<Role> edit(RoleBuilder role, {String? auditReason}) async {
final response = await client._http._execute(BasicRequest._new("/guilds/${this.guild.id}/roles/$id",
method: "PATCH", body: role._build(), auditLog: auditReason));
if (response is HttpResponseSuccess) {
return Role._new(response.jsonBody as Map<String, dynamic>, this.guild, client);
if(raw["tags"] != null) {
this.roleTags = RoleTags._new(raw["tags"] as Map<String, dynamic>);
} else {
this.roleTags = null;
}
return Future.error(response);
this.guild = client.guilds[this.guildId];
}
/// Deletes the role.
Future<void> delete({String? auditReason}) =>
client._http
._execute(BasicRequest._new("/guilds/${this.guild.id}/roles/$id", method: "DELETE", auditLog: auditReason));
/// Adds role to user.
Future<void> addToUser(User user, {String? auditReason}) =>
client._http._execute(
BasicRequest._new("/guilds/${guild.id}/members/${user.id}/roles/$id", method: "PUT", auditLog: auditReason));
/// Returns a mention of role. If role cannot be mentioned it returns name of role.
@override
String toString() => mention;
}
/// Additional [Role] role tags which hold optional data about role
class RoleTags {
/// Holds [Snowflake] of bot id if role is for bot user
late final Snowflake? botId;
/// True if role is for server nitro boosting
late final bool nitroRole;
/// Holds [Snowflake] of integration if role is part of twitch/other integration
late final Snowflake? integrationId;
/// Returns true if role is for bot.
bool get isBotRole => botId != null;
RoleTags._new(Map<String, dynamic> raw) {
this.botId = raw["bot_id"] != null ? Snowflake(raw["bot_id"]) : null;
this.nitroRole = raw["premium_subscriber"] != null ? raw["premium_subscriber"] as bool : false;
this.integrationId = raw["integration_id"] != null ? Snowflake(raw["integration_id"]) : null;
}
}

View file

@ -3,7 +3,10 @@ part of nyxx;
/// Type of webhook. Either [incoming] if it its normal webhook executable with token,
/// or [channelFollower] if its discord internal webhook
class WebhookType extends IEnum<int> {
/// Incoming Webhooks can post messages to channels with a generated token
static const WebhookType incoming = WebhookType._create(1);
/// Channel Follower Webhooks are internal webhooks used with Channel Following to post new messages into channels
static const WebhookType channelFollower = WebhookType._create(2);
const WebhookType._create(int? value) : super(value ?? 0);
@ -25,11 +28,11 @@ class Webhook extends SnowflakeEntity implements IMessageAuthor {
/// The webhook's name.
late final String? name;
/// The webhook's token.
late final String? token;
/// The webhook's token. Defaults to empty string
late final String token;
/// The webhook's channel, if this is accessed using a normal client and the client has that channel in it's cache.
late final TextChannel? channel;
late final GuildTextChannel? channel;
/// The webhook's guild, if this is accessed using a normal client and the client has that guild in it's cache.
late final Guild? guild;
@ -37,13 +40,15 @@ class Webhook extends SnowflakeEntity implements IMessageAuthor {
/// The user, if this is accessed using a normal client.
late final User? user;
// TODO: Create data class
/// Webhook type
late final WebhookType type;
/// Webhooks avatar hash
late final String? avatarHash;
/// Default webhook avatar id
int get defaultAvatarId => 0;
@override
String get username => this.name.toString();
@ -62,12 +67,12 @@ class Webhook extends SnowflakeEntity implements IMessageAuthor {
Webhook._new(Map<String, dynamic> raw, this.client) : super(Snowflake(raw["id"] as String)) {
this.name = raw["name"] as String?;
this.token = raw["token"] as String?;
this.token = raw["token"] as String? ?? "";
this.avatarHash = raw["avatar"] as String?;
this.type = WebhookType.from(raw["type"] as int);
if (raw["channel_id"] != null) {
this.channel = client.channels[Snowflake(raw["channel_id"] as String)] as TextChannel?;
this.channel = client.channels[Snowflake(raw["channel_id"] as String)] as GuildTextChannel?;
}
if (raw["guild_id"] != null) {
@ -79,17 +84,62 @@ class Webhook extends SnowflakeEntity implements IMessageAuthor {
}
}
/// Executes webhook. Webhooks can send multiple embeds in one messsage using [embeds].
///
/// [wait] - waits for server confirmation of message send before response,
/// and returns the created message body (defaults to false; when false a message that is not save does not return an error)
Future<Message> execute(
{dynamic content,
List<AttachmentBuilder>? files,
List<EmbedBuilder>? embeds,
bool? tts,
AllowedMentions? allowedMentions,
bool? wait,
String? avatarUrl}) async {
allowedMentions ??= client._options.allowedMentions;
final reqBody = {
if (content != null) "content": content.toString(),
if (allowedMentions != null) "allowed_mentions": allowedMentions._build(),
if(embeds != null) "embeds" : [
for(final e in embeds)
e._build()
],
if (content != null && tts != null) "tts": tts,
if(avatarUrl != null) "avatar_url" : avatarUrl,
};
final queryParams = { if(wait != null) "wait" : wait };
_HttpResponse response;
if (files != null && files.isNotEmpty) {
response = await client._http
._execute(MultipartRequest._new("/webhooks/${this.id.toString()}/${this.token}", files, method: "POST", fields: reqBody, queryParams: queryParams));
} else {
response = await client._http
._execute(BasicRequest._new("/webhooks/${this.id.toString()}/${this.token}", body: reqBody, method: "POST", queryParams: queryParams));
}
if (response is HttpResponseSuccess) {
return Message._deserialize(response.jsonBody as Map<String, dynamic>, client);
}
return Future.error(response);
}
@override
String avatarURL({String format = "webp", int size = 128}) {
if (this.avatarHash != null) {
return "https://cdn.${Constants.cdnHost}/avatars/${this.id}/${this.avatarHash}.$format?size=$size";
}
return "https://cdn.${Constants.cdnHost}/embed/avatars/0.png?size=$size";
return "https://cdn.${Constants.cdnHost}/embed/avatars/$defaultAvatarId.png?size=$size";
}
/// Edits the webhook.
Future<Webhook> edit({String? name, TextChannel? channel, File? avatar, String? encodedAvatar, String? auditReason}) async {
Future<Webhook> edit(
{String? name, ITextChannel? channel, File? avatar, String? encodedAvatar, String? auditReason}) async {
final body = <String, dynamic>{
if (name != null) "name": name,
if (channel != null) "channel_id": channel.id.toString()
@ -111,7 +161,7 @@ class Webhook extends SnowflakeEntity implements IMessageAuthor {
/// Deletes the webhook.
Future<void> delete({String? auditReason}) =>
client._http._execute(BasicRequest._new("/webhooks/$id/$token", method: "DELETE", auditLog: auditReason));
client._http._execute(BasicRequest._new("/webhooks/$id/$token", method: "DELETE", auditLog: auditReason));
/// Returns a string representation of this object.
@override

View file

@ -3,15 +3,12 @@ part of nyxx;
/// Represents emoji. Subclasses provides abstraction to custom emojis(like [GuildEmoji]).
abstract class Emoji {
/// Emojis name.
String name;
Emoji(this.name);
final String? name;
Emoji._new(this.name);
// TODO: Emojis stuff
factory Emoji._deserialize(Map<String, dynamic> raw) {
if (raw["id"] != null) {
return GuildEmoji._partial(raw);
}
return UnicodeEmoji(raw["name"] as String);
}

View file

@ -1,24 +1,57 @@
part of nyxx;
abstract class IGuildEmoji extends Emoji implements SnowflakeEntity {
/// True if emoji is partial.
final bool partial;
/// Snowflake id of emoji
@override
late final Snowflake id;
@override
DateTime get createdAt => id.timestamp;
IGuildEmoji._new(Map<String, dynamic> raw, this.partial) : super._new(raw["name"] as String?) {
this.id = Snowflake(raw["id"] as String);
}
}
class PartialGuildEmoji extends IGuildEmoji {
PartialGuildEmoji._new(Map<String, dynamic> raw) : super._new(raw, true);
/// Encodes Emoji to API format
@override
String encode() => "$id";
/// Formats Emoji to message format
@override
String format() => "<:$id>";
/// Returns cdn url to emoji
String get cdnUrl => "https://cdn.discordapp.com/emojis/${this.id}.png";
/// Returns encoded string ready to send via message.
@override
String toString() => format();
}
/// Emoji object. Handles Unicode emojis and custom ones.
/// Always check if object is partial via [partial] field before accessing fields or methods,
/// due any of field can be null or empty
class GuildEmoji extends Emoji implements SnowflakeEntity, GuildEntity {
class GuildEmoji extends IGuildEmoji implements SnowflakeEntity, GuildEntity {
/// Reference tp [Nyxx] object
Nyxx? client;
Nyxx client;
/// Emojis guild
@override
late final Guild? guild;
/// Emoji guild
late final Guild guild;
/// Emojis guild id
@override
late final Snowflake guildId;
/// Snowflake id of emoji
late final Snowflake id;
/// Roles this emoji is whitelisted to
late final List<Role> roles;
/// Roles which can use this emote
late final Iterable<IRole> roles;
/// whether this emoji must be wrapped in colons
late final bool requireColons;
@ -29,43 +62,25 @@ class GuildEmoji extends Emoji implements SnowflakeEntity, GuildEntity {
/// whether this emoji is animated
late final bool animated;
/// True if emoji is partial.
/// Always check before accessing fields or methods, due any of field can be null or empty
late final bool partial;
/// Creates full emoji object
GuildEmoji._new(Map<String, dynamic> raw, this.guild, this.client) : super("") {
this.id = Snowflake(raw["id"] as String);
GuildEmoji._new(Map<String, dynamic> raw, this.guildId, this.client) : super._new(raw, false) {
this.guild = client.guilds[this.guildId];
this.name = raw["name"] as String;
this.requireColons = raw["require_colons"] as bool? ?? false;
this.managed = raw["managed"] as bool? ?? false;
this.animated = raw["animated"] as bool? ?? false;
this.roles = [];
if (raw["roles"] != null) {
for (final roleId in raw["roles"]) {
final role = this.guild.roles[Snowflake(roleId)];
if (role != null) {
this.roles.add(role);
}
}
}
this.partial = false;
}
/// Creates partial object - only [id] and [name]
GuildEmoji._partial(Map<String, dynamic> raw) : super(raw["name"] as String) {
this.id = Snowflake(raw["id"] as String);
this.partial = true;
this.roles = [
if (raw["roles"] != null)
for (final roleId in raw["roles"])
IRole._new(Snowflake(roleId), this.guildId, client)
];
}
/// Allows to edit emoji
Future<GuildEmoji> edit({String? name, List<Snowflake>? roles}) async {
if (name == null && roles == null) {
return Future.error("Both name and roles fields cannot be null");
return Future.error(ArgumentError("Both name and roles fields cannot be null"));
}
final body = <String, dynamic>{
@ -73,20 +88,20 @@ class GuildEmoji extends Emoji implements SnowflakeEntity, GuildEntity {
if (roles != null) "roles": roles.map((r) => r.toString())
};
final response = await client?._http._execute(
BasicRequest._new("/guilds/${guild.id.toString()}/emojis/${this.id.toString()}", method: "PATCH", body: body));
final response = await client._http._execute(
BasicRequest._new("/guilds/${this.guildId}/emojis/${this.id.toString()}", method: "PATCH", body: body));
if (response is HttpResponseSuccess) {
return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, guild, client);
return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, this.guildId, client);
}
return Future.error(response ?? "Cannot perform http request if emoji is partial");
return Future.error(response);
}
/// Deletes emoji
Future<void> delete() async =>
client?._http._execute(
BasicRequest._new("/guilds/${this.guild.id.toString()}/emojis/${this.id.toString()}", method: "DELETE"));
client._http._execute(
BasicRequest._new("/guilds/${this.guildId}/emojis/${this.id.toString()}", method: "DELETE"));
/// Encodes Emoji to API format
@override

View file

@ -8,14 +8,15 @@ class DMMessage extends Message {
/// Returns clickable url to this message.
@override
String get url => "https://discordapp.com/channels/@me"
"/${this.channel.id}/${this.id}";
"/${this.channelId}/${this.id}";
DMMessage._new(Map<String, dynamic> raw, Nyxx client) : super._new(raw, client) {
final user = client.users[Snowflake(raw["author"]["id"] as String)];
final user = client.users[Snowflake(raw["author"]["id"])];
if (user == null) {
final authorData = raw["author"] as Map<String, dynamic>;
this.author = User._new(authorData, client);
this.client.users.add(this.author.id, this.author);
} else {
this.author = user;
}
@ -26,7 +27,10 @@ class DMMessage extends Message {
class GuildMessage extends Message implements GuildEntity {
/// The message's guild.
@override
late final Guild guild;
late final Guild? guild;
/// Id of message's guild
late final Snowflake guildId;
/// Reference to original message if this message cross posts other message
late final MessageReference? crosspostReference;
@ -36,10 +40,10 @@ class GuildMessage extends Message implements GuildEntity {
/// Returns clickable url to this message.
@override
String get url => "https://discordapp.com/channels/${this.guild.id}"
"/${this.channel.id}/${this.id}";
String get url => "https://discordapp.com/channels/${this.guildId}"
"/${this.channelId}/${this.id}";
/// The message's author. Can be instance of [Member] or [Webhook]
/// The message's author. Can be instance of [CacheMember] or [Webhook]
@override
late final IMessageAuthor author;
@ -47,33 +51,34 @@ class GuildMessage extends Message implements GuildEntity {
/// True if message is sent by a webhook
bool get isByWebhook => author is Webhook;
/// A list of IDs for the role mentions in the message.
late List<Role> roleMentions;
/// Role mentions in this message
late final List<IRole> roleMentions;
GuildMessage._new(Map<String, dynamic> raw, Nyxx client) : super._new(raw, client) {
if (raw["message_reference"] != null) {
this.crosspostReference = MessageReference._new(raw["message_reference"] as Map<String, dynamic>, client);
}
this.guild = client.guilds[Snowflake(raw["guild_id"])]!;
this.guildId = Snowflake(raw["guild_id"]);
this.guild = client.guilds[this.guildId];
if (raw["webhook_id"] != null) {
this.author = Webhook._new(raw["author"] as Map<String, dynamic>, client);
} else if (raw["author"] != null) {
final member = this.guild.members[Snowflake(raw["author"]["id"] as String)];
final member = this.guild?.members[Snowflake(raw["author"]["id"])];
if (member == null) {
if (raw["member"] == null) {
this.author = User._new(raw["author"] as Map<String, dynamic>, client);
this.client.users[this.author.id] = this.author as User;
} else {
final authorData = raw["author"] as Map<String, dynamic>;
final memberData = raw["member"] as Map<String, dynamic>;
final author =
Member._fromUser(authorData, memberData, client.guilds[Snowflake(raw["guild_id"])] as Guild, client);
final author = this.guild == null ? CachelessMember._fromUser(authorData, memberData, guildId, client) : CacheMember._fromUser(authorData, memberData, this.guild!, client);
client.users[author.id] = author;
guild.members[author.id] = author;
guild?.members[author.id] = author;
this.author = author;
}
} else {
@ -83,7 +88,8 @@ class GuildMessage extends Message implements GuildEntity {
this.roleMentions = [
if (raw["mention_roles"] != null)
for (var r in raw["mention_roles"]) this.guild.roles[Snowflake(r)] as Role
for (var roleId in raw["mention_roles"])
this.guild == null ? IRole ._new(Snowflake(roleId), guildId, client) : guild!.roles[Snowflake(roleId)]!
];
}
@ -91,7 +97,7 @@ class GuildMessage extends Message implements GuildEntity {
/// This endpoint requires the "DISCOVERY" feature to be present for the guild.
Future<void> crosspost() async =>
this.client._http._execute(BasicRequest._new(
"/channels/${this.channel.id.toString()}/messages/${this.id.toString()}/crosspost",
"/channels/${this.channelId.toString()}/messages/${this.id.toString()}/crosspost",
method: "POST"));
}
@ -105,15 +111,18 @@ abstract class Message extends SnowflakeEntity implements Disposable {
late String content;
/// Channel in which message was sent
late final MessageChannel channel;
late final ITextChannel channel;
/// Id of channel in which message was sent
late final Snowflake channelId;
/// The timestamp of when the message was last edited, null if not edited.
late final DateTime? editedTimestamp;
/// The message's author. Can be instance of [Member]
/// The message's author. Can be instance of [CacheMember]
IMessageAuthor get author;
/// The mentions in the message. [User] value of this map can be [Member]
/// The mentions in the message. [User] value of this map can be [CacheMember]
late List<User> mentions;
/// A collection of the embeds in the message.
@ -151,10 +160,18 @@ abstract class Message extends SnowflakeEntity implements Disposable {
return DMMessage._new(raw, client);
}
Message._new(Map<String, dynamic> raw, this.client) : super(Snowflake(raw["id"] as String)) {
Message._new(Map<String, dynamic> raw, this.client) : super(Snowflake(raw["id"])) {
this.content = raw["content"] as String;
this.channelId = Snowflake(raw["channel_id"]);
final channel = client.channels[this.channelId] as ITextChannel?;
this.channel = client.channels[Snowflake(raw["channel_id"] as String)] as MessageChannel;
if(channel == null) {
// TODO: channel stuff
this.channel = DummyTextChannel._new({"id" : raw["channel_id"]}, -1, client);
} else {
this.channel = channel;
}
this.pinned = raw["pinned"] as bool;
this.tts = raw["tts"] as bool;
@ -194,23 +211,44 @@ abstract class Message extends SnowflakeEntity implements Disposable {
@override
String toString() => this.content;
// TODO: Manage message flags better
/// Suppresses embeds in message. Can be executed in other users messages.
Future<Message> suppressEmbeds() async {
final body = <String, dynamic>{
"flags" : 1 << 2
};
final response = await client._http
._execute(BasicRequest._new("/channels/${this.channelId}/messages/${this.id}", method: "PATCH", body: body));
if (response is HttpResponseSuccess) {
return Message._deserialize(response.jsonBody as Map<String, dynamic>, client);
}
return Future.error(response);
}
/// Edits the message.
///
/// Throws an [Exception] if the HTTP request errored.
/// Message.edit("My edited content!");
Future<Message> edit({dynamic content, EmbedBuilder? embed, AllowedMentions? allowedMentions}) async {
Future<Message> edit({dynamic content, EmbedBuilder? embed, AllowedMentions? allowedMentions, MessageEditBuilder? builder}) async {
if (builder != null) {
content = builder._content;
embed = builder.embed;
allowedMentions = builder.allowedMentions;
}
if (this.author.id != client.self.id) {
return Future.error("Cannot edit someones message");
return Future.error(ArgumentError("Cannot edit someones message"));
}
final body = <String, dynamic>{
if (content != null) "content": content.toString(),
if (embed != null) "embed": embed._build(),
if (allowedMentions != null) "allowed_mentions": allowedMentions._build()
if (allowedMentions != null) "allowed_mentions": allowedMentions._build(),
};
final response = await client._http
._execute(BasicRequest._new("/channels/${this.channel.id}/messages/${this.id}", method: "PATCH", body: body));
._execute(BasicRequest._new("/channels/${this.channelId}/messages/${this.id}", method: "PATCH", body: body));
if (response is HttpResponseSuccess) {
return Message._deserialize(response.jsonBody as Map<String, dynamic>, client);
@ -222,46 +260,58 @@ abstract class Message extends SnowflakeEntity implements Disposable {
/// Add reaction to message.
Future<void> createReaction(Emoji emoji) =>
client._http._execute(BasicRequest._new(
"/channels/${this.channel.id}/messages/${this.id}/reactions/${emoji.encode()}/@me",
"/channels/${this.channelId}/messages/${this.id}/reactions/${emoji.encode()}/@me",
method: "PUT"));
/// Deletes reaction of bot. Emoji as ":emoji_name:"
Future<void> deleteReaction(Emoji emoji) =>
client._http._execute(BasicRequest._new(
"/channels/${this.channel.id}/messages/${this.id}/reactions/${emoji.encode()}/@me",
"/channels/${this.channelId}/messages/${this.id}/reactions/${emoji.encode()}/@me",
method: "DELETE"));
/// Deletes reaction of given user.
Future<void> deleteUserReaction(Emoji emoji, String userId) =>
client._http._execute(BasicRequest._new(
"/channels/${this.channel.id}/messages/${this.id}/reactions/${emoji.encode()}/$userId",
"/channels/${this.channelId}/messages/${this.id}/reactions/${emoji.encode()}/$userId",
method: "DELETE"));
/// Deletes all reactions
Future<void> deleteAllReactions() =>
client._http
._execute(BasicRequest._new("/channels/${this.channel.id}/messages/${this.id}/reactions", method: "DELETE"));
._execute(BasicRequest._new("/channels/${this.channelId}/messages/${this.id}/reactions", method: "DELETE"));
/// Deletes the message.
///
/// Throws an [Exception] if the HTTP request errored.
Future<void> delete({String? auditReason}) =>
client._http._execute(
BasicRequest._new("/channels/${this.channel.id}/messages/${this.id}", method: "DELETE", auditLog: auditReason));
BasicRequest._new("/channels/${this.channelId}/messages/${this.id}", method: "DELETE", auditLog: auditReason));
/// Pins [Message] in current [Channel]
Future<void> pinMessage() =>
client._http._execute(BasicRequest._new("/channels/${channel.id}/pins/$id", method: "PUT"));
client._http._execute(BasicRequest._new("/channels/${this.channelId}/pins/$id", method: "PUT"));
/// Unpins [Message] in current [Channel]
Future<void> unpinMessage() =>
client._http._execute(BasicRequest._new("/channels/${channel.id}/pins/$id", method: "DELETE"));
client._http._execute(BasicRequest._new("/channels/${this.channelId}/pins/$id", method: "DELETE"));
@override
bool operator ==(other) {
if (other is Message) {
return other.content == this.content || other.embeds.any((e) => this.embeds.any((f) => e == f));
return this.id == other.id;
}
if(other is Snowflake) {
return this.id == other;
}
if(other is int) {
return this.id == other;
}
if(other is String) {
return this.id == other;
}
return false;

View file

@ -19,7 +19,9 @@ class Reaction {
if (rawEmoji["id"] == null) {
this.emoji = UnicodeEmoji(rawEmoji["name"] as String);
} else {
this.emoji = GuildEmoji._partial(rawEmoji);
//TODO: EMOJIS STUUF
//this.emoji = PartialGuildEmoji._new(rawEmoji);
}
}

View file

@ -2,17 +2,18 @@ part of nyxx;
/// Represents unicode emoji. Contains only emoji code.
class UnicodeEmoji extends Emoji {
UnicodeEmoji(String code) : super(code);
/// Create unicode emoji from given code
UnicodeEmoji(String name) : super._new(name);
/// Returns Emoji
String get code => this.name;
String get code => this.name!;
/// Returns runes of emoji
Runes get runes => this.name.runes;
Runes get runes => this.name!.runes;
/// Encodes Emoji so that can be used in messages.
@override
String encode() => this.name;
String encode() => this.name!;
/// Returns encoded string ready to send via message.
@override

View file

@ -1,100 +0,0 @@
part of nyxx;
/// Provides common properties for [Permissions] and [PermissionsBuilder]
abstract class AbstractPermissions {
/// The raw permission code.
late final int raw;
/// True if user can create InstantInvite
late final bool createInstantInvite;
/// True if user can kick members
late final bool kickMembers;
/// True if user can ban members
late final bool banMembers;
/// True if user is administrator
late final bool administrator;
/// True if user can manager channels
late final bool manageChannels;
/// True if user can manager guilds
late final bool manageGuild;
/// Allows to add reactions
late final bool addReactions;
/// Allows for using priority speaker in a voice channel
late final bool prioritySpeaker;
/// Allow to view audit logs
late final bool viewAuditLog;
/// Allow viewing channels (OLD READ_MESSAGES)
late final bool viewChannel;
/// True if user can send messages
late final bool sendMessages;
/// True if user can send TTF messages
late final bool sendTtsMessages;
/// True if user can manage messages
late final bool manageMessages;
/// True if user can send links in messages
late final bool embedLinks;
/// True if user can attach files in messages
late final bool attachFiles;
/// True if user can read messages history
late final bool readMessageHistory;
/// True if user can mention everyone
late final bool mentionEveryone;
/// True if user can use external emojis
late final bool useExternalEmojis;
/// True if user can connect to voice channel
late final bool connect;
/// True if user can speak
late final bool speak;
/// True if user can mute members
late final bool muteMembers;
/// True if user can deafen members
late final bool deafenMembers;
/// True if user can move members
late final bool moveMembers;
/// Allows for using voice-activity-detection in a voice channel
late final bool useVad;
/// True if user can change nick
late final bool changeNickname;
/// True if user can manager others nicknames
late final bool manageNicknames;
/// True if user can manage server's roles
late final bool manageRoles;
/// True if user can manage webhooks
late final bool manageWebhooks;
/// Allows management and editing of emojis
late final bool manageEmojis;
/// Allows the user to go live
late final bool stream;
/// Allows for viewing guild insights
late final bool viewGuildInsights;
}

View file

@ -1,7 +1,7 @@
part of nyxx;
/// Holds permissions overrides for channel
class PermissionsOverrides extends SnowflakeEntity {
class PermissionsOverrides extends SnowflakeEntity implements Convertable<PermissionOverrideBuilder> {
/// Type of entity
late final String type;
@ -18,7 +18,10 @@ class PermissionsOverrides extends SnowflakeEntity {
this.allow = raw["allow"] as int;
this.deny = raw["deny"] as int;
permissions = Permissions.fromOverwrite(0, allow, deny);
type = raw["type"] as String;
this.permissions = Permissions.fromOverwrite(0, allow, deny);
this.type = raw["type"] as String;
}
@override
PermissionOverrideBuilder toBuilder() => PermissionOverrideBuilder.from(this.type, this.id, this.permissions);
}

View file

@ -1,7 +1,103 @@
part of nyxx;
/// Permissions for a role or channel override.
class Permissions extends AbstractPermissions {
class Permissions implements Convertable<PermissionsBuilder> {
/// The raw permission code.
late final int raw;
/// True if user can create InstantInvite
late final bool createInstantInvite;
/// True if user can kick members
late final bool kickMembers;
/// True if user can ban members
late final bool banMembers;
/// True if user is administrator
late final bool administrator;
/// True if user can manager channels
late final bool manageChannels;
/// True if user can manager guilds
late final bool manageGuild;
/// Allows to add reactions
late final bool addReactions;
/// Allows for using priority speaker in a voice channel
late final bool prioritySpeaker;
/// Allow to view audit logs
late final bool viewAuditLog;
/// Allow viewing channels (OLD READ_MESSAGES)
late final bool viewChannel;
/// True if user can send messages
late final bool sendMessages;
/// True if user can send TTF messages
late final bool sendTtsMessages;
/// True if user can manage messages
late final bool manageMessages;
/// True if user can send links in messages
late final bool embedLinks;
/// True if user can attach files in messages
late final bool attachFiles;
/// True if user can read messages history
late final bool readMessageHistory;
/// True if user can mention everyone
late final bool mentionEveryone;
/// True if user can use external emojis
late final bool useExternalEmojis;
/// True if user can connect to voice channel
late final bool connect;
/// True if user can speak
late final bool speak;
/// True if user can mute members
late final bool muteMembers;
/// True if user can deafen members
late final bool deafenMembers;
/// True if user can move members
late final bool moveMembers;
/// Allows for using voice-activity-detection in a voice channel
late final bool useVad;
/// True if user can change nick
late final bool changeNickname;
/// True if user can manager others nicknames
late final bool manageNicknames;
/// True if user can manage server's roles
late final bool manageRoles;
/// True if user can manage webhooks
late final bool manageWebhooks;
/// Allows management and editing of emojis
late final bool manageEmojis;
/// Allows the user to go live
late final bool stream;
/// Allows for viewing guild insights
late final bool viewGuildInsights;
/// Makes a [Permissions] object from a raw permission code.
Permissions.fromInt(int permissions) {
_construct(permissions);
@ -72,4 +168,7 @@ class Permissions extends AbstractPermissions {
return false;
}
@override
PermissionsBuilder toBuilder() => PermissionsBuilder.from(this);
}

View file

@ -1,7 +1,35 @@
part of nyxx;
/// A user with [Guild] context.
class Member extends User implements GuildEntity {
/// Represents [Guild] member. Can be either [CachelessMember] or [CacheMember] depending on client config and cache state.
/// Interface allows basic operations on member but does not guarantee data to be valid or available
abstract class IMember extends User {
IMember._new(Map<String, dynamic> raw, Nyxx client) : super._new(raw, client);
/// Checks if member has specified role. Returns true if user is assigned to given role.
bool hasRole(bool Function(IRole role) func);
/// Bans the member and optionally deletes [deleteMessageDays] days worth of messages.
Future<void> ban({int? deleteMessageDays, String? reason, String? auditReason});
/// Adds role to user
///
/// ```
/// var r = guild.roles.values.first;
/// await member.addRole(r);
/// ```
Future<void> addRole(IRole role, {String? auditReason});
/// Removes [role] from user.
Future<void> removeRole(IRole role, {String? auditReason});
/// Kicks the member from guild
Future<void> kick({String? auditReason});
/// Edits members. Allows to move user in voice channel, mute or deaf, change nick, roles.
Future<void> edit({String? nick, List<IRole>? roles, bool? mute, bool? deaf, VoiceChannel? channel, String? auditReason});
}
/// Stateless [IMember] instance. Does not have reference to guild.
class CachelessMember extends IMember {
/// The members nickname, null if not set.
String? nickname;
@ -14,56 +42,27 @@ class Member extends User implements GuildEntity {
/// Weather or not the member is muted.
late final bool mute;
/// A list of [Role]s the member has.
late List<Role> roles;
/// Id of guild
final Snowflake guildId;
/// The highest displayed role that a member has, null if they dont have any
Role? hoistedRole;
/// Roles of member. It will contain instance of [IRole] if [CachelessMember] or instance of [Role] if instance of [CacheMember]
late Iterable<IRole> roles;
@override
/// Highest role of member
late IRole? hoistedRole;
/// The guild that the member is a part of.
Guild guild;
factory CachelessMember._standard(Map<String, dynamic> data, Snowflake guildId, Nyxx client) =>
CachelessMember._new(data, data["user"] as Map<String, dynamic>, guildId, client);
/// Returns highest role for member
Role get highestRole => roles.isEmpty ? guild.everyoneRole : roles.reduce((f, s) => f.position > s.position ? f : s);
factory CachelessMember._fromUser(Map<String, dynamic> dataUser, Map<String, dynamic> dataMember, Snowflake guildId, Nyxx client) =>
CachelessMember._new(dataMember, dataUser, guildId, client);
/// Color of highest role of user
DiscordColor get color => highestRole.color;
/// Voice state of member
VoiceState? get voiceState => guild.voiceStates[this.id];
/// Returns total permissions of user.
Permissions get effectivePermissions {
if (this == guild.owner) {
return Permissions.all();
}
var total = guild.everyoneRole.permissions.raw;
for (final role in roles) {
total |= role.permissions.raw;
if (PermissionsUtils.isApplied(total, PermissionsConstants.administrator)) {
return Permissions.fromInt(PermissionsConstants.allPermissions);
}
}
return Permissions.fromInt(total);
}
factory Member._standard(Map<String, dynamic> data, Guild guild, Nyxx client) =>
Member._new(data, data["user"] as Map<String, dynamic>, guild, client);
factory Member._fromUser(Map<String, dynamic> dataUser, Map<String, dynamic> dataMember, Guild guild, Nyxx client) =>
Member._new(dataMember, dataUser, guild, client);
Member._new(Map<String, dynamic> raw, Map<String, dynamic> user, this.guild, Nyxx client) : super._new(user, client) {
CachelessMember._new(Map<String, dynamic> raw, Map<String, dynamic> userRaw, this.guildId, Nyxx client) : super._new(userRaw, client) {
this.nickname = raw["nick"] as String?;
this.deaf = raw["deaf"] as bool;
this.mute = raw["mute"] as bool;
this.roles = [for (var id in raw["roles"]) guild.roles[Snowflake(id)] as Role];
this.roles = [for (var id in raw["roles"]) IRole._new(Snowflake(id), this.guildId, client)];
if (raw["hoisted_role"] != null && roles.isNotEmpty) {
// TODO: NNBD: try-catch in where
@ -83,7 +82,7 @@ class Member extends User implements GuildEntity {
}
}
bool _updateMember(String? nickname, List<Role> roles) {
bool _updateMember(String? nickname, List<IRole> roles) {
var changed = false;
if (this.nickname != nickname) {
@ -100,44 +99,50 @@ class Member extends User implements GuildEntity {
}
/// Checks if member has specified role. Returns true if user is assigned to given role.
bool hasRole(bool Function(Role role) func) => this.roles.any(func);
@override
bool hasRole(bool Function(IRole role) func) => this.roles.any(func);
/// Bans the member and optionally deletes [deleteMessageDays] days worth of messages.
@override
Future<void> ban({int? deleteMessageDays, String? reason, String? auditReason}) async {
final body = <String, dynamic>{
if (deleteMessageDays != null) "delete-message-days": deleteMessageDays,
if (reason != null) "reason": reason
};
return client._http._execute(BasicRequest._new("/guilds/${this.guild.id}/bans/${this.id}",
await client._http._execute(BasicRequest._new("/guilds/${this.guildId}/bans/${this.id}",
method: "PUT", auditLog: auditReason, body: body));
}
/// Adds role to user
/// Adds role to user.
///
/// ```
/// var r = guild.roles.values.first;
/// await member.addRole(r);
/// ```
Future<void> addRole(Role role, {String? auditReason}) =>
client._http._execute(BasicRequest._new("/guilds/${guild.id}/members/${this.id}/roles/${role.id}",
method: "PUT", auditLog: auditReason));
@override
Future<void> addRole(IRole role, {String? auditReason}) =>
client._http._execute(BasicRequest._new("/guilds/$guildId/members/${this.id}/roles/${role.id}",
method: "PUT", auditLog: auditReason));
/// Removes [role] from user.
Future<void> removeRole(Role role, {String? auditReason}) =>
client._http._execute(BasicRequest._new(
"/guilds/${this.guild.id.toString()}/members/${this.id.toString()}/roles/${role.id.toString()}",
method: "DELETE",
auditLog: auditReason));
@override
Future<void> removeRole(IRole role, {String? auditReason}) =>
client._http._execute(BasicRequest._new(
"/guilds/${this.guildId.toString()}/members/${this.id.toString()}/roles/${role.id.toString()}",
method: "DELETE",
auditLog: auditReason));
/// Kicks the member from guild
@override
Future<void> kick({String? auditReason}) =>
client._http._execute(
BasicRequest._new("/guilds/${this.guild.id}/members/${this.id}", method: "DELETE", auditLog: auditReason));
client._http._execute(
BasicRequest._new("/guilds/${this.guildId}/members/${this.id}", method: "DELETE", auditLog: auditReason));
/// Edits members. Allows to move user in voice channel, mute or deaf, change nick, roles.
@override
Future<void> edit(
{String? nick, List<Role>? roles, bool? mute, bool? deaf, VoiceChannel? channel, String? auditReason}) {
{String? nick, List<IRole>? roles, bool? mute, bool? deaf, VoiceChannel? channel, String? auditReason}) {
final body = <String, dynamic>{
if (nick != null) "nick": nick,
if (roles != null) "roles": roles.map((f) => f.id.toString()).toList(),
@ -146,10 +151,66 @@ class Member extends User implements GuildEntity {
if (channel != null) "channel_id": channel.id.toString()
};
return client._http._execute(BasicRequest._new("/guilds/${this.guild.id.toString()}/members/${this.id.toString()}",
return client._http._execute(BasicRequest._new("/guilds/${this.guildId.toString()}/members/${this.id.toString()}",
method: "PATCH", auditLog: auditReason, body: body));
}
@override
String toString() => super.toString();
}
/// A user with [Guild] context.
class CacheMember extends CachelessMember implements GuildEntity {
/// The guild that the member is a part of.
@override
Guild guild;
/// Returns highest role for member
IRole get highestRole => this.roles.isEmpty ? guild.everyoneRole : roles.reduce((f, s) => (f as Role).position > (s as Role).position ? f : s);
/// Color of highest role of user
DiscordColor get color => (highestRole as Role).color;
/// Voice state of member
VoiceState? get voiceState => guild.voiceStates[this.id];
/// Returns total permissions of user.
Permissions get effectivePermissions {
if (this == guild.owner) {
return Permissions.all();
}
var total = (guild.everyoneRole as Role).permissions.raw;
for (final role in roles) {
if(role is! Role) {
continue;
}
total |= role.permissions.raw;
if (PermissionsUtils.isApplied(total, PermissionsConstants.administrator)) {
return Permissions.fromInt(PermissionsConstants.allPermissions);
}
}
return Permissions.fromInt(total);
}
// TODO: Remove duplicate
@override
bool _updateMember(String? nickname, List<IRole> roles) => super._updateMember(nickname, roles);
factory CacheMember._standard(Map<String, dynamic> data, Guild guild, Nyxx client) =>
CacheMember._new(data, data["user"] as Map<String, dynamic>, guild, client);
factory CacheMember._fromUser(Map<String, dynamic> dataUser, Map<String, dynamic> dataMember, Guild guild, Nyxx client) =>
CacheMember._new(dataMember, dataUser, guild, client);
CacheMember._new(Map<String, dynamic> raw, Map<String, dynamic> user, this.guild, Nyxx client) : super._new(raw, user, guild.id, client) {
this.roles = [
for(final role in this.roles)
// TODO: NNBD
this.guild.roles[role.id]!
];
}
}

View file

@ -13,7 +13,7 @@ class VoiceState {
Guild? guild;
/// Channel id user is connected
VoiceChannel? channel;
CacheVoiceChannel? channel;
/// Whether this user is muted by the server
late final bool deaf;
@ -29,7 +29,7 @@ class VoiceState {
VoiceState._new(Map<String, dynamic> raw, Nyxx client, [Guild? guild]) {
if (raw["channel_id"] != null) {
this.channel = client.channels[Snowflake(raw["channel_id"])] as VoiceChannel?;
this.channel = client.channels[Snowflake(raw["channel_id"])] as CacheVoiceChannel?;
}
this.deaf = raw["deaf"] as bool;

View file

@ -1,8 +0,0 @@
part of nyxx;
/// Thrown when token is empty or null
class NoTokenError implements Exception {
/// Returns a string representation of this object.
@override
String toString() => "NotSetupError: Token is null or empty!";
}

View file

@ -3,15 +3,15 @@ part of nyxx;
// TODO: Decide what about guild store channels
/// Sent when a channel is created.
class ChannelCreateEvent {
/// The channel that was created, either a [GuildChannel], [DMChannel], or [GroupDMChannel].
/// The channel that was created, either a [CacheGuildChannel], [DMChannel], or [GroupDMChannel].
late final Channel channel;
ChannelCreateEvent._new(Map<String, dynamic> raw, Nyxx client) {
this.channel = Channel._deserialize(raw, client);
this.channel = Channel._deserialize(raw["d"] as Map<String, dynamic>, client);
client.channels[channel.id] = channel;
if (this.channel is GuildChannel) {
(this.channel as GuildChannel).guild.channels[channel.id] = channel;
if (this.channel is CacheGuildChannel) {
(this.channel as CacheGuildChannel).guild.channels[channel.id] = channel;
}
}
}
@ -32,8 +32,8 @@ class ChannelDeleteEvent {
}
client.channels.remove(channelSnowflake);
if (this.channel is GuildChannel) {
(this.channel as GuildChannel).guild.channels.remove(channelSnowflake);
if (this.channel is CacheGuildChannel) {
(this.channel as CacheGuildChannel).guild.channels.remove(channelSnowflake);
}
}
}
@ -41,7 +41,7 @@ class ChannelDeleteEvent {
/// Fired when channel"s pinned messages are updated
class ChannelPinsUpdateEvent {
/// Channel where pins were updated
TextChannel? channel;
CacheTextChannel? channel;
/// ID of channel pins were updated
late final Snowflake channelId;
@ -57,7 +57,7 @@ class ChannelPinsUpdateEvent {
this.lastPingTimestamp = DateTime.parse(raw["d"]["last_pin_timestamp"] as String);
}
this.channel = client.channels[Snowflake(raw["d"]["channel_id"])] as TextChannel;
this.channel = client.channels[Snowflake(raw["d"]["channel_id"])] as CacheTextChannel?;
if (raw["d"]["guild_id"] != null) {
this.guildId = Snowflake(raw["d"]["guild_id"]);
@ -71,12 +71,12 @@ class ChannelUpdateEvent {
late final Channel updatedChannel;
ChannelUpdateEvent._new(Map<String, dynamic> raw, Nyxx client) {
this.updatedChannel = Channel._deserialize(raw, client);
this.updatedChannel = Channel._deserialize(raw["d"] as Map<String, dynamic>, client);
client.channels[this.updatedChannel.id] = updatedChannel;
if (this.updatedChannel is GuildChannel) {
(this.updatedChannel as GuildChannel).guild.channels[this.updatedChannel.id] = updatedChannel;
if (this.updatedChannel is CacheGuildChannel) {
(this.updatedChannel as CacheGuildChannel).guild.channels[this.updatedChannel.id] = updatedChannel;
}
}
}

View file

@ -5,8 +5,16 @@ class DisconnectEvent {
/// The shard that got disconnected.
Shard shard;
/// The close code.
int closeCode;
/// Reason of disconnection
DisconnectEventReason reason;
DisconnectEvent._new(this.shard, this.closeCode);
DisconnectEvent._new(this.shard, this.reason);
}
/// Reason why shard was disconnected.
class DisconnectEventReason extends IEnum<int> {
/// When shard is disconnected due invalid shard session.
static const DisconnectEventReason invalidSession = const DisconnectEventReason._from(9);
const DisconnectEventReason._from(int value) : super(value);
}

View file

@ -5,13 +5,9 @@ class GuildCreateEvent {
/// The guild created.
late final Guild guild;
GuildCreateEvent._new(Map<String, dynamic> raw, Shard shard, Nyxx client) {
GuildCreateEvent._new(Map<String, dynamic> raw, Nyxx client) {
this.guild = Guild._new(client, raw["d"] as Map<String, dynamic>, true, true);
client.guilds[guild.id] = guild;
if (client._options.forceFetchMembers) {
shard.send(OPCodes.requestGuildMember, {"guild_id": guild.id.toString(), "query": "", "limit": 0});
}
}
}
@ -46,7 +42,7 @@ class GuildDeleteEvent {
/// False if user was kicked from guild
late final bool unavailable;
GuildDeleteEvent._new(Map<String, dynamic> raw, Shard shard, Nyxx client) {
GuildDeleteEvent._new(Map<String, dynamic> raw, Nyxx client) {
this.guildId = Snowflake(raw["d"]["id"]);
this.unavailable = raw["d"]["unavailable"] as bool;
this.guild = client.guilds[this.guildId];
@ -89,7 +85,7 @@ class GuildMemberRemoveEvent {
/// Sent when a member is updated.
class GuildMemberUpdateEvent {
/// The member after the update if member is updated.
late final Member? member;
late final IMember? member;
/// User if user is updated. Will be null if member is not null.
late final User? user;
@ -101,16 +97,16 @@ class GuildMemberUpdateEvent {
return;
}
final member = guild.members[Snowflake(raw["d"]["user"]["id"])];
this.member = guild.members[Snowflake(raw["d"]["user"]["id"])];
if (member == null) {
if (this.member == null || this.member is! CacheMember) {
return;
}
final nickname = raw["d"]["nickname"] as String?;
final roles = (raw["d"]["roles"] as List<dynamic>).map((str) => guild.roles[Snowflake(str)]!).toList();
if (member._updateMember(nickname, roles)) {
if ((this.member as CacheMember)._updateMember(nickname, roles)) {
return;
}
@ -122,7 +118,7 @@ class GuildMemberUpdateEvent {
/// Sent when a member joins a guild.
class GuildMemberAddEvent {
/// The member that joined.
late final Member? member;
late final CacheMember? member;
GuildMemberAddEvent._new(Map<String, dynamic> raw, Nyxx client) {
final guild = client.guilds[Snowflake(raw["d"]["guild_id"])];
@ -131,7 +127,7 @@ class GuildMemberAddEvent {
return;
}
this.member = Member._standard(raw["d"] as Map<String, dynamic>, guild, client);
this.member = CacheMember._standard(raw["d"] as Map<String, dynamic>, guild, client);
guild.members[member!.id] = member!;
if (!client.users.hasKey(member!.id)) {
@ -205,20 +201,26 @@ class GuildBanRemoveEvent {
/// Fired when emojis are updated
class GuildEmojisUpdateEvent {
/// New list of changes emojis
late final Map<Snowflake, GuildEmoji> emojis;
final Map<Snowflake, GuildEmoji> emojis = {};
/// Id of guild where event happend
late final Snowflake guildId;
/// Instance of guild if available
late final Guild? guild;
GuildEmojisUpdateEvent._new(Map<String, dynamic> json, Nyxx client) {
if (client.ready) {
final guild = client.guilds[Snowflake(json["d"]["guild_id"])];
this.guildId = Snowflake(json["d"]["guild_id"]);
this.guild = client.guilds[this.guildId];
for(final rawEmoji in json["d"]["emojis"]) {
final emoji = GuildEmoji._new(rawEmoji as Map<String, dynamic>, guildId, client);
this.emojis[emoji.id] = emoji;
emojis = {};
if (guild != null) {
json["d"]["emojis"].forEach((o) {
final emoji = GuildEmoji._new(o as Map<String, dynamic>, guild, client);
guild.emojis[emoji.id] = emoji;
emojis[emoji.id] = emoji;
});
guild!.emojis[emoji.id] = emoji;
emojis[emoji.id] = emoji;
}
}
}
@ -227,56 +229,69 @@ class GuildEmojisUpdateEvent {
/// Sent when a role is created.
class RoleCreateEvent {
/// The role that was created.
Role? role;
late final Role role;
/// Id of guild where event happend
late final Snowflake guildId;
/// Instance of [Guild] if available
late final Guild? guild;
RoleCreateEvent._new(Map<String, dynamic> json, Nyxx client) {
if (client.ready) {
final guild = client.guilds[Snowflake(json["d"]["guild_id"])];
this.guildId = Snowflake(json["d"]["guild_id"]);
this.guild = client.guilds[this.guildId];
if (guild != null) {
this.role = Role._new(json["d"]["role"] as Map<String, dynamic>, guild, client);
guild.roles[role!.id] = role!;
}
this.role = Role._new(json["d"]["role"] as Map<String, dynamic>, guildId, client);
if (guild != null) {
guild!.roles[role.id] = role;
}
}
}
/// Sent when a role is deleted.
class RoleDeleteEvent {
/// The role that was deleted.
Role? role;
/// The role that was deleted, if available
IRole? role;
/// Id of tole that was deleted
late final Snowflake roleId;
/// Id of guild where event happend
late final Snowflake guildId;
/// Instance of [Guild] if available
late final Guild? guild;
RoleDeleteEvent._new(Map<String, dynamic> json, Nyxx client) {
final guild = client.guilds[Snowflake(json["d"]["guild_id"])];
this.guildId = Snowflake(json["d"]["guild_id"]);
this.guild = client.guilds[this.guildId];
this.roleId = Snowflake(json["d"]["role_id"]);
if (guild != null) {
this.role = guild.roles[Snowflake(json["d"]["role_id"])];
guild.roles.remove(role!.id);
this.role = guild!.roles[this.roleId];
guild!.roles.remove(role!.id);
}
}
}
/// Sent when a role is updated.
class RoleUpdateEvent {
/// The role prior to the update.
Role? oldRole;
/// The role after the update.
Role? newRole;
late final Role role;
/// Id of guild where event happend
late final Snowflake guildId;
/// Instance of [Guild] if available
late final Guild? guild;
RoleUpdateEvent._new(Map<String, dynamic> json, Nyxx client) {
final guild = client.guilds[Snowflake(json["d"]["guild_id"])];
this.guildId = Snowflake(json["d"]["guild_id"]);
this.guild = client.guilds[this.guildId];
this.role = Role._new(json["d"]["role"] as Map<String, dynamic>, guildId, client);
if (guild != null) {
this.oldRole = guild.roles[Snowflake(json["d"]["role"]["id"])];
this.newRole = Role._new(json["d"]["role"] as Map<String, dynamic>, guild, client);
if (oldRole != null) {
oldRole!.guild.roles[oldRole!.id] = newRole!;
} else {
guild.roles.add(newRole!.id, newRole!);
}
this.guild!.roles[role.id] = role;
}
}
}

View file

@ -4,7 +4,7 @@ part of nyxx;
/// You can use the `chunk_index` and `chunk_count` to calculate how many chunks are left for your request.
class MemberChunkEvent {
/// Guild members
late final Iterable<Member> members;
late final Iterable<IMember> members;
/// Id of guild
late final Snowflake guildId;
@ -27,7 +27,10 @@ class MemberChunkEvent {
/// Nonce is used to identify events.
String? nonce;
MemberChunkEvent._new(Map<String, dynamic> raw, Nyxx client) {
/// Id of shard where chunk was received
final int shardId;
MemberChunkEvent._new(Map<String, dynamic> raw, Nyxx client, this.shardId) {
this.chunkIndex = raw["d"]["chunk_index"] as int;
this.chunkCount = raw["d"]["chunk_count"] as int;
@ -44,7 +47,7 @@ class MemberChunkEvent {
this.members = [
for (var memberRaw in raw["d"]["members"])
Member._standard(memberRaw as Map<String, dynamic>, this.guild!, client)
CacheMember._standard(memberRaw as Map<String, dynamic>, this.guild!, client)
];
// TODO: Thats probably redundant

View file

@ -104,7 +104,7 @@ abstract class MessageReactionEvent {
if (json["d"]["emoji"]["id"] == null) {
this.emoji = UnicodeEmoji(json["d"]["emoji"]["name"] as String);
} else {
this.emoji = GuildEmoji._partial(json["d"]["emoji"] as Map<String, dynamic>);
this.emoji = PartialGuildEmoji._new(json["d"]["emoji"] as Map<String, dynamic>);
}
}
}
@ -225,7 +225,8 @@ class MessageReactionRemoveEmojiEvent {
if (json["d"]["emoji"]["id"] == null) {
this.emoji = UnicodeEmoji(json["d"]["emoji"]["name"] as String);
} else {
this.emoji = GuildEmoji._partial(json["d"]["emoji"] as Map<String, dynamic>);
// TODO: emojis stuff
//this.emoji = GuildEmoji._partial(json["d"]["emoji"] as Map<String, dynamic>);
}
if (this.message != null) {

View file

@ -3,7 +3,7 @@ part of nyxx;
/// Sent when a user starts typing.
class TypingEvent {
/// The channel that the user is typing in.
MessageChannel? channel;
ITextChannel? channel;
/// Id of the channel that the user is typing in.
late final Snowflake channelId;
@ -19,7 +19,7 @@ class TypingEvent {
TypingEvent._new(Map<String, dynamic> json, Nyxx client) {
this.channelId = Snowflake(json["d"]["channel_id"]);
this.channel = client.channels[channelId] as MessageChannel?;
this.channel = client.channels[channelId] as ITextChannel?;
this.userId = Snowflake(json["d"]["user_id"]);
this.user = client.users[this.userId];

View file

@ -2,7 +2,6 @@ part of nyxx;
/// Emitted when guild's voice server changes
class VoiceServerUpdateEvent {
/// Raw websocket event payload
final Map<String, dynamic> raw;

View file

@ -33,4 +33,8 @@ class Constants {
/// Url to Nyxx repo
static const String repoUrl = "https://github.com/l7ssha/nyxx";
// TODO: invesitgate &compress=zlib-stream
/// Returns [Uri] to gateway
static Uri gatewayUri(String gatewayHost) => Uri.parse("$gatewayHost?v=6&encoding=json");
}

View file

@ -9,58 +9,59 @@ class _WS {
late int remaining;
late DateTime resetAt;
late int recommendedShardsNum;
final Logger logger = Logger("Client");
final Logger _logger = Logger("Client");
int _shardsReady = 0;
/// Makes a new WS manager.
_WS(this._client) {
_client._http._execute(BasicRequest._new("/gateway/bot")).then((httpResponse) {
if (httpResponse is HttpResponseError) {
this.logger.severe("Cannot get gateway url");
this._logger.severe("Cannot get gateway url: [${httpResponse.errorCode}; ${httpResponse.errorMessage}]");
exit(1);
}
final response = httpResponse as HttpResponseSuccess;
this.gateway = response.jsonBody["url"] as String;
this.remaining = response.jsonBody["session_start_limit"]["remaining"] as int;
this.resetAt =
DateTime.now().add(Duration(milliseconds: response.jsonBody["session_start_limit"]["reset_after"] as int));
logger.info("Remaining ${this.remaining} connections starts. Limit will reset at ${this.resetAt}");
this.resetAt = DateTime.now().add(Duration(milliseconds: response.jsonBody["session_start_limit"]["reset_after"] as int));
this.recommendedShardsNum = response.jsonBody["shards"] as int;
checkForConnections();
setupShard(_client._options.shardIndex);
this.connectShard(0);
this._client.shardManager = ShardManager._new(this, this._client._options.shardCount != null ? this._client._options.shardCount! : this.recommendedShardsNum);
});
}
void checkForConnections() {
if (this.remaining < 50) logger.warning("50 connection starts left.");
_logger.info("Remaining ${this.remaining} connections starts. Limit will reset at ${this.resetAt}");
if (this.remaining < 50) {
_logger.warning("50 connection starts left.");
}
if (this.remaining < 10) {
logger.severe("Exiting to prevent API abuse. 10 connections starts left.");
_logger.severe("Exiting to prevent API abuse. 10 connections starts left.");
exit(1);
}
}
void setupShard(int shardId) {
final shard = Shard._new(this, shardId);
_client.shard = shard;
}
void connectShard(int index) {
_client.shard._connect(false, true);
}
Future<void> propagateReady() async {
this._shardsReady++;
if(_client.ready || this._shardsReady < (_client._options.shardCount ?? 1)) {
return;
}
_client.ready = true;
final httpResponse = await _client._http._execute(BasicRequest._new("/oauth2/applications/@me"));
if (httpResponse is HttpResponseError) {
this.logger.severe("Cannot get bot identity");
this._logger.severe("Cannot get bot identity");
exit(1);
}
@ -69,6 +70,6 @@ class _WS {
_client.app = ClientOAuth2Application._new(response.jsonBody as Map<String, dynamic>, _client);
_client._events.onReady.add(ReadyEvent._new(_client));
logger.info("Connected and ready! Logged as `${_client.self.tag}`");
_logger.info("Connected and ready! Logged as `${_client.self.tag}`");
}
}

View file

@ -0,0 +1,12 @@
part of nyxx;
/// Thrown when embed doesnt meet requirements to be valid
class EmbedBuilderArgumentException implements Exception {
/// Custom error message specific to context of exception
final String message;
EmbedBuilderArgumentException._new(this.message);
@override
String toString() => "EmbedBuilderArgumentException: $message";
}

View file

@ -0,0 +1,12 @@
part of nyxx;
/// Thrown when operation is unsupported due invalid or wrong shard being accessed.
class InvalidShardException implements Exception {
/// Custom error message specific to context of exception
final String message;
InvalidShardException._new(this.message);
@override
String toString() => "InvalidShardException: Unsupported shard operation: $message";
}

View file

@ -0,0 +1,11 @@
part of nyxx;
/// Thrown when cannot convert provided data to [Snowflake]
class InvalidSnowflakeException implements Exception {
final dynamic _invalidSnowflake;
InvalidSnowflakeException._new(this._invalidSnowflake);
@override
String toString() => "InvalidSnowflakeException: Cannot parse [$_invalidSnowflake] to Snowflake";
}

View file

@ -0,0 +1,11 @@
part of nyxx;
/// Thrown when token is empty or null
class MissingTokenError implements Error {
/// Returns a string representation of this object.
@override
String toString() => "MissingTokenError: Token is null or empty!";
@override
StackTrace? get stackTrace => StackTrace.empty;
}

View file

@ -8,14 +8,14 @@ abstract class _HttpRequest {
final bool ratelimit;
Nyxx? _client;
late Nyxx _client;
_HttpRequest._new(String path, {this.method = "GET", this.queryParams, this.auditLog, this.ratelimit = true}) {
this.uri = Uri.https(Constants.host, Constants.baseUri + path);
}
Map<String, String> _genHeaders() => {
"Authorization": "Bot ${_client?._token}",
"Authorization": "Bot ${_client._token}",
if (this.auditLog != null) "X-Audit-Log-Reason": this.auditLog!,
"User-Agent": "Nyxx (${Constants.repoUrl}, ${Constants.version})"
};

View file

@ -2,7 +2,7 @@ part of nyxx;
/// Specifies objects which can be converted to [Builder]
// ignore: one_member_abstracts
abstract class Convertable<T extends Builder> {
abstract class Convertable<T> {
/// Returns instance of [Builder] with current data
T toBuilder();
}

View file

@ -1,7 +1,7 @@
part of nyxx;
//TODO: Consider what should be here and what should not
/// Could be either [User], [Member] or [Webhook].
/// Could be either [User], [CacheMember] or [Webhook].
/// [Webhook] will have most of field missing.
abstract class IMessageAuthor implements SnowflakeEntity {
/// User name

View file

@ -17,7 +17,7 @@ abstract class ISend {
Map<String, dynamic> _initMessage(dynamic content, EmbedBuilder? embed, AllowedMentions? allowedMentions) {
if (content == null && embed == null) {
throw Exception("When sending message both content and embed cannot be null");
throw ArgumentError("When sending message both content and embed cannot be null");
}
allowedMentions ??= client._options.allowedMentions;

View file

@ -0,0 +1,422 @@
part of nyxx;
class Shard implements Disposable {
/// Id of shard
final int id;
/// Reference to [ShardManager]
final ShardManager manager;
/// Emitted when the shard encounters a connection error
late final Stream<Shard> onDisconnect = manager.onDisconnect.where((event) => event.id == this);
/// Emitted when shard receives member chunk.
late final Stream<MemberChunkEvent> onMemberChunk = manager.onMemberChunk.where((event) => event.shardId == this.id);
/// List of handled guild ids
final List<Snowflake> guilds = [];
/// Gets the latest gateway latency.
///
/// To calculate the gateway latency, nyxx measures the time it takes for Discord to answer the gateway
/// heartbeat packet with a heartbeat ack packet. Note this value is updated each time gateway responses to ack.
Duration get gatewayLatency => _gatewayLatency;
/// Returns true if shard is connected to websocket
bool get connected => _connected;
late final Isolate _shardIsolate; // Reference to isolate
late final Stream<dynamic> _receiveStream; // Broadcast stream on which data from isolate is received
late final ReceivePort _receivePort; // Port on which data from isolate is received
late final SendPort _isolateSendPort; // Port on which data can be sent to isolate
late SendPort _sendPort; // Sendport for isolate
String? _sessionId; // Id of gateway session
int _sequence = 0; // Event sequence
late Timer _heartbeatTimer; // Heartbeat time
bool _connected = false; // Connection status
bool _resume = false; // Resume status
Duration _gatewayLatency = const Duration(); // latency of discord
late DateTime _lastHeartbeatSent; // Datetime when last heartbeat was sent
bool _heartbeatAckReceived = false; // True if last heartbeat was acked
Shard._new(this.id, this.manager, String gatewayUrl) {
this._receivePort = ReceivePort();
this._receiveStream = _receivePort.asBroadcastStream();
this._isolateSendPort = _receivePort.sendPort;
Isolate.spawn(_shardHandler, _isolateSendPort).then((isolate) async {
this._shardIsolate = isolate;
this._sendPort = await _receiveStream.first as SendPort;
this._sendPort.send({"cmd" : "INIT", "gatewayUrl" : gatewayUrl });
this._receiveStream.listen(_handle);
});
}
/// Sends WS data.
void send(int opCode, dynamic d) {
this._sendPort.send({"cmd": "SEND", "data" : {"op": opCode, "d": d}});
}
/// Updates clients voice state for [Guild] with given [guildId]
void changeVoiceState(Snowflake? guildId, Snowflake? channelId, {bool selfMute = false, bool selfDeafen = false}) {
this.send(OPCodes.voiceStateUpdate, <String, dynamic> {
"guild_id" : guildId.toString(),
"channel_id" : channelId?.toString(),
"self_mute" : selfMute,
"self_deaf" : selfDeafen
});
}
/// Allows to set presence for current shard.
void setPresence(PresenceBuilder presenceBuilder) {
this.send(OPCodes.statusUpdate, presenceBuilder._build());
}
/// Syncs all guilds
void guildSync() => this.send(OPCodes.guildSync, this.guilds.map((e) => e.toString()));
/// Allows to request members objects from gateway
/// [guild] can be either Snowflake or Iterable<Snowflake>
void requestMembers(/* Snowflake|Iterable<Snowflake> */ dynamic guild,
{String? query, Iterable<Snowflake>? userIds, int limit = 0, bool presences = false, String? nonce}) {
if (query != null && userIds != null) {
throw ArgumentError("Both `query` and userIds cannot be specified.");
}
dynamic guildPayload;
if (guild is Snowflake) {
if(!this.guilds.contains(guild)) {
throw InvalidShardException._new("Cannot request member for guild on wrong shard");
}
guildPayload = guild.toString();
} else if (guild is Iterable<Snowflake>) {
if(!this.guilds.any((element) => guild.contains(element))) {
throw InvalidShardException._new("Cannot request member for guild on wrong shard");
}
guildPayload = guild.map((e) => e.toString()).toList();
} else {
throw ArgumentError("Guild has to be either Snowflake or Iterable<Snowflake>");
}
final payload = <String, dynamic>{
"guild_id": guildPayload,
if (query != null) "query": query,
if (userIds != null) "user_ids": userIds.map((e) => e.toString()).toList(),
"limit": limit,
"presences": presences,
if (nonce != null) "nonce": nonce
};
this.send(OPCodes.requestGuildMember, payload);
}
void _heartbeat() {
this.send(OPCodes.heartbeat, _sequence == 0 ? null : _sequence);
this._lastHeartbeatSent = DateTime.now();
if(!this._heartbeatAckReceived) {
manager._logger.warning("Not received previous heartbeat ack");
return;
}
this._heartbeatAckReceived = false;
}
void _handleError(dynamic data) {
final closeCode = data["errorCode"] as int;
this._connected = false;
this._heartbeatTimer.cancel();
manager._logger.severe("Shard $id disconnected. Error code: [${data['errorCode']}] | Error message: [${data['errorReason']}]");
switch (closeCode) {
case 4004:
case 4010:
exit(1);
break;
case 4013:
manager._logger.shout("Cannot connect to gateway due intent value is invalid. "
"Check https://discordapp.com/developers/docs/topics/gateway#gateway-intents for more info.");
exit(1);
break;
case 4014:
manager._logger.shout("You sent a disallowed intent for a Gateway Intent. "
"You may have tried to specify an intent that you have not enabled or are not whitelisted for. "
"Check https://discordapp.com/developers/docs/topics/gateway#gateway-intents for more info.");
exit(1);
break;
case 4007:
case 4009:
_reconnect();
break;
default:
_connect();
break;
}
}
// Connects to gateway
void _connect() {
manager._logger.info("Connecting to gateway on shard $id!");
this._resume = false;
Future.delayed(const Duration(seconds: 2), () => this._sendPort.send({ "cmd" : "CONNECT"}));
}
// Reconnects to gateway
void _reconnect() {
manager._logger.info("Resuming connection to gateway on shard $id!");
this._resume = true;
Future.delayed(const Duration(seconds: 1), () => this._sendPort.send({ "cmd" : "CONNECT"}));
}
Future<void> _handle(dynamic rawData) async {
if(rawData["cmd"] == "CONNECT_ACK") {
manager._logger.info("Shard $id connected to gateway!");
return;
}
if(rawData["cmd"] == "ERROR" || rawData["cmd"] == "DISCONNECTED") {
_handleError(rawData);
return;
}
if(rawData["jsonData"] == null) {
return;
}
final discordPayload = rawData["jsonData"] as Map<String, dynamic>;
if (discordPayload["op"] == OPCodes.dispatch && manager._ws._client._options.ignoredEvents.contains(discordPayload["t"] as String)) {
return;
}
if (discordPayload["s"] != null) {
this._sequence = discordPayload["s"] as int;
}
await _dispatch(discordPayload);
}
Future<void> _dispatch(Map<String, dynamic> rawPayload) async {
switch (rawPayload["op"] as int) {
case OPCodes.heartbeatAck:
this._heartbeatAckReceived = true;
this._gatewayLatency = DateTime.now().difference(this._lastHeartbeatSent);
break;
case OPCodes.hello:
if (this._sessionId == null || !_resume) {
final identifyMsg = <String, dynamic>{
"token": manager._ws._client._token,
"properties": <String, dynamic> {
"\$os": Platform.operatingSystem,
"\$browser": "nyxx",
"\$device": "nyxx",
},
"large_threshold": manager._ws._client._options.largeThreshold,
"compress": manager._ws._client._options.compressedGatewayPayloads,
"guild_subscriptions" : manager._ws._client._options.guildSubscriptions,
if (manager._ws._client._options.initialPresence != null)
"presence" : manager._ws._client._options.initialPresence!._build()
};
if (manager._ws._client._options.gatewayIntents != null) {
identifyMsg["intents"] = manager._ws._client._options.gatewayIntents!._calculate();
}
identifyMsg["shard"] = <int>[this.id, manager._numShards];
this.send(OPCodes.identify, identifyMsg);
} else if (_resume) {
this.send(OPCodes.resume,
<String, dynamic>{"token": manager._ws._client._token, "session_id": this._sessionId, "seq": this._sequence});
}
this._heartbeatTimer = Timer.periodic(
Duration(milliseconds: rawPayload["d"]["heartbeat_interval"] as int), (Timer t) => this._heartbeat());
break;
case OPCodes.invalidSession:
manager._logger.severe("Invalid session on shard $id. ${(rawPayload["d"] as bool) ? "Resuming..." : "Reconnecting..."}");
_heartbeatTimer.cancel();
manager._ws._client._events.onDisconnect.add(DisconnectEvent._new(this, DisconnectEventReason.invalidSession));
if (rawPayload["d"] as bool) {
_reconnect();
} else {
_connect();
}
break;
case OPCodes.dispatch:
final j = rawPayload["t"] as String;
switch (j) {
case "READY":
this._sessionId = rawPayload["d"]["session_id"] as String;
manager._ws._client.self = ClientUser._new(rawPayload["d"]["user"] as Map<String, dynamic>, manager._ws._client);
this._connected = true;
manager._logger.info("Shard ${this.id} ready!");
if (!_resume) {
await manager._ws.propagateReady();
}
break;
case "GUILD_MEMBERS_CHUNK":
manager._onMemberChunk.add(MemberChunkEvent._new(rawPayload, manager._ws._client, this.id));
break;
case "MESSAGE_REACTION_REMOVE_ALL":
manager._ws._client._events.onMessageReactionsRemoved.add(MessageReactionsRemovedEvent._new(rawPayload, manager._ws._client));
break;
case "MESSAGE_REACTION_ADD":
MessageReactionAddedEvent._new(rawPayload, manager._ws._client);
break;
case "MESSAGE_REACTION_REMOVE":
MessageReactionRemovedEvent._new(rawPayload, manager._ws._client);
break;
case "MESSAGE_DELETE_BULK":
manager._ws._client._events.onMessageDeleteBulk.add(MessageDeleteBulkEvent._new(rawPayload, manager._ws._client));
break;
case "CHANNEL_PINS_UPDATE":
manager._ws._client._events.onChannelPinsUpdate.add(ChannelPinsUpdateEvent._new(rawPayload, manager._ws._client));
break;
case "VOICE_STATE_UPDATE":
manager._ws._client._events.onVoiceStateUpdate.add(VoiceStateUpdateEvent._new(rawPayload, manager._ws._client));
break;
case "VOICE_SERVER_UPDATE":
manager._ws._client._events.onVoiceServerUpdate.add(VoiceServerUpdateEvent._new(rawPayload, manager._ws._client));
break;
case "GUILD_EMOJIS_UPDATE":
manager._ws._client._events.onGuildEmojisUpdate.add(GuildEmojisUpdateEvent._new(rawPayload, manager._ws._client));
break;
case "MESSAGE_CREATE":
manager._ws._client._events.onMessageReceived.add(MessageReceivedEvent._new(rawPayload, manager._ws._client));
break;
case "MESSAGE_DELETE":
manager._ws._client._events.onMessageDelete.add(MessageDeleteEvent._new(rawPayload, manager._ws._client));
break;
case "MESSAGE_UPDATE":
manager._ws._client._events.onMessageUpdate.add(MessageUpdateEvent._new(rawPayload, manager._ws._client));
break;
case "GUILD_CREATE":
final event = GuildCreateEvent._new(rawPayload, manager._ws._client);
this.guilds.add(event.guild.id);
manager._ws._client._events.onGuildCreate.add(event);
break;
case "GUILD_UPDATE":
manager._ws._client._events.onGuildUpdate.add(GuildUpdateEvent._new(rawPayload, manager._ws._client));
break;
case "GUILD_DELETE":
manager._ws._client._events.onGuildDelete.add(GuildDeleteEvent._new(rawPayload, manager._ws._client));
break;
case "GUILD_BAN_ADD":
manager._ws._client._events.onGuildBanAdd.add(GuildBanAddEvent._new(rawPayload, manager._ws._client));
break;
case "GUILD_BAN_REMOVE":
manager._ws._client._events.onGuildBanRemove.add(GuildBanRemoveEvent._new(rawPayload, manager._ws._client));
break;
case "GUILD_MEMBER_ADD":
manager._ws._client._events.onGuildMemberAdd.add(GuildMemberAddEvent._new(rawPayload, manager._ws._client));
break;
case "GUILD_MEMBER_REMOVE":
manager._ws._client._events.onGuildMemberRemove.add(GuildMemberRemoveEvent._new(rawPayload, manager._ws._client));
break;
case "GUILD_MEMBER_UPDATE":
manager._ws._client._events.onGuildMemberUpdate.add(GuildMemberUpdateEvent._new(rawPayload, manager._ws._client));
break;
case "CHANNEL_CREATE":
manager._ws._client._events.onChannelCreate.add(ChannelCreateEvent._new(rawPayload, manager._ws._client));
break;
case "CHANNEL_UPDATE":
manager._ws._client._events.onChannelUpdate.add(ChannelUpdateEvent._new(rawPayload, manager._ws._client));
break;
case "CHANNEL_DELETE":
manager._ws._client._events.onChannelDelete.add(ChannelDeleteEvent._new(rawPayload, manager._ws._client));
break;
case "TYPING_START":
manager._ws._client._events.onTyping.add(TypingEvent._new(rawPayload, manager._ws._client));
break;
case "PRESENCE_UPDATE":
manager._ws._client._events.onPresenceUpdate.add(PresenceUpdateEvent._new(rawPayload, manager._ws._client));
break;
case "GUILD_ROLE_CREATE":
manager._ws._client._events.onRoleCreate.add(RoleCreateEvent._new(rawPayload, manager._ws._client));
break;
case "GUILD_ROLE_UPDATE":
manager._ws._client._events.onRoleUpdate.add(RoleUpdateEvent._new(rawPayload, manager._ws._client));
break;
case "GUILD_ROLE_DELETE":
manager._ws._client._events.onRoleDelete.add(RoleDeleteEvent._new(rawPayload, manager._ws._client));
break;
case "USER_UPDATE":
manager._ws._client._events.onUserUpdate.add(UserUpdateEvent._new(rawPayload, manager._ws._client));
break;
case "INVITE_CREATE":
manager._ws._client._events.onInviteCreated.add(InviteCreatedEvent._new(rawPayload, manager._ws._client));
break;
case "INVITE_DELETE":
manager._ws._client._events.onInviteDelete.add(InviteDeletedEvent._new(rawPayload, manager._ws._client));
break;
case "MESSAGE_REACTION_REMOVE_EMOJI":
manager._ws._client._events.onMessageReactionRemoveEmoji
.add(MessageReactionRemoveEmojiEvent._new(rawPayload, manager._ws._client));
break;
default:
print("UNKNOWN OPCODE: ${jsonEncode(rawPayload)}");
}
break;
}
}
@override
Future<void> dispose() async {
this.manager._logger.info("Started disposing shard $id...");
await this._receiveStream.firstWhere((element) => (element as Map<String, dynamic>)["cmd"] == "TERMINATE_OK");
this._shardIsolate.kill(priority: Isolate.immediate);
this.manager._logger.info("Shard $id disposed.");
}
}

View file

@ -0,0 +1,74 @@
part of nyxx;
/// Spawns, connects, monitors, manages and terminates shards.
/// Sharding will be automatic if no user settings are supplied in
/// [ClientOptions] when instantiating [Nyxx] client instance.
///
/// Discord gateways implement a method of user-controlled guild sharding which
/// allows for splitting events across a number of gateway connections.
/// Guild sharding is entirely user controlled, and requires no state-sharing
/// between separate connections to operate.
class ShardManager implements Disposable {
/// Emitted when the shard is ready.
late Stream<Shard> onConnected = this._onConnect.stream;
/// Emitted when the shard encounters a connection error.
late Stream<Shard> onDisconnect = this._onDisconnect.stream;
/// Emitted when shard receives member chunk.
late Stream<MemberChunkEvent> onMemberChunk = this._onMemberChunk.stream;
final StreamController<Shard> _onConnect = StreamController.broadcast();
final StreamController<Shard> _onDisconnect = StreamController.broadcast();
final StreamController<MemberChunkEvent> _onMemberChunk = StreamController.broadcast();
final Logger _logger = Logger("Shard Manager");
/// List of shards
Iterable<Shard> get shards => List.unmodifiable(_shards.values);
/// Average gateway latency across all shards
Duration get gatewayLatency
=> Duration(milliseconds: (this.shards.map((e) => e.gatewayLatency.inMilliseconds)
.fold<int>(0, (first, second) => first + second)) ~/ shards.length);
final _WS _ws;
final int _numShards;
final Map<int, Shard> _shards = {};
/// Starts shard manager
ShardManager._new(this._ws, this._numShards) {
_connect(_numShards - 1);
}
/// Sets presences on every shard
void setPresence(PresenceBuilder presenceBuilder) {
for (final shard in shards) {
shard.setPresence(presenceBuilder);
}
}
void _connect(int shardId) {
if(shardId < 0) {
return;
}
final shard = Shard._new(shardId, this, _ws.gateway);
_shards[shardId] = shard;
Future.delayed(const Duration(seconds: 1, milliseconds: 500), () => _connect(shardId - 1));
}
@override
Future<void> dispose() async {
this._logger.info("Closing gateway connections...");
for(final shard in this._shards.values) {
shard.dispose();
}
await this._onConnect.close();
await this._onDisconnect.close();
await this._onMemberChunk.close();
}
}

View file

@ -0,0 +1,101 @@
part of nyxx;
// Decodes zlib compresses string into string json
Map<String, dynamic> _decodeBytes(dynamic rawPayload, ZLibDecoder decoder) {
if (rawPayload is String) {
return jsonDecode(rawPayload) as Map<String, dynamic>;
}
// print("Size: ${(rawPayload as List<int>).length} bytes");
final decoded = decoder.convert(rawPayload as List<int>);
final rawStr = utf8.decode(decoded);
return jsonDecode(rawStr) as Map<String, dynamic>;
}
/*
Protocol used to communicate with shard isolate.
First message delivered to shardHandler will be init message with gateway uri
* DATA - sent along with data received from websocket
* DISCONNECTED - sent when shard disconnects
* ERROR - sent when error occurs
* INIT - inits ws connection
* CONNECT - sent when ws connection is established. additional data can contain if reconnected.
* SEND - sent along with data to send via websocket
*/
Future<void> _shardHandler(SendPort shardPort) async {
/// Port init
final receivePort = ReceivePort();
final receiveStream = receivePort.asBroadcastStream();
final sendPort = receivePort.sendPort;
shardPort.send(sendPort);
/// Initial data init
final initData = await receiveStream.first;
final gatewayUri = Constants.gatewayUri(initData["gatewayUrl"] as String);
transport.WebSocket? _socket;
StreamSubscription? _socketSubscription;
transport_vm.configureWTransportForVM();
Future<void> terminate() async {
await _socketSubscription?.cancel();
await _socket?.close(1000);
shardPort.send({ "cmd" : "TERMINATE_OK" });
}
if(!Platform.isWindows) {
// ignore: unawaited_futures
ProcessSignal.sigterm.watch().forEach((event) async {
await terminate();
});
}
// ignore: unawaited_futures
ProcessSignal.sigint.watch().forEach((event) async {
await terminate();
});
// Attempts to connect to ws
Future<void> _connect() async {
await transport.WebSocket.connect(gatewayUri).then((ws) {
final zlibDecoder = ZLibDecoder(); // Create zlib decoder specific to this connection. New connection should get new zlib context
_socket = ws;
_socketSubscription = _socket!.listen((data) {
shardPort.send({ "cmd" : "DATA", "jsonData" : _decodeBytes(data, zlibDecoder) });
}, onDone: () async {
shardPort.send({ "cmd" : "DISCONNECTED", "errorCode" : _socket!.closeCode, "errorReason" : _socket!.closeReason });
}, cancelOnError: true, onError: (err) => shardPort.send({ "cmd" : "ERROR", "error": err.toString(), "errorCode" : _socket!.closeCode, "errorReason" : _socket!.closeReason }));
shardPort.send({ "cmd" : "CONNECT_ACK" });
}, onError: (err, __) => shardPort.send({ "cmd" : "ERROR", "error": err.toString(), "errorCode" : _socket!.closeCode, "errorReason" : _socket!.closeReason }));
}
// Connects
await _connect();
await for(final message in receiveStream) {
final cmd = message["cmd"];
if(cmd == "SEND") {
if(_socket?.closeCode == null) {
_socket?.add(jsonEncode(message["data"]));
}
continue;
}
if(cmd == "CONNECT") {
await _socketSubscription?.cancel();
await _socket?.close(1000);
await _connect();
continue;
}
}
}

View file

@ -1,10 +1,13 @@
part of nyxx;
/// Abstract interface for enums in library
abstract class IEnum<T> {
final T _value;
/// Returns value of enum
T get value => _value;
/// Creates enum with given value
const IEnum(this._value);
@override

View file

@ -19,13 +19,13 @@ class EmbedAuthorBuilder implements Builder {
/// Builds object to Map() instance;
Map<String, dynamic> _build() {
if (this.name == null || this.name!.isEmpty) {
throw Exception("Author name cannot be null or empty");
throw EmbedBuilderArgumentException._new("Author name cannot be null or empty");
}
if (this.length! > 256) {
throw Exception("Author name is too long. (256 characters limit)");
throw EmbedBuilderArgumentException._new("Author name is too long. (256 characters limit)");
}
return <String, dynamic>{"name": name, if (url != null) "url": url, if (iconUrl != null) "icon_url": iconUrl};
return <String, dynamic>{ "name": name, if (url != null) "url": url, if (iconUrl != null) "icon_url": iconUrl};
}
}

View file

@ -33,11 +33,11 @@ class EmbedBuilder implements Builder {
EmbedAuthorBuilder? author;
/// Embed custom fields;
late final List<EmbedFieldBuilder> _fields;
late final List<EmbedFieldBuilder> fields;
/// Creates clean instance [EmbedBuilder]
EmbedBuilder() {
_fields = [];
fields = [];
}
/// Adds author to embed.
@ -60,46 +60,57 @@ class EmbedBuilder implements Builder {
Function(EmbedFieldBuilder field)? builder,
EmbedFieldBuilder? field}) {
if (field != null) {
_fields.add(field);
fields.add(field);
return;
}
if (builder != null) {
final tmp = EmbedFieldBuilder();
builder(tmp);
_fields.add(tmp);
fields.add(tmp);
return;
}
_fields.add(EmbedFieldBuilder(name, content, inline));
fields.add(EmbedFieldBuilder(name, content, inline));
}
/// Total lenght of all text fields of embed
/// Replaces field where [name] witch provided new field.
void replaceField({dynamic? name,
dynamic? content,
bool inline = false,
Function(EmbedFieldBuilder field)? builder,
EmbedFieldBuilder? field}) {
this.fields.removeWhere((element) => element.name == name);
this.addField(name: name, content: content, inline: inline, builder: builder, field: field);
}
/// Total length of all text fields of embed
int get length =>
(this.title?.length ?? 0) +
(this.description?.length ?? 0) +
(this.footer?.length ?? 0) +
(this.author?.length ?? 0) +
(_fields.isEmpty ? 0 : _fields.map((embed) => embed.length).reduce((f, s) => f + s));
(fields.isEmpty ? 0 : fields.map((embed) => embed.length).reduce((f, s) => f + s));
@override
/// Builds object to Map() instance;
Map<String, dynamic> _build() {
if (this.title != null && this.title!.length > 256) {
throw Exception("Embed title is too long (256 characters limit)");
throw EmbedBuilderArgumentException._new("Embed title is too long (256 characters limit)");
}
if (this.description != null && this.description!.length > 2048) {
throw Exception("Embed description is too long (2048 characters limit)");
throw EmbedBuilderArgumentException._new("Embed description is too long (2048 characters limit)");
}
if (this._fields.length > 25) {
throw Exception("Embed cannot contain more than 25 fields");
if (this.fields.length > 25) {
throw EmbedBuilderArgumentException._new("Embed cannot contain more than 25 fields");
}
if (this.length > 6000) {
throw Exception("Total length of embed cannot exceed 6000 characters");
throw EmbedBuilderArgumentException._new("Total length of embed cannot exceed 6000 characters");
}
return <String, dynamic>{
@ -113,7 +124,7 @@ class EmbedBuilder implements Builder {
if (imageUrl != null) "image": <String, dynamic>{"url": imageUrl},
if (thumbnailUrl != null) "thumbnail": <String, dynamic>{"url": thumbnailUrl},
if (author != null) "author": author!._build(),
if (_fields.isNotEmpty) "fields": _fields.map((builder) => builder._build()).toList()
if (fields.isNotEmpty) "fields": fields.map((builder) => builder._build()).toList()
};
}
}

View file

@ -21,9 +21,13 @@ class EmbedFieldBuilder implements Builder {
/// Builds object to Map() instance;
Map<String, dynamic> _build() {
if (this.name.toString().length > 256) throw Exception("Field name is too long. (256 characters limit)");
if (this.name.toString().length > 256) {
throw EmbedBuilderArgumentException._new("Field name is too long. (256 characters limit)");
}
if (this.content.toString().length > 1024) throw Exception("Field content is too long. (1024 characters limit)");
if (this.content.toString().length > 1024) {
throw EmbedBuilderArgumentException._new("Field content is too long. (1024 characters limit)");
}
return <String, dynamic>{
"name": name != null ? name.toString() : "\u200B",

View file

@ -15,7 +15,9 @@ class EmbedFooterBuilder implements Builder {
/// Builds object to Map() instance;
Map<String, dynamic> _build() {
if (this.text != null && this.length! > 2048) throw Exception("Footer text is too long. (1024 characters limit)");
if (this.text != null && this.length! > 2048) {
throw EmbedBuilderArgumentException._new("Footer text is too long. (1024 characters limit)");
}
return <String, dynamic>{if (text != null) "text": text, if (iconUrl != null) "icon_url": iconUrl};
}

View file

@ -1,21 +1,18 @@
part of nyxx;
/// Allows to create pre built custom messages which can be passed to classes which inherits from [ISend].
class MessageBuilder {
final _content = StringBuffer();
/// Message builder for editing messages.
class MessageEditBuilder {
/// Clear character which can be used to skip first line in message body or sanitize message content
static const clearCharacter = "";
/// Embed to include in message
EmbedBuilder? embed;
/// Set to true if message should be TTS
bool? tts;
/// List of files to send with message
List<AttachmentBuilder>? files;
/// [AllowedMentions] object to control mentions in message
AllowedMentions? allowedMentions;
final _content = StringBuffer();
/// Clears current content of message and sets new
set content(Object content) {
_content.clear();
@ -31,27 +28,8 @@ class MessageBuilder {
builder(embed!);
}
/// Add attachment
void addAttachment(AttachmentBuilder attachment) {
if (this.files == null) this.files = [];
this.files!.add(attachment);
}
/// Add attachment from specified file
void addFileAttachment(File file, {String? name, bool spoiler = false}) {
addAttachment(AttachmentBuilder.file(file, name: name, spoiler: spoiler));
}
/// Add attachment from specified bytes
void addBytesAttachment(List<int> bytes, String name, {bool spoiler = false}) {
addAttachment(AttachmentBuilder.bytes(bytes, name, spoiler: spoiler));
}
/// Add attachment at specified path
void addPathAttachment(String path, {String? name, bool spoiler = false}) {
addAttachment(AttachmentBuilder.path(path, name: name, spoiler: spoiler));
}
/// Appends clear character. Can be used to skip first line in message body.
void appendClearCharacter() => _content.writeln(clearCharacter);
/// Appends empty line to message
void appendNewLine() => _content.writeln();
@ -81,6 +59,37 @@ class MessageBuilder {
void appendWithDecoration(Object text, MessageDecoration decoration) {
_content.write("$decoration$text$decoration");
}
}
/// Allows to create pre built custom messages which can be passed to classes which inherits from [ISend].
class MessageBuilder extends MessageEditBuilder {
/// Set to true if message should be TTS
bool? tts;
/// List of files to send with message
List<AttachmentBuilder>? files;
/// Add attachment
void addAttachment(AttachmentBuilder attachment) {
if (this.files == null) this.files = [];
this.files!.add(attachment);
}
/// Add attachment from specified file
void addFileAttachment(File file, {String? name, bool spoiler = false}) {
addAttachment(AttachmentBuilder.file(file, name: name, spoiler: spoiler));
}
/// Add attachment from specified bytes
void addBytesAttachment(List<int> bytes, String name, {bool spoiler = false}) {
addAttachment(AttachmentBuilder.bytes(bytes, name, spoiler: spoiler));
}
/// Add attachment at specified path
void addPathAttachment(String path, {String? name, bool spoiler = false}) {
addAttachment(AttachmentBuilder.path(path, name: name, spoiler: spoiler));
}
/// Sends message
Future<Message?> send(ISend entity) => entity.send(builder: this);

Some files were not shown because too many files have changed in this diff Show more