Merge pull request #80 from l7ssha/rewrite_modular_sharding
Sharding support
This commit is contained in:
commit
41692ef2b7
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -30,4 +30,5 @@ pubspec.lock
|
|||
/test/mirrors.dart
|
||||
/private
|
||||
private-*.dart
|
||||
[Rr]pc*
|
||||
[Rr]pc*
|
||||
**/doc/api/**
|
|
@ -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;
|
||||
}
|
|
@ -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.**
|
|
@ -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)?
|
||||
|
|
|
@ -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});
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
38
nyxx/example/channel.dart
Normal 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}");
|
||||
}
|
||||
});
|
||||
}
|
34
nyxx/example/create-add-role.dart
Normal file
34
nyxx/example/create-add-role.dart
Normal 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}");
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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
29
nyxx/example/invite.dart
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
61
nyxx/example/kick-ban.dart
Normal file
61
nyxx/example/kick-ban.dart
Normal 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: "👍");
|
||||
}
|
||||
});
|
||||
}
|
42
nyxx/example/permissions.dart
Normal file
42
nyxx/example/permissions.dart
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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!");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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!");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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!");
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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>{
|
||||
|
|
|
@ -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"));
|
||||
|
|
52
nyxx/lib/src/core/guild/GuildFeature.dart
Normal file
52
nyxx/lib/src/core/guild/GuildFeature.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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!";
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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}`");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
12
nyxx/lib/src/internal/exceptions/InvalidShardException.dart
Normal file
12
nyxx/lib/src/internal/exceptions/InvalidShardException.dart
Normal 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";
|
||||
}
|
11
nyxx/lib/src/internal/exceptions/MissingTokenError.dart
Normal file
11
nyxx/lib/src/internal/exceptions/MissingTokenError.dart
Normal 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;
|
||||
}
|
|
@ -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})"
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
422
nyxx/lib/src/internal/shard/Shard.dart
Normal file
422
nyxx/lib/src/internal/shard/Shard.dart
Normal 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.");
|
||||
}
|
||||
}
|
74
nyxx/lib/src/internal/shard/ShardManager.dart
Normal file
74
nyxx/lib/src/internal/shard/ShardManager.dart
Normal 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();
|
||||
}
|
||||
}
|
99
nyxx/lib/src/internal/shard/shardHandler.dart
Normal file
99
nyxx/lib/src/internal/shard/shardHandler.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>{
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
36
nyxx/lib/src/utils/builders/PresenceBuilder.dart
Normal file
36
nyxx/lib/src/utils/builders/PresenceBuilder.dart
Normal 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
|
||||
};
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ environment:
|
|||
dependencies:
|
||||
logging: "^0.11.4"
|
||||
w_transport: "^3.2.8"
|
||||
http: "^0.12.1"
|
||||
|
||||
dev_dependencies:
|
||||
dart_style:
|
||||
|
|
Loading…
Reference in a new issue