Merge pull request #80 from l7ssha/rewrite_modular_sharding

Sharding support
This commit is contained in:
Szymon Uglis 2020-06-30 17:11:01 +02:00 committed by GitHub
commit 41692ef2b7
72 changed files with 1990 additions and 1785 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
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,41 @@ 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
/// 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,70 +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 {
/// TODO: Cache
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

@ -41,8 +41,8 @@ void main() {
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;
}

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);
// 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

@ -24,6 +24,14 @@ 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/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";
@ -43,10 +51,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";
@ -72,6 +76,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";
@ -92,7 +97,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";
@ -125,6 +129,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";
@ -153,7 +158,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

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;
@ -180,9 +180,17 @@ class Nyxx implements Disposable {
transport_vm.configureWTransportForVM();
if (_token.isEmpty) {
throw NoTokenError();
throw MissingTokenError();
}
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 +211,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 +238,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 +258,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()}"));
@ -268,7 +280,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()}"));
@ -290,7 +304,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"));
@ -321,17 +335,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.
///
@ -347,17 +361,22 @@ 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);
}
}
@ -369,7 +388,7 @@ void setupDefaultLogging([Level? loglevel]) {
var color = "";
if (rec.level == Level.WARNING) {
color = "\u001B[33m";
} else if (rec.level == Level.SEVERE) {
} else if (rec.level == Level.SEVERE || rec.level == Level.SHOUT) {
color = "\u001B[31m";
} else if (rec.level == Level.INFO) {
color = "\u001B[32m";

View file

@ -1,381 +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 = this._onConnect.stream;
/// Emitted when the shard encounters an error.
late Stream<Shard> onDisconnect = this._onDisconnect.stream;
/// Emitted when shard receives member chunk.
late Stream<MemberChunkEvent> onMemberChunk = this._onMemberChunk.stream;
/// 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;
final StreamController<Shard> _onConnect = StreamController<Shard>.broadcast();
late final StreamController<Shard> _onDisconnect = StreamController<Shard>.broadcast();
late final StreamController<MemberChunkEvent> _onMemberChunk = StreamController.broadcast();
Shard._new(this._ws, this.id);
/// 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": "zlib-stream"
};
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 {
this._heartbeatTimer.cancel();
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

@ -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;

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

@ -21,7 +21,7 @@ class Channel extends SnowflakeEntity {
if(guild != null) {
channelGuild = guild;
} else {
} else if(raw["guild_id"] != null) {
channelGuild = client.guilds[Snowflake(raw["guild_id"])];
}
@ -33,14 +33,18 @@ class Channel extends SnowflakeEntity {
return GroupDMChannel._new(raw, client);
break;
case 0:
return CachelessTextChannel._new(raw, channelGuild == null ? Snowflake(raw["guild_id"]) : channelGuild.id, client);
if(channelGuild == null) {
return CachelessTextChannel._new(raw, Snowflake(raw["guild_id"]), client);
}
return CacheTextChannel._new(raw, channelGuild, client);
break;
case 2:
if(channelGuild == null) {
return CachelessVoiceChannel._new(raw, Snowflake(raw["guild_id"]), client);
}
return CacheVoiceChannel._new(raw, channelGuild ,client);
return CacheVoiceChannel._new(raw, channelGuild, client);
break;
case 4:
return CategoryChannel._new(raw, channelGuild == null ? Snowflake(raw["guild_id"]) : channelGuild.id, client);

View file

@ -4,42 +4,41 @@ part of nyxx;
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 chan.send(content: "Very nice message!");
/// await channel.send(content: "Very nice message!");
/// ```
///
/// Can be used in combination with [Emoji]. Just run `toString()` on [Emoji] instance:
/// ```
/// var emoji = guild.emojis.values.firstWhere((e) => e.name.startsWith("dart"));
/// await chan.send(content: "Dart is superb! ${emoji.toString()}");
/// 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 = new EmbedBuilder()
/// 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 chan.send(files: [new File("kitten.png"), new File("kitten.jpg")], content: "Kittens ^-^"]);
/// 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")}";
///
/// await e.message.channel
/// .send(files: [new File("kitten.jpg")], embed: embed, content: "HEJKA!");
/// channel.send(files: [new File("kitten.jpg")], embed: embed, content: "HEJKA!");
/// ```
@override
Future<Message> send(
@ -51,12 +50,15 @@ abstract class ITextChannel implements Channel, MessageChannel {
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.
@ -65,6 +67,7 @@ abstract class ITextChannel implements Channel, MessageChannel {
/// 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,
@ -73,5 +76,6 @@ abstract class ITextChannel implements Channel, MessageChannel {
/// ```
/// 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

@ -56,8 +56,6 @@ abstract class MessageChannel implements Channel, ISend, Disposable {
return messages[id];
}
@override
/// Sends message to channel. Performs `toString()` on thing passed to [content]. Allows to send embeds with [embed] field.
///
/// ```
@ -93,6 +91,7 @@ abstract class MessageChannel implements Channel, ISend, Disposable {
/// 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 +120,7 @@ abstract class MessageChannel implements Channel, ISend, Disposable {
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 +133,9 @@ abstract class MessageChannel implements Channel, ISend, Disposable {
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.

View file

@ -6,7 +6,7 @@ abstract class IGuildChannel extends Channel {
/// The channel"s name.
String get name;
/// The channel's position in the channel list.
/// Relative position of channel in context of channel list
int get position;
/// Id of [Guild] that the channel is in.
@ -30,9 +30,12 @@ abstract class IGuildChannel extends Channel {
/// ```
Stream<InviteWithMeta> getChannelInvites();
/// Allows to set permissions for channel. [id] can be either User or Role
/// 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> editChannelPermission(PermissionsBuilder perms, SnowflakeEntity id, {String? auditReason});
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]
@ -41,7 +44,7 @@ abstract class IGuildChannel extends Channel {
/// Creates new [Invite] for [Channel] and returns it"s instance
///
/// ```
/// var inv = await chan.createInvite(maxUses: 2137);
/// var invite = await channel.createInvite(maxUses: 2137);
/// ```
Future<Invite> createInvite({int? maxAge, int? maxUses, bool? temporary, bool? unique, String? auditReason});
}
@ -106,34 +109,53 @@ abstract class CachelessGuildChannel extends IGuildChannel {
}
}
/// Allows to set permissions for channel. [id] can be either User or Role
/// Throws if [id] isn't [User] or [Role]
/// Allows to set permissions for channel. [entity] can be either [User] or [Role]
/// Throws if [entity] isn't [User] or [Role]
@override
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");
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"));
}
return client._http._execute(BasicRequest._new("/channels/${this.id}/permissions/${id.toString()}",
method: "PUT", body: perms._build()._build(), auditLog: auditReason));
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 Exception("`id` property must be either Role or User");
if (id is! Role && id is! User) {
throw ArgumentError("`id` property must be either Role or User");
}
return client._http
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
/// Creates new [Invite] for [Channel] and returns it's instance
///
/// ```
/// var inv = await chan.createInvite(maxUses: 2137);
/// final invite = await channel.createInvite(maxUses: 2137);
/// ```
@override
Future<Invite> createInvite({int? maxAge, int? maxUses, bool? temporary, bool? unique, String? auditReason}) async {
@ -210,7 +232,7 @@ abstract class CacheGuildChannel extends CachelessGuildChannel {
permissions &= ~overEveryone.deny;
permissions |= overEveryone.allow;
// ignore: avoid_catches_without_on_clauses, empty_catches
// ignore: avoid_catches_without_on_clauses, empty_catches
} on Exception {}
try {
@ -218,7 +240,7 @@ abstract class CacheGuildChannel extends CachelessGuildChannel {
permissions &= ~overRole.deny;
permissions |= overRole.allow;
// ignore: avoid_catches_without_on_clauses, empty_catches
// ignore: avoid_catches_without_on_clauses, empty_catches
} on Exception {}
return Permissions.fromInt(permissions);

View file

@ -1,8 +1,7 @@
part of nyxx;
/// [CachelessTextChannel] represents single text channel on [Guild].
/// Inhertits from [MessageChannel] and mixes [CacheGuildChannel].
class CachelessTextChannel extends CachelessGuildChannel with MessageChannel, ISend implements Mentionable, ITextChannel {
/// [ITextChannel] in context of [Guild].
abstract class GuildTextChannel implements Channel, CachelessGuildChannel, ITextChannel {
/// The channel's topic.
late final String? topic;
@ -18,18 +17,18 @@ class CachelessTextChannel extends CachelessGuildChannel with MessageChannel, IS
String get url => "https://discordapp.com/channels/${this.guildId.toString()}"
"/${this.id.toString()}";
CachelessTextChannel._new(Map<String, dynamic> raw, Snowflake guildId, Nyxx client) : super._new(raw, 0, guildId, client) {
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;
}
/// Edits the channel.
Future<CachelessTextChannel> edit({String? name, String? topic, int? position, int? slowModeTreshold}) async {
Future<CachelessTextChannel> 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));
@ -58,11 +57,11 @@ class CachelessTextChannel extends CachelessGuildChannel with MessageChannel, IS
/// Valid file types for [avatarFile] are jpeg, gif and png.
///
/// ```
/// var webhook = await channnel.createWebhook("!a Send nudes kek6407");
/// final webhook = await channnel.createWebhook("!a Send nudes kek6407");
/// ```
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};
@ -102,3 +101,21 @@ class CachelessTextChannel extends CachelessGuildChannel with MessageChannel, IS
/// 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

@ -39,6 +39,28 @@ 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]

View file

@ -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

@ -30,7 +30,7 @@ class Guild extends SnowflakeEntity implements Disposable {
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;
@ -63,7 +63,7 @@ class Guild extends SnowflakeEntity implements Disposable {
late final int systemChannelFlags;
/// Channel where "PUBLIC" guilds display rules and/or guidelines
late final CacheGuildChannel? rulesChannel;
late final IGuildChannel? rulesChannel;
/// The guild owner's ID
late final User? owner;
@ -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 CacheGuildChannel? publicUpdatesChannel;
late final IGuildChannel? publicUpdatesChannel;
/// Permission of current(bot) user in this guild
Permissions? currentUserPermissions;
@ -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;
@ -169,7 +172,7 @@ class Guild extends SnowflakeEntity implements Disposable {
}
}
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);
@ -229,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 CacheGuildChannel?;
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 CacheGuildChannel?;
this.publicUpdatesChannel = this.channels[Snowflake(raw["public_updates_channel_id"])] as IGuildChannel?;
}
}
@ -300,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>{
@ -343,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())
}));
@ -405,7 +408,7 @@ class Guild extends SnowflakeEntity implements Disposable {
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(
@ -532,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
@ -577,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}));
}
@ -589,8 +592,8 @@ class Guild extends SnowflakeEntity implements Disposable {
///
/// await guild.ban(member);
/// ```
Future<void> ban(CacheMember 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
@ -598,8 +601,8 @@ class Guild extends SnowflakeEntity implements Disposable {
/// ```
/// await guild.kick(member);
/// ```
Future<void> kick(CacheMember 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.
@ -690,15 +693,15 @@ class Guild extends SnowflakeEntity implements Disposable {
yield CacheMember._standard(member, this, client);
}
}
/*
/// 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<IMember> searchMembersGateway(String query, {int limit = 0}) async* {
final nonce = "$query${id.toString()}";
this.client.shard.requestMembers(this.id, query: query, limit: limit, nonce: nonce);
this.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;
@ -712,7 +715,7 @@ class Guild extends SnowflakeEntity implements Disposable {
}
}
}
*/
/// Gets all of the webhooks for this channel.
Stream<Webhook> getWebhooks() async* {
final response = await client._http._execute(BasicRequest._new("/channels/$id/webhooks"));

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

@ -28,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 CachelessTextChannel? 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;
@ -40,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();
@ -65,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 CachelessTextChannel?;
this.channel = client.channels[Snowflake(raw["channel_id"] as String)] as GuildTextChannel?;
}
if (raw["guild_id"] != null) {
@ -82,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, ITextChannel? 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()
@ -114,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,7 +3,7 @@ part of nyxx;
/// Represents emoji. Subclasses provides abstraction to custom emojis(like [GuildEmoji]).
abstract class Emoji {
/// Emojis name.
String name;
final String? name;
Emoji._new(this.name);

View file

@ -1,10 +1,38 @@
part of nyxx;
abstract class IGuildEmoji extends Emoji {
abstract class IGuildEmoji extends Emoji implements SnowflakeEntity {
/// True if emoji is partial.
final bool partial;
IGuildEmoji._new(String name, this.partial) : super._new(name);
/// 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.
@ -22,10 +50,6 @@ class GuildEmoji extends IGuildEmoji implements SnowflakeEntity, GuildEntity {
@override
late final Snowflake guildId;
/// Snowflake id of emoji
@override
late final Snowflake id;
/// Roles which can use this emote
late final Iterable<IRole> roles;
@ -39,8 +63,7 @@ class GuildEmoji extends IGuildEmoji implements SnowflakeEntity, GuildEntity {
late final bool animated;
/// Creates full emoji object
GuildEmoji._new(Map<String, dynamic> raw, this.guildId, this.client) : super._new(raw["name"] as String, false) {
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.requireColons = raw["require_colons"] as bool? ?? false;
@ -57,7 +80,7 @@ class GuildEmoji extends IGuildEmoji implements SnowflakeEntity, GuildEntity {
/// 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>{

View file

@ -50,6 +50,7 @@ class GuildMessage extends Message implements GuildEntity {
/// True if message is sent by a webhook
bool get isByWebhook => author is Webhook;
/// Role mentions in this message
late final List<IRole> roleMentions;
GuildMessage._new(Map<String, dynamic> raw, Nyxx client) : super._new(raw, client) {
@ -68,6 +69,7 @@ class GuildMessage extends Message implements GuildEntity {
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>;
@ -208,19 +210,34 @@ 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 {
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
@ -275,7 +292,19 @@ abstract class Message extends SnowflakeEntity implements Disposable {
@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

@ -6,14 +6,14 @@ class UnicodeEmoji extends Emoji {
UnicodeEmoji(String code) : super._new(code);
/// 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

@ -110,11 +110,11 @@ class CachelessMember extends IMember {
if (reason != null) "reason": reason
};
return client._http._execute(BasicRequest._new("/guilds/${this.guildId}/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;

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

@ -7,7 +7,7 @@ class ChannelCreateEvent {
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 CacheGuildChannel) {
@ -71,7 +71,7 @@ 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;

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 CacheMember? 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 (this.member!._updateMember(nickname, roles)) {
if ((this.member as CacheMember)._updateMember(nickname, roles)) {
return;
}

View file

@ -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;

View file

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

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 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

@ -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,99 @@
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" });
}
// 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

@ -87,19 +87,19 @@ class EmbedBuilder implements Builder {
/// 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");
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>{

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

@ -2,6 +2,9 @@ part of nyxx;
/// Allows to create pre built custom messages which can be passed to classes which inherits from [ISend].
class MessageBuilder {
/// Clear character which can be used to skip first line in message body or sanitize message content
static const clearCharacter = "";
final _content = StringBuffer();
/// Embed to include in message
@ -53,6 +56,9 @@ class MessageBuilder {
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();

View file

@ -8,44 +8,199 @@ class _PermissionsSet {
Map<String, dynamic> _build() => {"allow": allow, "deny": deny};
}
/// Builder for manipulating [PermissionsOverrides]. Created from existing override or manually by passing [type] and [id] of enttiy.
class PermissionOverrideBuilder extends PermissionsBuilder {
/// Type of permission override either `role` or `member`
final String type;
/// Id of entity of permission override
final Snowflake id;
/// Create builder manually from known data. Id is id of entity. [type] can be either `role` or `member`.
PermissionOverrideBuilder.from(this.type, this.id, Permissions permissions) : super.from(permissions);
}
/// Builder for permissions.
class PermissionsBuilder extends AbstractPermissions {
class PermissionsBuilder {
/// The raw permission code.
int? raw;
/// True if user can create InstantInvite
bool? createInstantInvite;
/// True if user can kick members
bool? kickMembers;
/// True if user can ban members
bool? banMembers;
/// True if user is administrator
bool? administrator;
/// True if user can manager channels
bool? manageChannels;
/// True if user can manager guilds
bool? manageGuild;
/// Allows to add reactions
bool? addReactions;
/// Allows for using priority speaker in a voice channel
bool? prioritySpeaker;
/// Allow to view audit logs
bool? viewAuditLog;
/// Allow viewing channels (OLD READ_MESSAGES)
bool? viewChannel;
/// True if user can send messages
bool? sendMessages;
/// True if user can send TTF messages
bool? sendTtsMessages;
/// True if user can manage messages
bool? manageMessages;
/// True if user can send links in messages
bool? embedLinks;
/// True if user can attach files in messages
bool? attachFiles;
/// True if user can read messages history
bool? readMessageHistory;
/// True if user can mention everyone
bool? mentionEveryone;
/// True if user can use external emojis
bool? useExternalEmojis;
/// True if user can connect to voice channel
bool? connect;
/// True if user can speak
bool? speak;
/// True if user can mute members
bool? muteMembers;
/// True if user can deafen members
bool? deafenMembers;
/// True if user can move members
bool? moveMembers;
/// Allows for using voice-activity-detection in a voice channel
bool? useVad;
/// True if user can change nick
bool? changeNickname;
/// True if user can manager others nicknames
bool? manageNicknames;
/// True if user can manage server's roles
bool? manageRoles;
/// True if user can manage webhooks
bool? manageWebhooks;
/// Allows management and editing of emojis
bool? manageEmojis;
/// Allows the user to go live
bool? stream;
/// Allows for viewing guild insights
bool? viewGuildInsights;
/// Empty permission builder
PermissionsBuilder();
/// Permission builder from existing [Permissions] object.
PermissionsBuilder.from(Permissions permissions) {
this
..createInstantInvite = permissions.createInstantInvite
..kickMembers = permissions.kickMembers
..banMembers = permissions.banMembers
..administrator = permissions.administrator
..manageChannels = permissions.manageChannels
..manageGuild = permissions.manageGuild
..addReactions = permissions.addReactions
..viewAuditLog = permissions.viewAuditLog
..viewChannel = permissions.viewChannel
..sendMessages = permissions.sendMessages
..prioritySpeaker = permissions.prioritySpeaker
..sendTtsMessages = permissions.sendTtsMessages
..manageMessages = permissions.manageMessages
..embedLinks = permissions.embedLinks
..attachFiles = permissions.attachFiles
..readMessageHistory = permissions.readMessageHistory
..mentionEveryone = permissions.mentionEveryone
..useExternalEmojis = permissions.useExternalEmojis
..connect = permissions.connect
..speak = permissions.speak
..muteMembers = permissions.muteMembers
..deafenMembers = permissions.deafenMembers
..moveMembers = permissions.moveMembers
..useVad = permissions.useVad
..changeNickname = permissions.changeNickname
..manageNicknames = permissions.manageNicknames
..manageRoles = permissions.manageRoles
..manageWebhooks = permissions.manageWebhooks
..manageEmojis = permissions.manageEmojis
..stream = permissions.stream
..viewGuildInsights = permissions.viewGuildInsights;
}
_PermissionsSet _build() {
final tmp = _PermissionsSet();
final permissionSet = _PermissionsSet();
_apply(tmp, this.createInstantInvite, PermissionsConstants.createInstantInvite);
_apply(tmp, this.kickMembers, PermissionsConstants.kickMembers);
_apply(tmp, this.banMembers, PermissionsConstants.banMembers);
_apply(tmp, this.administrator, PermissionsConstants.administrator);
_apply(tmp, this.manageChannels, PermissionsConstants.manageChannels);
_apply(tmp, this.addReactions, PermissionsConstants.addReactions);
_apply(tmp, this.viewAuditLog, PermissionsConstants.viewAuditLog);
_apply(tmp, this.viewChannel, PermissionsConstants.viewChannel);
_apply(tmp, this.manageGuild, PermissionsConstants.manageGuild);
_apply(tmp, this.sendMessages, PermissionsConstants.sendMessages);
_apply(tmp, this.sendTtsMessages, PermissionsConstants.sendTtsMessages);
_apply(tmp, this.manageMessages, PermissionsConstants.manageMessages);
_apply(tmp, this.embedLinks, PermissionsConstants.embedLinks);
_apply(tmp, this.attachFiles, PermissionsConstants.attachFiles);
_apply(tmp, this.readMessageHistory, PermissionsConstants.readMessageHistory);
_apply(tmp, this.mentionEveryone, PermissionsConstants.mentionEveryone);
_apply(tmp, this.useExternalEmojis, PermissionsConstants.externalEmojis);
_apply(tmp, this.connect, PermissionsConstants.connect);
_apply(tmp, this.speak, PermissionsConstants.speak);
_apply(tmp, this.muteMembers, PermissionsConstants.muteMembers);
_apply(tmp, this.deafenMembers, PermissionsConstants.deafenMembers);
_apply(tmp, this.moveMembers, PermissionsConstants.moveMembers);
_apply(tmp, this.useVad, PermissionsConstants.useVad);
_apply(tmp, this.changeNickname, PermissionsConstants.changeNickname);
_apply(tmp, this.manageNicknames, PermissionsConstants.manageNicknames);
_apply(tmp, this.manageRoles, PermissionsConstants.manageRolesOrPermissions);
_apply(tmp, this.manageWebhooks, PermissionsConstants.manageWebhooks);
_apply(permissionSet, this.createInstantInvite, PermissionsConstants.createInstantInvite);
_apply(permissionSet, this.kickMembers, PermissionsConstants.kickMembers);
_apply(permissionSet, this.banMembers, PermissionsConstants.banMembers);
_apply(permissionSet, this.administrator, PermissionsConstants.administrator);
_apply(permissionSet, this.manageChannels, PermissionsConstants.manageChannels);
_apply(permissionSet, this.addReactions, PermissionsConstants.addReactions);
_apply(permissionSet, this.viewAuditLog, PermissionsConstants.viewAuditLog);
_apply(permissionSet, this.viewChannel, PermissionsConstants.viewChannel);
_apply(permissionSet, this.manageGuild, PermissionsConstants.manageGuild);
_apply(permissionSet, this.sendMessages, PermissionsConstants.sendMessages);
_apply(permissionSet, this.sendTtsMessages, PermissionsConstants.sendTtsMessages);
_apply(permissionSet, this.manageMessages, PermissionsConstants.manageMessages);
_apply(permissionSet, this.embedLinks, PermissionsConstants.embedLinks);
_apply(permissionSet, this.attachFiles, PermissionsConstants.attachFiles);
_apply(permissionSet, this.readMessageHistory, PermissionsConstants.readMessageHistory);
_apply(permissionSet, this.mentionEveryone, PermissionsConstants.mentionEveryone);
_apply(permissionSet, this.useExternalEmojis, PermissionsConstants.externalEmojis);
_apply(permissionSet, this.connect, PermissionsConstants.connect);
_apply(permissionSet, this.speak, PermissionsConstants.speak);
_apply(permissionSet, this.muteMembers, PermissionsConstants.muteMembers);
_apply(permissionSet, this.deafenMembers, PermissionsConstants.deafenMembers);
_apply(permissionSet, this.moveMembers, PermissionsConstants.moveMembers);
_apply(permissionSet, this.useVad, PermissionsConstants.useVad);
_apply(permissionSet, this.changeNickname, PermissionsConstants.changeNickname);
_apply(permissionSet, this.manageNicknames, PermissionsConstants.manageNicknames);
_apply(permissionSet, this.manageRoles, PermissionsConstants.manageRolesOrPermissions);
_apply(permissionSet, this.manageWebhooks, PermissionsConstants.manageWebhooks);
_apply(permissionSet, this.viewGuildInsights, PermissionsConstants.viewGuildInsights);
_apply(permissionSet, this.stream, PermissionsConstants.stream);
_apply(permissionSet, this.manageEmojis, PermissionsConstants.manageEmojis);
return tmp;
return permissionSet;
}
// TODO: NNBD - To consider
void _apply(_PermissionsSet perm, bool applies, int constant) {
void _apply(_PermissionsSet perm, bool? applies, int constant) {
if(applies == null) {
return;
}
if (applies) {
perm.allow |= constant;
} else {

View file

@ -0,0 +1,36 @@
part of nyxx;
/// Allows to build object of user presence used later when setting user presence.
class PresenceBuilder implements Builder {
/// Status of user.
UserStatus? status;
/// If is afk
bool? afk;
/// Type of activity.
Activity? game;
/// WHen activity was started
DateTime? since;
/// Empty constructor to when setting all values manually.
PresenceBuilder();
/// Default builder constructor.
PresenceBuilder.of({this.status, this.afk, this.game, this.since});
@override
Map<String, dynamic> _build() => <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
};
}

View file

@ -4,10 +4,16 @@ part of nyxx;
extension IntExtensions on int {
/// Converts int to [Snowflake]
Snowflake toSnowflake() => Snowflake(this);
/// Converts int to [SnowflakeEntity]
SnowflakeEntity toSnowflakeEntity() => SnowflakeEntity(this.toSnowflake());
}
/// Extension on int
extension StringExtensions on String {
/// Converts String to [Snowflake]
Snowflake toSnowflake() => Snowflake(this);
/// Converts String to [SnowflakeEntity]
SnowflakeEntity toSnowflakeEntity() => SnowflakeEntity(this.toSnowflake());
}

View file

@ -37,19 +37,19 @@ class PermissionsUtils {
allowRaw = publicOverride.allow;
denyRaw = publicOverride.deny;
// ignore: avoid_catches_without_on_clauses, empty_catches
} catch (e) {}
} on Error { }
var allowRole = 0;
var denyRole = 0;
for (final role in member.roles) {
try {
final chanOveride = channel.permissionOverrides.firstWhere((f) => f.id == role.id);
final chanOverride = channel.permissionOverrides.firstWhere((f) => f.id == role.id);
denyRole |= chanOveride.deny;
allowRole |= chanOveride.allow;
denyRole |= chanOverride.deny;
allowRole |= chanOverride.allow;
// ignore: avoid_catches_without_on_clauses, empty_catches
} catch (e) {}
} on Error { }
}
allowRaw = (allowRaw & ~denyRole) | allowRole;
@ -62,7 +62,7 @@ class PermissionsUtils {
allowRaw = (allowRaw & ~memberOverride.deny) | memberOverride.allow;
denyRaw = (denyRaw & ~memberOverride.allow) | memberOverride.deny;
// ignore: avoid_catches_without_on_clauses, empty_catches
} catch (e) {}
} on Error { }
return [allowRaw, denyRaw];
}

View file

@ -15,7 +15,6 @@ environment:
dependencies:
logging: "^0.11.4"
w_transport: "^3.2.8"
http: "^0.12.1"
dev_dependencies:
dart_style: