Merge pull request #76 from l7ssha/rewrite_modular_nocache
Dont require cache when possible
This commit is contained in:
commit
3efad683c8
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
|
||||
Member? get member => guild?.members[author!.id];
|
||||
IMember? get member => guild?.members[author!.id];
|
||||
|
||||
CommandContext._new(this.channel, this.author, this.guild, this.message);
|
||||
/// Reference to client
|
||||
Nyxx get client => channel.client;
|
||||
|
||||
/// Shard on which message was sent
|
||||
int get shardId => this.guild != null ? this.guild!.shard.id : 0;
|
||||
|
||||
/// Substring by which command was matched
|
||||
final String commandMatcher;
|
||||
|
||||
CommandContext._new(this.channel, this.author, this.guild, this.message, this.commandMatcher);
|
||||
|
||||
/// Reply to message. It allows to send regular message, Embed or both.
|
||||
///
|
||||
/// ```
|
||||
/// /// Class body
|
||||
/// @Command()
|
||||
/// Future<void> getAv(CommandContext context, String message) async {
|
||||
/// await context.reply(content: uset.avatarURL());
|
||||
/// Future<void> getAv(CommandContext context) async {
|
||||
/// await context.reply(content: context.user.avatarURL());
|
||||
/// }
|
||||
/// ```
|
||||
Future<Message> reply(
|
||||
|
@ -40,8 +47,7 @@ class CommandContext {
|
|||
/// Reply to messages, then delete it when [duration] expires.
|
||||
///
|
||||
/// ```
|
||||
/// @Command()
|
||||
/// Future<void> getAv(CommandContext context, String message) async {
|
||||
/// Future<void> getAv(CommandContext context) async {
|
||||
/// await context.replyTemp(content: user.avatarURL());
|
||||
/// }
|
||||
/// ```
|
||||
|
@ -56,8 +62,7 @@ class CommandContext {
|
|||
|
||||
/// Replies to message after delay specified with [duration]
|
||||
/// ```
|
||||
/// @Command()
|
||||
/// Future<void> getAv(CommandContext context, String message) async {
|
||||
/// Future<void> getAv(CommandContext context async {
|
||||
/// await context.replyDelayed(Duration(seconds: 2), content: user.avatarURL());
|
||||
/// }
|
||||
/// ```
|
||||
|
@ -77,18 +82,22 @@ class CommandContext {
|
|||
builder: builder,
|
||||
allowedMentions: allowedMentions));
|
||||
|
||||
/// Gather emojis of message in given time
|
||||
/// Awaits for emoji under given [msg]
|
||||
Future<Emoji> awaitEmoji(Message msg) async =>
|
||||
(await this.client.onMessageReactionAdded.where((event) => event.message == msg).first).emoji;
|
||||
|
||||
/// Collects emojis within given [duration]. Returns empty map if no reaction received
|
||||
///
|
||||
/// ```
|
||||
/// Future<void> getAv(CommandContext context, String message) async {
|
||||
/// var msg = await context.replyDelayed(content: context.user.avatarURL());
|
||||
/// var emojis = await context.collectEmojis(msg, Duration(seconds: 15));
|
||||
/// Future<void> getAv(CommandContext context) async {
|
||||
/// final msg = await context.replyDelayed(content: context.user.avatarURL());
|
||||
/// final emojis = await context.awaitEmojis(msg, Duration(seconds: 15));
|
||||
///
|
||||
/// ...
|
||||
/// }
|
||||
/// ```
|
||||
Future<Map<Emoji, int>?> collectEmojis(Message msg, Duration duration) => Future<Map<Emoji, int>?>(() async {
|
||||
final collectedEmoji = <Emoji, int>{};
|
||||
Future<Map<Emoji, int>> awaitEmojis(Message msg, Duration duration){
|
||||
final collectedEmoji = <Emoji, int>{};
|
||||
return Future<Map<Emoji, int>>(() async {
|
||||
await for (final event in msg.client.onMessageReactionAdded.where((evnt) => evnt.message != null && evnt.message!.id == msg.id)) {
|
||||
if (collectedEmoji.containsKey(event.emoji)) {
|
||||
// TODO: NNBD: weird stuff
|
||||
|
@ -104,7 +113,9 @@ class CommandContext {
|
|||
}
|
||||
|
||||
return collectedEmoji;
|
||||
}).timeout(duration, onTimeout: () => null);
|
||||
}).timeout(duration, onTimeout: () => collectedEmoji);
|
||||
}
|
||||
|
||||
|
||||
/// Waits for first [TypingEvent] and returns it. If timed out returns null.
|
||||
/// Can listen to specific user by specifying [user]
|
||||
|
@ -113,9 +124,8 @@ class CommandContext {
|
|||
/// Gets all context channel messages that satisfies [predicate].
|
||||
///
|
||||
/// ```
|
||||
/// @Command()
|
||||
/// Future<void> getAv(CommandContext context, String message) async {
|
||||
/// var messages = await context.nextMessagesWhere((msg) => msg.content.startsWith("fuck"));
|
||||
/// Future<void> getAv(CommandContext context) async {
|
||||
/// final messages = await context.nextMessagesWhere((msg) => msg.content.startsWith("fuck"));
|
||||
/// }
|
||||
/// ```
|
||||
Stream<MessageReceivedEvent> nextMessagesWhere(bool Function(MessageReceivedEvent msg) predicate, {int limit = 100}) => channel.onMessage.where(predicate).take(limit);
|
||||
|
@ -123,15 +133,50 @@ class CommandContext {
|
|||
/// Gets next [num] number of any messages sent within one context (same channel).
|
||||
///
|
||||
/// ```
|
||||
/// @Command()
|
||||
/// Future<void> getAv(CommandContext context, String message) async {
|
||||
/// Future<void> getAv(CommandContext context) async {
|
||||
/// // gets next 10 messages
|
||||
/// var messages = await context.nextMessages(10);
|
||||
/// final messages = await context.nextMessages(10);
|
||||
/// }
|
||||
/// ```
|
||||
Stream<MessageReceivedEvent> nextMessages(int num) => channel.onMessage.take(num);
|
||||
|
||||
/// Returns stream of all code blocks in message
|
||||
/// Starts typing loop and ends when [callback] resolves.
|
||||
Future<T> enterTypingState<T>(Future<T> Function() callback) async {
|
||||
this.channel.startTypingLoop();
|
||||
final result = await callback();
|
||||
this.channel.stopTypingLoop();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Returns list of words separated with space and/or text surrounded by quotes
|
||||
/// Text: `hi this is "example stuff" which 'can be parsed'` will return
|
||||
/// `List<String> [hi, this, is, example stuff, which, can be parsed]`
|
||||
Iterable<String> getArguments() sync* {
|
||||
final regex = RegExp('([A-Z0-9a-z]+)|["\']([^"]*)["\']');
|
||||
|
||||
final matches = regex.allMatches(this.message.content.replaceFirst(commandMatcher, ""));
|
||||
|
||||
for(final match in matches) {
|
||||
final group1 = match.group(1);
|
||||
|
||||
yield group1 ?? match.group(2)!;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns list which content of quotes.
|
||||
/// Text: `hi this is "example stuff" which 'can be parsed'` will return
|
||||
/// `List<String> [example stuff, can be parsed]`
|
||||
Iterable<String> getQuotedText() sync* {
|
||||
final regex = RegExp('["\']([^"]*)["\']');
|
||||
|
||||
final matches = regex.allMatches(this.message.content.replaceFirst(commandMatcher, ""));
|
||||
for(final match in matches) {
|
||||
yield match.group(1)!;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns list of all code blocks in message
|
||||
/// Language string `dart, java` will be ignored and not included
|
||||
/// """
|
||||
/// n> eval ```(dart)?
|
||||
|
|
|
@ -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,69 +56,132 @@ class Commander {
|
|||
if (prefix == null) {
|
||||
_prefixHandler = prefixHandler!;
|
||||
} else {
|
||||
_prefixHandler = (ctx, msg) => prefix;
|
||||
_prefixHandler = (_) => prefix;
|
||||
}
|
||||
|
||||
this._beforeComandHandler = beforeCommandHandler;
|
||||
this._beforeCommandHandler = beforeCommandHandler;
|
||||
this._afterHandlerFunction = afterCommandHandler;
|
||||
|
||||
this._commandExecutionError = commandExecutionError;
|
||||
this._loggerHandlerFunction = loggerHandlerFunction ?? _defaultLogger;
|
||||
|
||||
client.onMessageReceived.listen(_handleMessage);
|
||||
|
||||
this._logger.info("Commander ready!");
|
||||
}
|
||||
|
||||
Future<void> _handleMessage(MessageReceivedEvent event) async {
|
||||
final prefix = await _prefixHandler(event.message);
|
||||
if (prefix == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!event.message.content.startsWith(prefix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find matching command with given message content
|
||||
final matchingCommand = _commandEntities._findMatchingCommand(event.message.content.replaceFirst(prefix, "").trim().split(" ")) as CommandHandler?;
|
||||
|
||||
if(matchingCommand == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fullCommandName = getFullCommandName(matchingCommand);
|
||||
|
||||
// construct commandcontext
|
||||
final context = CommandContext._new(event.message.channel, event.message.author,
|
||||
event.message is GuildMessage ? (event.message as GuildMessage).guild : null, event.message, "$prefix$fullCommandName");
|
||||
|
||||
// Invoke before handler for commands
|
||||
if (!(await _invokeBeforeHandler(matchingCommand, context))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoke before handler for commander
|
||||
if(this._beforeCommandHandler != null && !(await this._beforeCommandHandler!(context))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute command
|
||||
try {
|
||||
await matchingCommand.commandHandler(context, event.message.content);
|
||||
} on Exception catch (e) {
|
||||
if(this._commandExecutionError != null) {
|
||||
await _commandExecutionError!(context, e);
|
||||
}
|
||||
} on Error catch (e) {
|
||||
if(this._commandExecutionError != null) {
|
||||
await _commandExecutionError!(context, e);
|
||||
}
|
||||
}
|
||||
|
||||
// execute logger callback
|
||||
_loggerHandlerFunction(context, fullCommandName, this._logger);
|
||||
|
||||
// invoke after handler of command
|
||||
await _invokeAfterHandler(matchingCommand, context);
|
||||
|
||||
// Invoke after handler for commander
|
||||
if (this._afterHandlerFunction != null) {
|
||||
this._afterHandlerFunction!(context);
|
||||
}
|
||||
}
|
||||
|
||||
// Invokes command after handler and its parents
|
||||
Future<void> _invokeAfterHandler(CommandEntity? commandEntity, CommandContext context) async {
|
||||
if(commandEntity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(commandEntity.afterHandler != null) {
|
||||
await commandEntity.afterHandler!(context);
|
||||
}
|
||||
|
||||
if(commandEntity.parent != null) {
|
||||
await _invokeAfterHandler(commandEntity.parent, context);
|
||||
}
|
||||
}
|
||||
|
||||
// Invokes command before handler and its parents. It will check for next before handlers if top handler returns true.
|
||||
Future<bool> _invokeBeforeHandler(CommandEntity? commandEntity, CommandContext context) async {
|
||||
if(commandEntity == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(commandEntity.beforeHandler == null) {
|
||||
return _invokeBeforeHandler(commandEntity.parent, context);
|
||||
}
|
||||
|
||||
if(await commandEntity.beforeHandler!(context)) {
|
||||
return _invokeBeforeHandler(commandEntity.parent, context);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
FutureOr<void> _defaultLogger(CommandContext ctx, String commandName, Logger logger) {
|
||||
logger.info("Command [$commandName] executed by [${ctx.author!.tag}]");
|
||||
}
|
||||
|
||||
Future<void> _handleMessage(MessageReceivedEvent event) async {
|
||||
final context = CommandContext._new(event.message.channel, event.message.author,
|
||||
event.message is GuildMessage ? (event.message as GuildMessage).guild : null, event.message);
|
||||
|
||||
final prefix = await _prefixHandler(context, event.message.content);
|
||||
if (prefix == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: NNBD: try-catch in where
|
||||
CommandHandler? matchingCommand;
|
||||
try {
|
||||
matchingCommand = _commands.firstWhere(
|
||||
(element) => _isCommandMatching(element.commandName, event.message.content.replaceFirst(prefix, "")));
|
||||
} on Error {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._beforeComandHandler != null && !await this._beforeComandHandler!(context, event.message.content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchingCommand.beforeHandler != null &&
|
||||
!await matchingCommand.beforeHandler!(context, event.message.content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await matchingCommand.commandHandler(context, event.message.content);
|
||||
|
||||
// execute logger callback
|
||||
_loggerHandlerFunction(context, matchingCommand.commandName, this._logger);
|
||||
|
||||
if (matchingCommand.afterHandler != null) {
|
||||
await matchingCommand.afterHandler!(context, event.message.content);
|
||||
}
|
||||
|
||||
if (this._afterHandlerFunction != null) {
|
||||
this._afterHandlerFunction!(context, event.message.content);
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers command with given [commandName]. Allows to specify command specific before and after command execution callbacks
|
||||
void registerCommand(String commandName, CommandHandlerFunction commandHandler,
|
||||
{PassHandlerFunction? beforeHandler, CommandHandlerFunction? afterHandler}) {
|
||||
this._commands.add(
|
||||
_InternalCommandHandler(commandName, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler));
|
||||
void registerCommand(String commandName, CommandHandlerFunction commandHandler, {PassHandlerFunction? beforeHandler, AfterHandlerFunction? afterHandler}) {
|
||||
this._commandEntities.add(BasicCommandHandler(commandName, commandHandler, beforeHandler: beforeHandler, afterHandler: afterHandler));
|
||||
|
||||
// TODO: That is not most efficient way
|
||||
this._commandEntities.sort((a, b) => -a.name.compareTo(b.name));
|
||||
}
|
||||
|
||||
/// Registers command as implemented [CommandHandler] class
|
||||
void registerCommandClass(CommandHandler commandHandler) => this._commands.add(commandHandler);
|
||||
/// Registers command as implemented [CommandEntity] class
|
||||
void registerCommandGroup(CommandGroup commandGroup) => this._commandEntities.add(commandGroup);
|
||||
}
|
||||
|
||||
/// Full qualified command name with its parents names
|
||||
String getFullCommandName(CommandEntity entity) {
|
||||
var commandName = entity.name;
|
||||
|
||||
for(var e = entity.parent; e != null; e = e.parent) {
|
||||
commandName = "${e.name} $commandName";
|
||||
}
|
||||
|
||||
return commandName.trim();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ documentation: https://github.com/l7ssha/nyxx/wiki
|
|||
issue_tracker: https://github.com/l7ssha/nyxx/issue
|
||||
|
||||
environment:
|
||||
sdk: '>=2.9.0-2.0.dev <3.0.0'
|
||||
sdk: '>=2.9.0-10.0.dev <3.0.0'
|
||||
|
||||
dependencies:
|
||||
nyxx: "^1.0.0"
|
||||
|
|
|
@ -5,17 +5,15 @@ import "package:nyxx/nyxx.dart";
|
|||
import "package:nyxx.commander/commander.dart";
|
||||
|
||||
void main() {
|
||||
setupDefaultLogging();
|
||||
|
||||
final bot = Nyxx(Platform.environment["DISCORD_TOKEN"]!, ignoreExceptions: false);
|
||||
|
||||
bot.onMessageReceived.listen((event) async {
|
||||
if (event.message.content == "Test 1") {
|
||||
event.message.delete();
|
||||
event.message.delete(); // ignore: unawaited_futures
|
||||
}
|
||||
|
||||
if (event.message.content == "Test 2") {
|
||||
event.message.delete();
|
||||
event.message.delete(); // ignore: unawaited_futures
|
||||
}
|
||||
|
||||
if (event.message.content == "Test 10") {
|
||||
|
@ -27,22 +25,22 @@ void main() {
|
|||
});
|
||||
|
||||
bot.onReady.listen((e) async {
|
||||
final channel = bot.channels[Snowflake("422285619952222208")] as TextChannel;
|
||||
final channel = bot.channels[Snowflake("422285619952222208")] as CachelessTextChannel;
|
||||
|
||||
await channel.send(content: "Testing Commander");
|
||||
|
||||
final msg1 = await channel.send(content: "test>test1");
|
||||
msg1.delete();
|
||||
msg1.delete(); // ignore: unawaited_futures
|
||||
|
||||
final msg2 = await channel.send(content: "test>test2 arg1");
|
||||
msg2.delete();
|
||||
msg2.delete(); // ignore: unawaited_futures
|
||||
|
||||
final msg3 = await channel.send(content: "test>test3");
|
||||
msg3.delete();
|
||||
msg3.delete(); // ignore: unawaited_futures
|
||||
});
|
||||
|
||||
Commander(bot, prefix: "test>", beforeCommandHandler: (context, message) async {
|
||||
if (message.endsWith("test3")) {
|
||||
Commander(bot, prefix: "test>", beforeCommandHandler: (context) async {
|
||||
if (context.message.content.endsWith("test3")) {
|
||||
await context.channel.send(content: "Test 10");
|
||||
return true;
|
||||
}
|
||||
|
|
10
nyxx.extensions/lib/emoji.dart
Normal file
10
nyxx.extensions/lib/emoji.dart
Normal file
|
@ -0,0 +1,10 @@
|
|||
library nyxx.extensions.emoji;
|
||||
|
||||
import "dart:convert";
|
||||
|
||||
import "package:nyxx/nyxx.dart";
|
||||
import "package:w_transport/w_transport.dart" as w_transport;
|
||||
import "package:w_transport/vm.dart" as w_transport show vmTransportPlatform;
|
||||
|
||||
part "src/emoji/EmojiDefinition.dart";
|
||||
part "src/emoji/EmojiUtils.dart";
|
3
nyxx.extensions/lib/pagination.dart
Normal file
3
nyxx.extensions/lib/pagination.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
library nyxx.extensions.pagination;
|
||||
|
||||
export "src/pagination/pagination.dart";
|
1
nyxx.extensions/lib/scheduled_event.dart
Normal file
1
nyxx.extensions/lib/scheduled_event.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export "src/scheduler/scheduler.dart";
|
34
nyxx.extensions/lib/src/emoji/EmojiDefinition.dart
Normal file
34
nyxx.extensions/lib/src/emoji/EmojiDefinition.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
part of nyxx.extensions.emoji;
|
||||
|
||||
/// Wrapper class around discords emojis data
|
||||
class EmojiDefinition {
|
||||
/// Name of emoji
|
||||
late final String primaryName;
|
||||
|
||||
/// List of alternative names of emoji
|
||||
late final Iterable<String> names;
|
||||
|
||||
/// Literal emoji
|
||||
late final String rawEmoji;
|
||||
|
||||
/// List of utf32 code points
|
||||
late final Iterable<int> codePoints;
|
||||
|
||||
/// Name of asset used in discords frontend for this emoji
|
||||
late final String assetFileName;
|
||||
|
||||
/// Url of emoji picture
|
||||
late final String assetUrl;
|
||||
|
||||
EmojiDefinition._new(Map<String, dynamic> raw) {
|
||||
this.primaryName = raw["primaryName"] as String;
|
||||
this.names = (raw["names"] as Iterable<dynamic>).cast();
|
||||
this.rawEmoji = raw["surrogates"] as String;
|
||||
this.codePoints = (raw["utf32codepoints"] as Iterable<dynamic>).cast();
|
||||
this.assetFileName = raw["assetFileName"] as String;
|
||||
this.assetUrl = raw["assetUrl"] as String;
|
||||
}
|
||||
|
||||
/// Returns [UnicodeEmoji] object of this
|
||||
UnicodeEmoji toEmoji() => UnicodeEmoji(rawEmoji);
|
||||
}
|
40
nyxx.extensions/lib/src/emoji/EmojiUtils.dart
Normal file
40
nyxx.extensions/lib/src/emoji/EmojiUtils.dart
Normal file
|
@ -0,0 +1,40 @@
|
|||
part of nyxx.extensions.emoji;
|
||||
|
||||
List<EmojiDefinition> _emojisCache = [];
|
||||
|
||||
Future<Map<String, dynamic>> _downloadEmojiData() async {
|
||||
final request = w_transport.JsonRequest(transportPlatform: w_transport.vmTransportPlatform)
|
||||
..uri = emojiDataUri;
|
||||
|
||||
// ??? Had to trim 3 characters for some reason
|
||||
final bodyString = (await request.send("GET")).body.asString().substring(3);
|
||||
|
||||
return jsonDecode(bodyString) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Emoji definitions uri
|
||||
final Uri emojiDataUri = Uri.parse("https://static.emzi0767.com/misc/discordEmojiMap.json");
|
||||
|
||||
/// Returns emoji based on given [predicate]. Allows to cache results via [cache] parameter.
|
||||
Future<EmojiDefinition> filterEmojiDefinitions(bool Function(EmojiDefinition) predicate, {bool cache = false}) async =>
|
||||
(await getAllEmojiDefinitions(cache: cache)).firstWhere(predicate);
|
||||
|
||||
/// Returns all possible [EmojiDefinition]s. Allows to cache results via [cache] parameter.
|
||||
Future<Iterable<EmojiDefinition>> getAllEmojiDefinitions({bool cache = false}) async {
|
||||
if(_emojisCache.isNotEmpty) {
|
||||
return Future.value(_emojisCache);
|
||||
}
|
||||
|
||||
final rawData = await _downloadEmojiData();
|
||||
|
||||
final _emojis = [
|
||||
for(final ed in rawData["emojiDefinitions"])
|
||||
EmojiDefinition._new(ed as Map<String, dynamic>)
|
||||
];
|
||||
|
||||
if(cache) {
|
||||
_emojisCache = _emojis;
|
||||
}
|
||||
|
||||
return _emojis;
|
||||
}
|
122
nyxx.extensions/lib/src/pagination/pagination.dart
Normal file
122
nyxx.extensions/lib/src/pagination/pagination.dart
Normal file
|
@ -0,0 +1,122 @@
|
|||
import "dart:async" show Future, FutureOr, Stream;
|
||||
import "package:nyxx/nyxx.dart" show Emoji, ITextChannel, Message, MessageBuilder, MessageChannel, MessageEditBuilder, MessageReactionEvent, Nyxx, UnicodeEmoji;
|
||||
import "../../emoji.dart" show filterEmojiDefinitions;
|
||||
import "../utils.dart" show StreamUtils, StringUtils;
|
||||
|
||||
/// Handles data and constructing data
|
||||
abstract class IPaginationHandler {
|
||||
/// Used to generate message based on given [page] number.
|
||||
FutureOr<MessageEditBuilder> generatePage(int page);
|
||||
|
||||
/// Used to generate fist page of Paginated message.
|
||||
FutureOr<MessageBuilder> generateInitialPage();
|
||||
|
||||
/// Return total number of pages
|
||||
int get dataLength;
|
||||
|
||||
/// Emoji used to navigate to next page. Default: "▶"
|
||||
FutureOr<Emoji> get nextEmoji async => (await filterEmojiDefinitions((emoji) => emoji.primaryName == "arrow_forward", cache: true)).toEmoji();
|
||||
|
||||
/// Emoji used to navigate to previous page. Default: "◀"
|
||||
FutureOr<Emoji> get backEmoji async => (await filterEmojiDefinitions((emoji) => emoji.primaryName == "arrow_backward", cache: true)).toEmoji();
|
||||
|
||||
/// Emoji used to navigate to first page. Default: "⏮"
|
||||
FutureOr<Emoji> get firstEmoji async => (await filterEmojiDefinitions((emoji) => emoji.primaryName == "track_previous", cache: true)).toEmoji();
|
||||
|
||||
/// Emoji used to navigate to last page. Default: "⏭"
|
||||
FutureOr<Emoji> get lastEmoji async => (await filterEmojiDefinitions((emoji) => emoji.primaryName == "track_next", cache: true)).toEmoji();
|
||||
}
|
||||
|
||||
/// Basic pagination handler based on [String]. Each entry in [pages] will be different page.
|
||||
class BasicPaginationHandler extends IPaginationHandler {
|
||||
/// Pages of paginated message
|
||||
List<String> pages;
|
||||
|
||||
/// Generates new pagination from List of Strings. Each list element is single page.
|
||||
BasicPaginationHandler(this.pages);
|
||||
|
||||
/// Generates pagination from String. It divides String into 250 char long pages.
|
||||
factory BasicPaginationHandler.fromString(String str, MessageChannel channel) => BasicPaginationHandler(StringUtils.split(str, 250).toList());
|
||||
|
||||
/// Generates pagination from String but with user specified size of single page.
|
||||
factory BasicPaginationHandler.fromStringLen(String str, int len, MessageChannel channel) => BasicPaginationHandler(StringUtils.split(str, len).toList());
|
||||
|
||||
/// Generates pagination from String but with user specified number of pages.
|
||||
factory BasicPaginationHandler.fromStringEq(String str, int pieces, MessageChannel channel) => BasicPaginationHandler(StringUtils.splitEqually(str, pieces).toList());
|
||||
|
||||
@override
|
||||
FutureOr<MessageEditBuilder> generatePage(int page) =>
|
||||
MessageBuilder()..content = pages[page];
|
||||
|
||||
@override
|
||||
FutureOr<MessageBuilder> generateInitialPage() =>
|
||||
generatePage(0) as MessageBuilder;
|
||||
|
||||
@override
|
||||
int get dataLength => pages.length;
|
||||
}
|
||||
|
||||
/// Handles pagination interactivity. Allows to create paginated messages from List<String>
|
||||
/// Factory constructors allows to create message from String directly.
|
||||
///
|
||||
/// Pagination is sent by [paginate] method. And returns [Message] instance of sent message.
|
||||
///
|
||||
/// ```
|
||||
/// var pagination = new Pagination(["This is simple paginated", "data. Use it if you", "want to partition text by yourself"], ctx,channel);
|
||||
/// // It generated 2 equal (possibly) pages.
|
||||
/// var paginatedMessage = new Pagination.fromStringEq("This is text for pagination", 2);
|
||||
/// ```
|
||||
class Pagination<T extends IPaginationHandler> {
|
||||
/// Channel where message will be sent
|
||||
ITextChannel channel;
|
||||
|
||||
/// [IPaginationHandler] which will handle generating messages.
|
||||
T paginationHandler;
|
||||
|
||||
///
|
||||
Pagination(this.channel, this.paginationHandler);
|
||||
|
||||
/// Paginates a list of Strings - each String is a different page.
|
||||
Future<Message> paginate(Nyxx client, {Duration timeout = const Duration(minutes: 2)}) async {
|
||||
final nextEmoji = await paginationHandler.nextEmoji;
|
||||
final backEmoji = await paginationHandler.backEmoji;
|
||||
final firstEmoji = await paginationHandler.firstEmoji;
|
||||
final lastEmoji = await paginationHandler.lastEmoji;
|
||||
|
||||
final msg = await channel.send(builder: await paginationHandler.generateInitialPage());
|
||||
await msg.createReaction(firstEmoji);
|
||||
await msg.createReaction(backEmoji);
|
||||
await msg.createReaction(nextEmoji);
|
||||
await msg.createReaction(lastEmoji);
|
||||
|
||||
await Future(() async {
|
||||
var currPage = 0;
|
||||
final group = StreamUtils.merge(
|
||||
[client.onMessageReactionAdded, client.onMessageReactionsRemoved as Stream<MessageReactionEvent>]);
|
||||
|
||||
await for (final event in group) {
|
||||
final emoji = (event as dynamic).emoji as UnicodeEmoji;
|
||||
|
||||
if (emoji == nextEmoji) {
|
||||
if (currPage <= paginationHandler.dataLength - 2) {
|
||||
++currPage;
|
||||
await msg.edit(builder: await paginationHandler.generatePage(currPage));
|
||||
}
|
||||
} else if (emoji == backEmoji) {
|
||||
if (currPage >= 1) {
|
||||
--currPage;
|
||||
await msg.edit(builder: await paginationHandler.generatePage(currPage));
|
||||
}
|
||||
} else if (emoji == firstEmoji) {
|
||||
currPage = 0;
|
||||
await msg.edit(builder: await paginationHandler.generatePage(currPage));
|
||||
} else if (emoji == lastEmoji) {
|
||||
currPage = paginationHandler.dataLength;
|
||||
await msg.edit(builder: await paginationHandler.generatePage(currPage));
|
||||
}
|
||||
}
|
||||
}).timeout(timeout);
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
27
nyxx.extensions/lib/src/scheduler/scheduler.dart
Normal file
27
nyxx.extensions/lib/src/scheduler/scheduler.dart
Normal file
|
@ -0,0 +1,27 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:nyxx/nyxx.dart" show Nyxx;
|
||||
|
||||
/// Callback for [ScheduledEvent]. Is executed every period given in [ScheduledEvent]
|
||||
typedef ScheduledEventCallback = Future<void> Function(Nyxx, Timer);
|
||||
|
||||
/// Creates and starts new periodic event. [callback] will be executed every given duration of time.
|
||||
/// Event can be stopped via [stop] function.
|
||||
class ScheduledEvent {
|
||||
/// [Nyxx] instance
|
||||
final Nyxx client;
|
||||
|
||||
/// Callback which will be run every given period of time
|
||||
final ScheduledEventCallback callback;
|
||||
|
||||
late final Timer _timer;
|
||||
|
||||
/// Creates and starts new periodic event. [callback] will be executed every [duration] of time.
|
||||
/// Event can be stopped via [stop] function.
|
||||
ScheduledEvent(this.client, Duration duration, this.callback) {
|
||||
_timer = Timer.periodic(duration, (timer) => callback(client, timer));
|
||||
}
|
||||
|
||||
/// Stops [ScheduledEvent] if running.
|
||||
void stop() => _timer.cancel();
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
part of nyxx.interactivity;
|
||||
import "dart:async";
|
||||
|
||||
class _Utils {
|
||||
class StreamUtils {
|
||||
/// Merges list of stream into one stream
|
||||
static Stream<T> merge<T>(List<Stream<T>> streams) {
|
||||
var _open = streams.length;
|
||||
|
@ -16,7 +16,9 @@ class _Utils {
|
|||
}
|
||||
return streamController.stream;
|
||||
}
|
||||
}
|
||||
|
||||
class StringUtils {
|
||||
/// Splits string based on desired length
|
||||
static Iterable<String> split(String str, int length) sync* {
|
||||
var last = 0;
|
||||
|
@ -33,20 +35,4 @@ class _Utils {
|
|||
|
||||
return split(str, len);
|
||||
}
|
||||
|
||||
/// Partition list into chunks
|
||||
static Iterable<List<T>> partition<T>(List<T> lst, int len) sync* {
|
||||
for (var i = 0; i < lst.length; i += len) {
|
||||
yield lst.sublist(i, i + len);
|
||||
}
|
||||
}
|
||||
|
||||
/// Divides list into equal pieces
|
||||
static Stream<List<T>> chunk<T>(List<T> list, int chunkSize) async* {
|
||||
final len = list.length;
|
||||
for (var i = 0; i < len; i += chunkSize) {
|
||||
final size = i + chunkSize;
|
||||
yield list.sublist(i, size > len ? len : size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
nyxx.extensions/lib/utils.dart
Normal file
1
nyxx.extensions/lib/utils.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export "src/utils.dart";
|
|
@ -1,6 +1,6 @@
|
|||
name: nyxx.interactivity
|
||||
name: nyxx.extensions
|
||||
version: 1.0.0
|
||||
description: A Discord library for Dart.
|
||||
description: Extensions for Nyxx library
|
||||
authors:
|
||||
- Szymon Uglis <szymon.uglis@tuta.io>
|
||||
- Zach Vacura <admin@hackzzila.com>
|
||||
|
@ -10,7 +10,7 @@ documentation: https://github.com/l7ssha/nyxx/wiki
|
|||
issue_tracker: https://github.com/l7ssha/nyxx/issue
|
||||
|
||||
environment:
|
||||
sdk: '>=2.9.0-2.0.dev <3.0.0'
|
||||
sdk: '>=2.9.0-10.0.dev <3.0.0'
|
||||
|
||||
dependencies:
|
||||
nyxx: "^1.0.0"
|
29
nyxx.extensions/test/emoji_test.dart
Normal file
29
nyxx.extensions/test/emoji_test.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
import 'dart:io';
|
||||
|
||||
import "package:nyxx.extensions/emoji.dart";
|
||||
|
||||
main() async {
|
||||
print("Memory: ${(ProcessInfo.currentRss / 1024 / 1024).toStringAsFixed(2)} MB");
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
await filterEmojiDefinitions((emoji) => emoji.primaryName == "joy", cache: true);
|
||||
|
||||
print("TIME: ${stopwatch.elapsedMilliseconds} ms");
|
||||
|
||||
print("Memory: ${(ProcessInfo.currentRss / 1024 / 1024).toStringAsFixed(2)} MB");
|
||||
|
||||
stopwatch.reset();
|
||||
|
||||
await filterEmojiDefinitions((emoji) => emoji.primaryName == "cry", cache: true);
|
||||
|
||||
print("TIME: ${stopwatch.elapsedMilliseconds} ms");
|
||||
print("Memory: ${(ProcessInfo.currentRss / 1024 / 1024).toStringAsFixed(2)} MB");
|
||||
|
||||
stopwatch.reset();
|
||||
|
||||
await filterEmojiDefinitions((emoji) => emoji.primaryName == "tired_face", cache: true);
|
||||
|
||||
print("TIME: ${stopwatch.elapsedMilliseconds} ms");
|
||||
print("Memory: ${(ProcessInfo.currentRss / 1024 / 1024).toStringAsFixed(2)} MB");
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
library nyxx.interactivity;
|
||||
|
||||
import "dart:async";
|
||||
|
||||
import "package:nyxx/nyxx.dart";
|
||||
|
||||
part "src/utils/utils.dart";
|
||||
part "src/utils/emojis.dart";
|
||||
|
||||
part "src/poll.dart";
|
||||
part "src/pagination.dart";
|
|
@ -1,75 +0,0 @@
|
|||
part of nyxx.interactivity;
|
||||
|
||||
/// Handles pagination interactivity. Allows to create paginated messages from List<String>
|
||||
/// Factory constructors allows to create message from String directly.
|
||||
///
|
||||
/// Pagination is sent by [paginate] method. And returns [Message] instance of sent message.
|
||||
///
|
||||
/// ```
|
||||
/// var pagination = new Pagination(["This is simple paginated", "data. Use it if you", "want to partition text by yourself"], ctx,channel);
|
||||
/// // It generated 2 equal (possibly) pages.
|
||||
/// var paginatedMessage = new Pagination.fromStringEq("This is text for pagination", 2);
|
||||
/// ```
|
||||
class Pagination {
|
||||
/// Pages of paginated message
|
||||
List<String> pages;
|
||||
|
||||
/// Channel where message will be sent
|
||||
MessageChannel channel;
|
||||
|
||||
/// Generates new pagination from List of Strings. Each list element is single page.
|
||||
Pagination(this.pages, this.channel);
|
||||
|
||||
/// Generates pagination from String. It divides String into 250 char long pages.
|
||||
factory Pagination.fromString(String str, MessageChannel channel) => Pagination(_Utils.split(str, 250).toList(), channel);
|
||||
|
||||
/// Generates pagination from String but with user specified size of single page.
|
||||
factory Pagination.fromStringLen(String str, int len, MessageChannel channel) => Pagination(_Utils.split(str, len).toList(), channel);
|
||||
|
||||
/// Generates pagination from String but with user specified number of pages.
|
||||
factory Pagination.fromStringEq(String str, int pieces, MessageChannel channel) => Pagination(_Utils.splitEqually(str, pieces).toList(), channel);
|
||||
|
||||
/// Paginates a list of Strings - each String is a different page.
|
||||
Future<Message> paginate(Nyxx client, {Duration timeout = const Duration(minutes: 2)}) async {
|
||||
final nextEmoji = EmojiUtils.getEmoji("arrow_forward")!;
|
||||
final backEmoji = EmojiUtils.getEmoji("arrow_backward")!;
|
||||
final firstEmoji = EmojiUtils.getEmoji("track_previous")!;
|
||||
final lastEmoji = EmojiUtils.getEmoji("track_next")!;
|
||||
|
||||
final msg = await channel.send(content: pages[0]);
|
||||
await msg.createReaction(firstEmoji);
|
||||
await msg.createReaction(backEmoji);
|
||||
await msg.createReaction(nextEmoji);
|
||||
await msg.createReaction(lastEmoji);
|
||||
|
||||
await Future(() async {
|
||||
var currPage = 0;
|
||||
final group = _Utils.merge(
|
||||
[client.onMessageReactionAdded, client.onMessageReactionsRemoved as Stream<MessageReactionEvent>]);
|
||||
|
||||
await for (final event in group) {
|
||||
final emoji = (event as dynamic).emoji as UnicodeEmoji;
|
||||
|
||||
if (emoji == nextEmoji) {
|
||||
if (currPage <= pages.length - 2) {
|
||||
++currPage;
|
||||
await msg.edit(content: pages[currPage]);
|
||||
}
|
||||
} else if (emoji == backEmoji) {
|
||||
if (currPage >= 1) {
|
||||
--currPage;
|
||||
await msg.edit(content: pages[currPage]);
|
||||
}
|
||||
} else if (emoji == firstEmoji) {
|
||||
await msg.edit(content: pages.first);
|
||||
currPage = 0;
|
||||
} else if (emoji == lastEmoji) {
|
||||
await msg.edit(content: pages.last);
|
||||
currPage = pages.length;
|
||||
}
|
||||
}
|
||||
}).timeout(timeout);
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
part of nyxx.interactivity;
|
||||
|
||||
/// Creates new poll, generates options and collects results. Returns `Map<Emoji, int` as result. [timeout] is set by default to 10 minutes
|
||||
///
|
||||
/// ```
|
||||
/// Future<void> examplePoll(CommandContext ctx) async {
|
||||
/// var results = createPoll(ctx.channel, "This is awesome poll", {UnicodeEmoji(""): "One option", UnicodeEmoji(""): "second option"});
|
||||
/// }
|
||||
/// ```
|
||||
Future<Map<Emoji, int>> createPoll(TextChannel channel, String title, Map<Emoji, String> options,
|
||||
{Duration timeout = const Duration(minutes: 10),
|
||||
String? message,
|
||||
bool delete = false,
|
||||
dynamic Function(Map<Emoji, String> options, String message)? messageFactory}) async {
|
||||
var toSend;
|
||||
|
||||
if (messageFactory == null) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln(title);
|
||||
options.forEach((k, v) {
|
||||
buffer.writeln("${k.format()} - $v");
|
||||
});
|
||||
|
||||
if (message != null) buffer.writeln(message);
|
||||
|
||||
toSend = buffer.toString();
|
||||
} else {
|
||||
toSend = messageFactory(options, message!);
|
||||
}
|
||||
|
||||
Message msg;
|
||||
|
||||
if (toSend is String) {
|
||||
msg = await channel.send(content: toSend);
|
||||
} else if (toSend is EmbedBuilder) {
|
||||
msg = await channel.send(embed: toSend);
|
||||
} else {
|
||||
return Future.error("Cannot create poll");
|
||||
}
|
||||
|
||||
for (final emoji in options.keys) {
|
||||
await msg.createReaction(emoji);
|
||||
}
|
||||
|
||||
final emojiCollection = <Emoji, int>{};
|
||||
|
||||
return Future<Map<Emoji, int>>(() async {
|
||||
await for (final event in channel.client.onMessageReactionAdded.where((evnt) => evnt.message?.id == msg.id)) {
|
||||
if (emojiCollection.containsKey(event.emoji)) {
|
||||
// TODO: NNBD: weird stuff
|
||||
var value = emojiCollection[event.emoji];
|
||||
|
||||
if (value != null) {
|
||||
value += 1;
|
||||
emojiCollection[event.emoji] = value;
|
||||
}
|
||||
} else {
|
||||
emojiCollection[event.emoji] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return emojiCollection;
|
||||
}).timeout(timeout, onTimeout: () async {
|
||||
if (delete) await msg.delete();
|
||||
|
||||
return emojiCollection;
|
||||
});
|
||||
}
|
File diff suppressed because it is too large
Load diff
38
nyxx/example/channel.dart
Normal file
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 as CacheMember);
|
||||
|
||||
// Get current member permissions as builder
|
||||
final permissionsAsBuilder = permissions.toBuilder()..sendMessages = true;
|
||||
|
||||
// Get first channel override as builder and edit sendMessages property to allow sending messages for entities included in this override
|
||||
final channelOverridesAsBuilder = messageChannel.permissionOverrides.first.toBuilder()..sendMessages = true;
|
||||
|
||||
// Create new channel permission override
|
||||
await messageChannel.editChannelPermissions(PermissionsBuilder()..sendMessages = true, member);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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!");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@ library nyxx;
|
|||
|
||||
import "dart:async";
|
||||
|
||||
import "dart:collection";
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
|
||||
|
@ -25,6 +24,15 @@ part "src/ClientOptions.dart";
|
|||
|
||||
// INTERNAL
|
||||
|
||||
part "src/internal/exceptions/MissingTokenError.dart";
|
||||
part "src/internal/exceptions/EmbedBuilderArgumentException.dart";
|
||||
part "src/internal/exceptions/InvalidShardException.dart";
|
||||
part "src/internal/exceptions/InvalidSnowflakeException.dart";
|
||||
|
||||
part "src/internal/shard/Shard.dart";
|
||||
part "src/internal/shard/ShardManager.dart";
|
||||
part "src/internal/shard/shardHandler.dart";
|
||||
|
||||
part "src/internal/_Constants.dart";
|
||||
part "src/internal/_EventController.dart";
|
||||
part "src/internal/_WS.dart";
|
||||
|
@ -44,10 +52,6 @@ part "src/internal/interfaces/Convertable.dart";
|
|||
part "src/internal/interfaces/ISend.dart";
|
||||
part "src/internal/interfaces/Mentionable.dart";
|
||||
|
||||
// ERROR
|
||||
|
||||
part "src/errors/SetupErrors.dart";
|
||||
|
||||
// EVENTS
|
||||
|
||||
part "src/events/MemberChunkEvent.dart";
|
||||
|
@ -73,6 +77,7 @@ part "src/events/InviteEvents.dart";
|
|||
|
||||
part "src/utils/builders/Builder.dart";
|
||||
|
||||
part "src/utils/builders/PresenceBuilder.dart";
|
||||
part "src/utils/builders/AttachmentBuilder.dart";
|
||||
part "src/utils/builders/PermissionsBuilder.dart";
|
||||
part "src/utils/builders/EmbedBuilder.dart";
|
||||
|
@ -82,6 +87,8 @@ part "src/utils/builders/EmbedFooterBuilder.dart";
|
|||
part "src/utils/builders/GuildBuilder.dart";
|
||||
part "src/utils/builders/MessageBuilder.dart";
|
||||
|
||||
part "src/utils/extensions.dart";
|
||||
|
||||
// OBJECTS
|
||||
|
||||
part "src/core/AllowedMentions.dart";
|
||||
|
@ -91,7 +98,6 @@ part "src/core/DiscordColor.dart";
|
|||
|
||||
part "src/core/SnowflakeEntity.dart";
|
||||
part "src/core/Snowflake.dart";
|
||||
part "src/Shard.dart";
|
||||
part "src/core/guild/Webhook.dart";
|
||||
|
||||
part "src/core/voice/VoiceState.dart";
|
||||
|
@ -111,6 +117,8 @@ part "src/core/channel/dm/GroupDMChannel.dart";
|
|||
part "src/core/channel/dm/DMChannel.dart";
|
||||
part "src/core/channel/Channel.dart";
|
||||
part "src/core/channel/MessageChannel.dart";
|
||||
part "src/core/channel/ITextChannel.dart";
|
||||
part "src/core/channel/DummyTextChannel.dart";
|
||||
|
||||
part "src/core/embed/EmbedField.dart";
|
||||
part "src/core/embed/EmbedAuthor.dart";
|
||||
|
@ -122,6 +130,7 @@ part "src/core/embed/EmbedThumbnail.dart";
|
|||
|
||||
part "src/core/guild/ClientUser.dart";
|
||||
part "src/core/guild/Guild.dart";
|
||||
part "src/core/guild/GuildFeature.dart";
|
||||
part "src/core/user/Presence.dart";
|
||||
part "src/core/user/Member.dart";
|
||||
part "src/core/guild/Status.dart";
|
||||
|
@ -150,7 +159,6 @@ part "src/core/application/OAuth2Info.dart";
|
|||
part "src/core/application/AppTeam.dart";
|
||||
|
||||
part "src/core/permissions/Permissions.dart";
|
||||
part "src/core/permissions/AbstractPermissions.dart";
|
||||
part "src/core/permissions/PermissionOverrides.dart";
|
||||
part "src/core/permissions/PermissionsConstants.dart";
|
||||
|
||||
|
|
|
@ -7,11 +7,8 @@ class ClientOptions {
|
|||
/// **It means client won't send any of these. It doesn't mean filtering guild messages.**
|
||||
AllowedMentions? allowedMentions;
|
||||
|
||||
/// The index of this shard
|
||||
int shardIndex;
|
||||
|
||||
/// The total number of shards.
|
||||
int shardCount;
|
||||
int? shardCount;
|
||||
|
||||
/// The number of messages to cache for each channel.
|
||||
int messageCacheSize;
|
||||
|
@ -36,17 +33,28 @@ class ClientOptions {
|
|||
/// If you do not specify a certain intent, you will not receive any of the gateway events that are batched into that group.
|
||||
GatewayIntents? gatewayIntents;
|
||||
|
||||
/// Allows to receive compressed payloads from gateway
|
||||
bool compressedGatewayPayloads;
|
||||
|
||||
/// Enables dispatching of guild subscription events (presence and typing events)
|
||||
bool guildSubscriptions;
|
||||
|
||||
/// Initial bot presence
|
||||
PresenceBuilder? initialPresence;
|
||||
|
||||
/// Makes a new `ClientOptions` object.
|
||||
ClientOptions(
|
||||
{this.allowedMentions,
|
||||
this.shardIndex = 0,
|
||||
this.shardCount = 1,
|
||||
this.shardCount,
|
||||
this.messageCacheSize = 400,
|
||||
this.forceFetchMembers = false,
|
||||
this.cacheMembers = true,
|
||||
this.largeThreshold = 50,
|
||||
this.ignoredEvents = const [],
|
||||
this.gatewayIntents});
|
||||
this.gatewayIntents,
|
||||
this.compressedGatewayPayloads = true,
|
||||
this.guildSubscriptions = true,
|
||||
this.initialPresence });
|
||||
}
|
||||
|
||||
/// When identifying to the gateway, you can specify an intents parameter which
|
||||
|
@ -54,24 +62,57 @@ class ClientOptions {
|
|||
/// If you do not specify a certain intent, you will not receive any of the gateway events that are batched into that group.
|
||||
/// [Reference](https://discordapp.com/developers/docs/topics/gateway#gateway-intents)
|
||||
class GatewayIntents {
|
||||
/// Includes events: `GUILD_CREATE, GUILD_UPDATE, GUILD_DELETE, GUILD_ROLE_CREATE, GUILD_ROLE_UPDATE, GUILD_ROLE_DELETE, CHANNEL_DELETE, CHANNEL_CREATE, CHANNEL_UPDATE, CHANNEL_PINS_UPDATE`
|
||||
bool guilds = false;
|
||||
|
||||
/// Includes events: `GUILD_MEMBER_ADD, GUILD_MEMBER_UPDATE, GUILD_MEMBER_REMOVE`
|
||||
bool guildMembers = false;
|
||||
|
||||
/// Includes events: `GUILD_BAN_ADD, GUILD_BAN_REMOVE`
|
||||
bool guildBans = false;
|
||||
|
||||
/// Includes event: `GUILD_EMOJIS_UPDATE`
|
||||
bool guildEmojis = false;
|
||||
|
||||
/// Includes events: `GUILD_INTEGRATIONS_UPDATE`
|
||||
bool guildIntegrations = false;
|
||||
|
||||
/// Includes events: `WEBHOOKS_UPDATE`
|
||||
bool guildWebhooks = false;
|
||||
|
||||
/// Includes events: `INVITE_CREATE, INVITE_DELETE`
|
||||
bool guildInvites = false;
|
||||
|
||||
/// Includes events: `VOICE_STATE_UPDATE`
|
||||
bool guildVoiceState = false;
|
||||
|
||||
/// Includes events: `PRESENCE_UPDATE`
|
||||
bool guildPresences = false;
|
||||
|
||||
/// Include events: `MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, MESSAGE_DELETE_BULK`
|
||||
bool guildMessages = false;
|
||||
|
||||
/// Includes events: `MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI`
|
||||
bool guildMessageReactions = false;
|
||||
|
||||
/// Includes events: `TYPING_START`
|
||||
bool guildMessageTyping = false;
|
||||
|
||||
/// Includes events: `CHANNEL_CREATE, MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, CHANNEL_PINS_UPDATE`
|
||||
bool directMessages = false;
|
||||
|
||||
/// Includes events: `MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI`
|
||||
bool directMessageReactions = false;
|
||||
|
||||
/// Includes events: `TYPING_START`
|
||||
bool directMessageTyping = false;
|
||||
|
||||
bool _all = false;
|
||||
|
||||
/// Constructs intens config object
|
||||
GatewayIntents();
|
||||
|
||||
/// Return config with turned on all intents
|
||||
GatewayIntents.all() : _all = true;
|
||||
|
||||
int _calculate() {
|
||||
|
@ -88,8 +129,9 @@ class GatewayIntents {
|
|||
if (guildIntegrations) value += 1 << 4;
|
||||
if (guildWebhooks) value += 1 << 5;
|
||||
if (guildInvites) value += 1 << 6;
|
||||
if (guildVoiceState) value += 1 << 8;
|
||||
if (guildPresences) value += 1 << 9;
|
||||
if (guildVoiceState) value += 1 << 7;
|
||||
if (guildPresences) value += 1 << 8;
|
||||
if (guildMessages) value += 1 << 9;
|
||||
if (guildMessageReactions) value += 1 << 10;
|
||||
if (guildMessageTyping) value += 1 << 11;
|
||||
if (directMessages) value += 1 << 12;
|
||||
|
|
|
@ -50,7 +50,7 @@ class Nyxx implements Disposable {
|
|||
final String version = Constants.version;
|
||||
|
||||
/// Current client"s shard
|
||||
late Shard shard;
|
||||
late ShardManager shardManager;
|
||||
|
||||
/// Emitted when a shard is disconnected from the websocket.
|
||||
late Stream<DisconnectEvent> onDisconnect;
|
||||
|
@ -176,13 +176,33 @@ class Nyxx implements Disposable {
|
|||
|
||||
/// Creates and logs in a new client. If [ignoreExceptions] is true (by default is)
|
||||
/// isolate will ignore all exceptions and continue to work.
|
||||
Nyxx(this._token, {ClientOptions? options, bool ignoreExceptions = true}) {
|
||||
Nyxx(this._token, {ClientOptions? options, bool ignoreExceptions = true, bool useDefaultLogger = true, Level? defaultLoggerLogLevel}) {
|
||||
transport_vm.configureWTransportForVM();
|
||||
|
||||
if (_token.isEmpty) {
|
||||
throw NoTokenError();
|
||||
if(useDefaultLogger) {
|
||||
Logger.root.level = defaultLoggerLogLevel ?? Level.ALL;
|
||||
|
||||
Logger.root.onRecord.listen((LogRecord rec) {
|
||||
print("[${rec.time}] [${rec.level.name}] [${rec.loggerName}] ${rec.message}");
|
||||
});
|
||||
}
|
||||
|
||||
this._logger.info("Starting bot with pid: $pid");
|
||||
|
||||
if (_token.isEmpty) {
|
||||
throw MissingTokenError();
|
||||
}
|
||||
|
||||
if(!Platform.isWindows) {
|
||||
ProcessSignal.sigterm.watch().forEach((event) async {
|
||||
await this.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
ProcessSignal.sigint.watch().forEach((event) async {
|
||||
await this.dispose();
|
||||
});
|
||||
|
||||
if (ignoreExceptions) {
|
||||
Isolate.current.setErrorsFatal(false);
|
||||
|
||||
|
@ -203,7 +223,7 @@ class Nyxx implements Disposable {
|
|||
this._events = _EventController(this);
|
||||
this.onSelfMention = this
|
||||
.onMessageReceived
|
||||
.where((event) => event.message.mentions.any((mentionedUser) => mentionedUser == this.self));
|
||||
.where((event) => event.message.mentions.contains(this.self));
|
||||
this.onDmReceived = this
|
||||
.onMessageReceived
|
||||
.where((event) => event.message.channel is DMChannel || event.message.channel is GroupDMChannel);
|
||||
|
@ -230,7 +250,9 @@ class Nyxx implements Disposable {
|
|||
|
||||
/// Returns guild with given [guildId]
|
||||
Future<Guild> getGuild(Snowflake guildId, [bool useCache = true]) async {
|
||||
if (this.guilds.hasKey(guildId) && useCache) return this.guilds[guildId]!;
|
||||
if (this.guilds.hasKey(guildId) && useCache) {
|
||||
return this.guilds[guildId]!;
|
||||
}
|
||||
|
||||
final response = await _http._execute(BasicRequest._new("/guilds/$guildId"));
|
||||
|
||||
|
@ -248,7 +270,9 @@ class Nyxx implements Disposable {
|
|||
/// var channel = await client.getChannel<TextChannel>(Snowflake("473853847115137024"));
|
||||
/// ```
|
||||
Future<T> getChannel<T extends Channel>(Snowflake id, [bool useCache = true]) async {
|
||||
if (this.channels.hasKey(id) && useCache) return this.channels[id] as T;
|
||||
if (this.channels.hasKey(id) && useCache) {
|
||||
return this.channels[id] as T;
|
||||
}
|
||||
|
||||
final response = await this._http._execute(BasicRequest._new("/channels/${id.toString()}"));
|
||||
|
||||
|
@ -257,25 +281,7 @@ class Nyxx implements Disposable {
|
|||
}
|
||||
|
||||
final raw = (response as HttpResponseSuccess).jsonBody as Map<String, dynamic>;
|
||||
|
||||
switch (raw["type"] as int) {
|
||||
case 1:
|
||||
return DMChannel._new(raw, this) as T;
|
||||
case 3:
|
||||
return GroupDMChannel._new(raw, this) as T;
|
||||
case 0:
|
||||
case 5:
|
||||
final guild = this.guilds[Snowflake(raw["guild_id"])];
|
||||
return TextChannel._new(raw, guild!, this) as T;
|
||||
case 2:
|
||||
final guild = this.guilds[Snowflake(raw["guild_id"])];
|
||||
return VoiceChannel._new(raw, guild!, this) as T;
|
||||
case 4:
|
||||
final guild = this.guilds[Snowflake(raw["guild_id"])];
|
||||
return CategoryChannel._new(raw, guild!, this) as T;
|
||||
default:
|
||||
return Future.error("Cannot create channel of type [${raw["type"]}");
|
||||
}
|
||||
return Channel._deserialize(raw, this) as T;
|
||||
}
|
||||
|
||||
/// Get user instance with specified id.
|
||||
|
@ -286,7 +292,9 @@ class Nyxx implements Disposable {
|
|||
/// var user = client.getUser(Snowflake("302359032612651009"));
|
||||
/// ``
|
||||
Future<User?> getUser(Snowflake id, [bool useCache = true]) async {
|
||||
if (this.users.hasKey(id) && useCache) return this.users[id];
|
||||
if (this.users.hasKey(id) && useCache) {
|
||||
return this.users[id];
|
||||
}
|
||||
|
||||
final response = await this._http._execute(BasicRequest._new("/users/${id.toString()}"));
|
||||
|
||||
|
@ -308,7 +316,7 @@ class Nyxx implements Disposable {
|
|||
/// ```
|
||||
Future<Guild> createGuild(GuildBuilder builder) async {
|
||||
if (this.guilds.count >= 10) {
|
||||
return Future.error("Guild cannot be created if bot is in 10 or more guilds");
|
||||
return Future.error(ArgumentError("Guild cannot be created if bot is in 10 or more guilds"));
|
||||
}
|
||||
|
||||
final response = await this._http._execute(BasicRequest._new("/guilds", method: "POST"));
|
||||
|
@ -339,17 +347,17 @@ class Nyxx implements Disposable {
|
|||
/// var inv = client.getInvite("YMgffU8");
|
||||
/// ```
|
||||
Future<Invite> getInvite(String code) async {
|
||||
final r = await this._http._execute(BasicRequest._new("/invites/$code"));
|
||||
final response = await this._http._execute(BasicRequest._new("/invites/$code"));
|
||||
|
||||
if (r is HttpResponseSuccess) {
|
||||
return Invite._new(r.jsonBody as Map<String, dynamic>, this);
|
||||
if (response is HttpResponseSuccess) {
|
||||
return Invite._new(response.jsonBody as Map<String, dynamic>, this);
|
||||
}
|
||||
|
||||
return Future.error(r);
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
/// Returns number of shards
|
||||
int get shards => this._options.shardCount;
|
||||
int get shards => this.shardManager._shards.length;
|
||||
|
||||
/// Sets presence for bot.
|
||||
///
|
||||
|
@ -365,38 +373,21 @@ class Nyxx implements Disposable {
|
|||
/// bot.setPresence(game: Activity.of("Super duper game", type: ActivityType.streaming, url: "https://twitch.tv/l7ssha"))
|
||||
/// ```
|
||||
/// `url` property in `Activity` can be only set when type is set to `streaming`
|
||||
void setPresence({UserStatus? status, bool? afk, Activity? game, DateTime? since}) {
|
||||
this.shard.setPresence(status: status, afk: afk, game: game, since: since);
|
||||
void setPresence(PresenceBuilder presenceBuilder) {
|
||||
this.shardManager.setPresence(presenceBuilder);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await shard.dispose();
|
||||
this._logger.info("Disposing and closing bot...");
|
||||
|
||||
await shardManager.dispose();
|
||||
await this._events.dispose();
|
||||
await guilds.dispose();
|
||||
await users.dispose();
|
||||
await guilds.dispose();
|
||||
await this._events.dispose();
|
||||
|
||||
this._logger.info("Exiting...");
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up default logger
|
||||
void setupDefaultLogging([Level? loglevel]) {
|
||||
Logger.root.level = loglevel ?? Level.ALL;
|
||||
|
||||
Logger.root.onRecord.listen((LogRecord rec) {
|
||||
var color = "";
|
||||
if (rec.level == Level.WARNING) {
|
||||
color = "\u001B[33m";
|
||||
} else if (rec.level == Level.SEVERE) {
|
||||
color = "\u001B[31m";
|
||||
} else if (rec.level == Level.INFO) {
|
||||
color = "\u001B[32m";
|
||||
} else {
|
||||
color = "\u001B[0m";
|
||||
}
|
||||
|
||||
print("[${DateTime.now()}] "
|
||||
"$color[${rec.level.name}] [${rec.loggerName}]\u001B[0m: "
|
||||
"${rec.message}");
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,389 +0,0 @@
|
|||
part of nyxx;
|
||||
|
||||
/// Discord gateways implement a method of user-controlled guild sharding which allows for splitting events across a number of gateway connections.
|
||||
/// Guild sharding is entirely user controlled, and requires no state-sharing between separate connections to operate.
|
||||
///
|
||||
/// Shard is basically represents single websocket connection to gateway. Each shard can operate on up to 2500 guilds.
|
||||
class Shard implements Disposable {
|
||||
/// The shard id.
|
||||
late final int id;
|
||||
|
||||
/// Whether or not the shard is ready.
|
||||
bool connected = false;
|
||||
|
||||
/// Emitted when the shard is ready.
|
||||
late Stream<Shard> onConnected;
|
||||
|
||||
/// Emitted when the shard encounters an error.
|
||||
late Stream<Shard> onDisconnect;
|
||||
|
||||
/// Emitted when shard receives member chunk.
|
||||
late Stream<MemberChunkEvent> onMemberChunk;
|
||||
|
||||
/// Number of events seen by shard
|
||||
int get eventsSeen => _sequence;
|
||||
|
||||
final Logger _logger = Logger("Websocket");
|
||||
late final _WS _ws;
|
||||
|
||||
bool _acked = false;
|
||||
late Timer _heartbeatTimer;
|
||||
transport.WebSocket? _socket;
|
||||
StreamSubscription? _socketSubscription; // ignore: cancel_subscriptions
|
||||
|
||||
late int _sequence;
|
||||
String? _sessionId;
|
||||
|
||||
late final StreamController<Shard> _onConnect;
|
||||
late final StreamController<Shard> _onDisconnect;
|
||||
late final StreamController<MemberChunkEvent> _onMemberChunk;
|
||||
|
||||
Shard._new(this._ws, this.id) {
|
||||
this._onConnect = StreamController<Shard>.broadcast();
|
||||
this.onConnected = this._onConnect.stream;
|
||||
|
||||
this._onDisconnect = StreamController<Shard>.broadcast();
|
||||
this.onDisconnect = this._onDisconnect.stream;
|
||||
|
||||
this._onMemberChunk = StreamController.broadcast();
|
||||
this.onMemberChunk = this._onMemberChunk.stream;
|
||||
}
|
||||
|
||||
/// Allows to set presence for current shard.
|
||||
void setPresence({UserStatus? status, bool? afk, Activity? game, DateTime? since}) {
|
||||
final packet = <String, dynamic>{
|
||||
"status": (status != null) ? status.toString() : UserStatus.online.toString(),
|
||||
"afk": (afk != null) ? afk : false,
|
||||
if (game != null)
|
||||
"game": <String, dynamic>{
|
||||
"name": game.name,
|
||||
"type": game.type.value,
|
||||
if (game.type == ActivityType.streaming) "url": game.url
|
||||
},
|
||||
"since": (since != null) ? since.millisecondsSinceEpoch : null
|
||||
};
|
||||
|
||||
this.send(OPCodes.statusUpdate, packet);
|
||||
}
|
||||
|
||||
/// Syncs all guilds
|
||||
void guildSync() => this.send(OPCodes.guildSync, this._ws._client.guilds.keys.toList());
|
||||
|
||||
/// Sends WS data.
|
||||
void send(int opCode, dynamic d) {
|
||||
this._socket?.add(jsonEncode(<String, dynamic>{"op": opCode, "d": d}));
|
||||
}
|
||||
|
||||
/// Allows to request members objects from gateway
|
||||
/// [guild] can be either Snowflake or Iterable<Snowflake>
|
||||
void requestMembers(/* Snowflake|Iterable<Snowflake> */ dynamic guild,
|
||||
{String? query, Iterable<Snowflake>? userIds, int limit = 0, bool presences = false, String? nonce}) {
|
||||
if (query != null && userIds != null) {
|
||||
throw Exception("Both `query` and userIds cannot be specified.");
|
||||
}
|
||||
|
||||
dynamic guildPayload;
|
||||
|
||||
if (guild is Snowflake) {
|
||||
guildPayload = guild.toString();
|
||||
} else if (guild is Iterable<Snowflake>) {
|
||||
guildPayload = guild.map((e) => e.toString()).toList();
|
||||
} else {
|
||||
throw Exception("guild has to be either Snowflake or Iterable<Snowflake>");
|
||||
}
|
||||
|
||||
final payload = <String, dynamic>{
|
||||
"guild_id": guildPayload,
|
||||
if (query != null) "query": query,
|
||||
if (userIds != null) "user_ids": userIds.map((e) => e.toString()).toList(),
|
||||
"limit": limit,
|
||||
"presences": presences,
|
||||
if (nonce != null) "nonce": nonce
|
||||
};
|
||||
|
||||
this.send(OPCodes.requestGuildMember, payload);
|
||||
}
|
||||
|
||||
// Attempts to connect to ws
|
||||
void _connect([bool resume = false, bool init = false]) {
|
||||
this.connected = false;
|
||||
|
||||
if (!init && resume) {
|
||||
Future.delayed(const Duration(seconds: 3), () => _connect(true));
|
||||
return;
|
||||
}
|
||||
|
||||
transport.WebSocket.connect(Uri.parse("${this._ws.gateway}?v=6&encoding=json")).then((ws) {
|
||||
_socket = ws;
|
||||
this._socketSubscription = _socket!.listen((data) => this._handleMsg(_decodeBytes(data), resume),
|
||||
onDone: this._handleErr, onError: (err) => this._handleErr);
|
||||
}, onError: (_, __) => Future.delayed(const Duration(seconds: 6), () => this._connect()));
|
||||
}
|
||||
|
||||
// Decodes zlib compresses string into string json
|
||||
Map<String, dynamic> _decodeBytes(dynamic bytes) {
|
||||
if (bytes is String) return jsonDecode(bytes) as Map<String, dynamic>;
|
||||
|
||||
final decoded = zlib.decoder.convert(bytes as List<int>);
|
||||
final rawStr = utf8.decode(decoded);
|
||||
return jsonDecode(rawStr) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
void _heartbeat() {
|
||||
if (this._socket?.closeCode != null) return;
|
||||
if (!this._acked) _logger.warning("No ACK received");
|
||||
this.send(OPCodes.heartbeat, _sequence);
|
||||
this._acked = false;
|
||||
}
|
||||
|
||||
Future<void> _handleMsg(Map<String, dynamic> msg, bool resume) async {
|
||||
if (msg["op"] == OPCodes.dispatch && this._ws._client._options.ignoredEvents.contains(msg["t"] as String)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg["s"] != null) this._sequence = msg["s"] as int;
|
||||
|
||||
switch (msg["op"] as int) {
|
||||
case OPCodes.heartbeatAck:
|
||||
this._acked = true;
|
||||
break;
|
||||
case OPCodes.hello:
|
||||
if (this._sessionId == null || !resume) {
|
||||
final identifyMsg = <String, dynamic>{
|
||||
"token": _ws._client._token,
|
||||
"properties": <String, dynamic>{
|
||||
"\$os": Platform.operatingSystem,
|
||||
"\$browser": "nyxx",
|
||||
"\$device": "nyxx",
|
||||
},
|
||||
"large_threshold": this._ws._client._options.largeThreshold,
|
||||
"compress": true
|
||||
};
|
||||
|
||||
if (_ws._client._options.gatewayIntents != null) {
|
||||
identifyMsg["intents"] = _ws._client._options.gatewayIntents!._calculate();
|
||||
}
|
||||
|
||||
identifyMsg["shard"] = <int>[this.id, _ws._client._options.shardCount];
|
||||
|
||||
this.send(OPCodes.identify, identifyMsg);
|
||||
} else if (resume) {
|
||||
this.send(OPCodes.resume,
|
||||
<String, dynamic>{"token": _ws._client._token, "session_id": this._sessionId, "seq": this._sequence});
|
||||
}
|
||||
|
||||
this._heartbeatTimer = Timer.periodic(
|
||||
Duration(milliseconds: msg["d"]["heartbeat_interval"] as int), (Timer t) => this._heartbeat());
|
||||
|
||||
break;
|
||||
|
||||
case OPCodes.invalidSession:
|
||||
_logger.severe("Invalid session. Reconnecting...");
|
||||
_heartbeatTimer.cancel();
|
||||
_ws._client._events.onDisconnect.add(DisconnectEvent._new(this, 9));
|
||||
this._onDisconnect.add(this);
|
||||
|
||||
if (msg["d"] as bool) {
|
||||
Future.delayed(const Duration(seconds: 3), () => _connect(true));
|
||||
} else {
|
||||
Future.delayed(const Duration(seconds: 6), _connect);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case OPCodes.dispatch:
|
||||
final j = msg["t"] as String;
|
||||
|
||||
switch (j) {
|
||||
case "READY":
|
||||
this._sessionId = msg["d"]["session_id"] as String;
|
||||
_ws._client.self = ClientUser._new(msg["d"]["user"] as Map<String, dynamic>, _ws._client);
|
||||
|
||||
this.connected = true;
|
||||
_logger.info("Shard connected");
|
||||
this._onConnect.add(this);
|
||||
|
||||
if (!resume) {
|
||||
await _ws.propagateReady();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "GUILD_MEMBERS_CHUNK":
|
||||
this._onMemberChunk.add(MemberChunkEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "MESSAGE_REACTION_REMOVE_ALL":
|
||||
_ws._client._events.onMessageReactionsRemoved.add(MessageReactionsRemovedEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "MESSAGE_REACTION_ADD":
|
||||
MessageReactionAddedEvent._new(msg, _ws._client);
|
||||
break;
|
||||
|
||||
case "MESSAGE_REACTION_REMOVE":
|
||||
MessageReactionRemovedEvent._new(msg, _ws._client);
|
||||
break;
|
||||
|
||||
case "MESSAGE_DELETE_BULK":
|
||||
_ws._client._events.onMessageDeleteBulk.add(MessageDeleteBulkEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "CHANNEL_PINS_UPDATE":
|
||||
_ws._client._events.onChannelPinsUpdate.add(ChannelPinsUpdateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "VOICE_STATE_UPDATE":
|
||||
_ws._client._events.onVoiceStateUpdate.add(VoiceStateUpdateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "VOICE_SERVER_UPDATE":
|
||||
_ws._client._events.onVoiceServerUpdate.add(VoiceServerUpdateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "GUILD_EMOJIS_UPDATE":
|
||||
_ws._client._events.onGuildEmojisUpdate.add(GuildEmojisUpdateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "MESSAGE_CREATE":
|
||||
_ws._client._events.onMessageReceived.add(MessageReceivedEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "MESSAGE_DELETE":
|
||||
_ws._client._events.onMessageDelete.add(MessageDeleteEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "MESSAGE_UPDATE":
|
||||
_ws._client._events.onMessageUpdate.add(MessageUpdateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "GUILD_CREATE":
|
||||
_ws._client._events.onGuildCreate.add(GuildCreateEvent._new(msg, this, _ws._client));
|
||||
break;
|
||||
|
||||
case "GUILD_UPDATE":
|
||||
_ws._client._events.onGuildUpdate.add(GuildUpdateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "GUILD_DELETE":
|
||||
_ws._client._events.onGuildDelete.add(GuildDeleteEvent._new(msg, this, _ws._client));
|
||||
break;
|
||||
|
||||
case "GUILD_BAN_ADD":
|
||||
_ws._client._events.onGuildBanAdd.add(GuildBanAddEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "GUILD_BAN_REMOVE":
|
||||
_ws._client._events.onGuildBanRemove.add(GuildBanRemoveEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "GUILD_MEMBER_ADD":
|
||||
_ws._client._events.onGuildMemberAdd.add(GuildMemberAddEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "GUILD_MEMBER_REMOVE":
|
||||
_ws._client._events.onGuildMemberRemove.add(GuildMemberRemoveEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "GUILD_MEMBER_UPDATE":
|
||||
_ws._client._events.onGuildMemberUpdate.add(GuildMemberUpdateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "CHANNEL_CREATE":
|
||||
_ws._client._events.onChannelCreate.add(ChannelCreateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "CHANNEL_UPDATE":
|
||||
_ws._client._events.onChannelUpdate.add(ChannelUpdateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "CHANNEL_DELETE":
|
||||
_ws._client._events.onChannelDelete.add(ChannelDeleteEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "TYPING_START":
|
||||
_ws._client._events.onTyping.add(TypingEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "PRESENCE_UPDATE":
|
||||
_ws._client._events.onPresenceUpdate.add(PresenceUpdateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "GUILD_ROLE_CREATE":
|
||||
_ws._client._events.onRoleCreate.add(RoleCreateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "GUILD_ROLE_UPDATE":
|
||||
_ws._client._events.onRoleUpdate.add(RoleUpdateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "GUILD_ROLE_DELETE":
|
||||
_ws._client._events.onRoleDelete.add(RoleDeleteEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "USER_UPDATE":
|
||||
_ws._client._events.onUserUpdate.add(UserUpdateEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "INVITE_CREATE":
|
||||
_ws._client._events.onInviteCreated.add(InviteCreatedEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "INVITE_DELETE":
|
||||
_ws._client._events.onInviteDelete.add(InviteDeletedEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
case "MESSAGE_REACTION_REMOVE_EMOJI":
|
||||
_ws._client._events.onMessageReactionRemoveEmoji
|
||||
.add(MessageReactionRemoveEmojiEvent._new(msg, _ws._client));
|
||||
break;
|
||||
|
||||
default:
|
||||
print("UNKNOWN OPCODE: ${jsonEncode(msg)}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleErr() {
|
||||
this._heartbeatTimer.cancel();
|
||||
_logger.severe(
|
||||
"Shard disconnected. Error code: [${this._socket?.closeCode}] | Error message: [${this._socket?.closeReason}]");
|
||||
this.dispose();
|
||||
|
||||
switch (this._socket?.closeCode) {
|
||||
case 4004:
|
||||
case 4010:
|
||||
exit(1);
|
||||
break;
|
||||
case 4013:
|
||||
_logger.shout("Cannot connect to gateway due intent value is invalid. "
|
||||
"Check https://discordapp.com/developers/docs/topics/gateway#gateway-intents for more info.");
|
||||
exit(1);
|
||||
break;
|
||||
case 4014:
|
||||
_logger.shout("You sent a disallowed intent for a Gateway Intent. "
|
||||
"You may have tried to specify an intent that you have not enabled or are not whitelisted for. "
|
||||
"Check https://discordapp.com/developers/docs/topics/gateway#gateway-intents for more info.");
|
||||
exit(1);
|
||||
break;
|
||||
case 4007:
|
||||
case 4009:
|
||||
Future.delayed(const Duration(seconds: 3), () => this._connect(true));
|
||||
break;
|
||||
default:
|
||||
Future.delayed(const Duration(seconds: 6), () => _connect(false, true));
|
||||
break;
|
||||
}
|
||||
|
||||
_ws._client._events.onDisconnect.add(DisconnectEvent._new(this, this._socket?.closeCode!));
|
||||
this._onDisconnect.add(this);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await this._socketSubscription?.cancel();
|
||||
await this._socket?.close(1000);
|
||||
this._socket = null;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -4,4 +4,7 @@ part of nyxx;
|
|||
abstract class GuildEntity {
|
||||
/// Reference to [Guild] object
|
||||
Guild? get guild;
|
||||
|
||||
/// Id of [Guild]
|
||||
Snowflake get guildId;
|
||||
}
|
||||
|
|
|
@ -11,9 +11,6 @@ class Invite {
|
|||
/// A mini channel object for the invite's channel.
|
||||
late final Channel? channel;
|
||||
|
||||
/// Returns url invite
|
||||
String get url => "https://discord.gg/$code";
|
||||
|
||||
/// User who created this invite
|
||||
late final User? inviter;
|
||||
|
||||
|
@ -21,7 +18,10 @@ class Invite {
|
|||
late final User? targetUser;
|
||||
|
||||
/// Reference to bot instance
|
||||
Nyxx client;
|
||||
final Nyxx client;
|
||||
|
||||
/// Returns url to invite
|
||||
String get url => "https://discord.gg/$code";
|
||||
|
||||
Invite._new(Map<String, dynamic> raw, this.client) {
|
||||
this.code = raw["code"] as String;
|
||||
|
@ -30,6 +30,7 @@ class Invite {
|
|||
this.guild = client.guilds[Snowflake(raw["guild"]["id"])];
|
||||
}
|
||||
|
||||
// TODO: NNBD
|
||||
if (raw["channel"] != null) {
|
||||
this.channel = client.channels[Snowflake(raw["channel"]["id"])];
|
||||
}
|
||||
|
@ -41,7 +42,7 @@ class Invite {
|
|||
if (raw["inviter"] != null) {
|
||||
this.inviter = client.users[Snowflake(raw["inviter"]["id"])];
|
||||
}
|
||||
|
||||
|
||||
if (raw["target_user"] != null) {
|
||||
this.targetUser = client.users[Snowflake(raw["target_user"]["id"])];
|
||||
}
|
||||
|
|
|
@ -23,7 +23,11 @@ class Snowflake implements Comparable<Snowflake> {
|
|||
if (id is int) {
|
||||
_id = id;
|
||||
} else {
|
||||
_id = int.parse(id.toString());
|
||||
try {
|
||||
_id = int.parse(id.toString());
|
||||
} on FormatException {
|
||||
throw InvalidSnowflakeException._new(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,26 @@
|
|||
part of nyxx;
|
||||
|
||||
class AppTeam extends SnowflakeEntity {
|
||||
/// Hash of team icon
|
||||
late final String? iconHash;
|
||||
|
||||
/// Id of Team owner
|
||||
late final Snowflake ownerId;
|
||||
|
||||
/// List of members of team
|
||||
late final List<AppTeamMember> members;
|
||||
|
||||
/// Returns instance of [AppTeamMember] of team owner
|
||||
AppTeamMember get ownerMember => this.members.firstWhere((element) => element.user.id == this.ownerId);
|
||||
|
||||
AppTeam._new(Map<String, dynamic> raw) : super(Snowflake(raw["id"])) {
|
||||
this.iconHash = raw["icon"] as String?;
|
||||
this.ownerId = Snowflake(raw["owner_user_id"]);
|
||||
|
||||
this.members = [];
|
||||
for (Map<String, dynamic> obj in raw["members"]) {
|
||||
this.members.add(AppTeamMember._new(obj));
|
||||
}
|
||||
this.members = [
|
||||
for (final rawMember in raw["members"])
|
||||
AppTeamMember._new(rawMember as Map<String, dynamic>)
|
||||
];
|
||||
}
|
||||
|
||||
/// Returns url to team icon
|
||||
|
@ -26,9 +33,8 @@ class AppTeam extends SnowflakeEntity {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represent membership of user in [Team]
|
||||
/// Represent membership of user in [AppTeam]
|
||||
class AppTeamMember {
|
||||
|
||||
/// Basic information of user
|
||||
late final AppTeamUser user;
|
||||
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -23,54 +23,53 @@ class AuditLogEntry extends SnowflakeEntity {
|
|||
String? reason;
|
||||
|
||||
AuditLogEntry._new(Map<String, dynamic> raw, Nyxx client) : super(Snowflake(raw["id"] as String)) {
|
||||
targetId = raw["targetId"] as String;
|
||||
this.targetId = raw["targetId"] as String;
|
||||
|
||||
changes = [
|
||||
this.changes = [
|
||||
if (raw["changes"] != null)
|
||||
for (var o in raw["changes"]) AuditLogChange._new(o as Map<String, dynamic>)
|
||||
];
|
||||
|
||||
user = client.users[Snowflake(raw["user_id"])];
|
||||
type = AuditLogEntryType(raw["action_type"] as int);
|
||||
this.user = client.users[Snowflake(raw["user_id"])];
|
||||
this.type = AuditLogEntryType._create(raw["action_type"] as int);
|
||||
|
||||
if (raw["options"] != null) {
|
||||
options = raw["options"] as String;
|
||||
this.options = raw["options"] as String;
|
||||
}
|
||||
|
||||
reason = raw["reason"] as String;
|
||||
this.reason = raw["reason"] as String;
|
||||
}
|
||||
}
|
||||
|
||||
class AuditLogEntryType extends IEnum<int> {
|
||||
static const AuditLogEntryType guildUpdate = AuditLogEntryType._of(1);
|
||||
static const AuditLogEntryType channelCreate = AuditLogEntryType._of(10);
|
||||
static const AuditLogEntryType channelUpdate = AuditLogEntryType._of(11);
|
||||
static const AuditLogEntryType channelDelete = AuditLogEntryType._of(12);
|
||||
static const AuditLogEntryType channelOverwriteCreate = AuditLogEntryType._of(13);
|
||||
static const AuditLogEntryType channelOverwriteUpdate = AuditLogEntryType._of(14);
|
||||
static const AuditLogEntryType channelOverwriteDelete = AuditLogEntryType._of(15);
|
||||
static const AuditLogEntryType memberKick = AuditLogEntryType._of(20);
|
||||
static const AuditLogEntryType memberPrune = AuditLogEntryType._of(21);
|
||||
static const AuditLogEntryType memberBanAdd = AuditLogEntryType._of(22);
|
||||
static const AuditLogEntryType memberBanRemove = AuditLogEntryType._of(23);
|
||||
static const AuditLogEntryType memberUpdate = AuditLogEntryType._of(24);
|
||||
static const AuditLogEntryType memberRoleUpdate = AuditLogEntryType._of(25);
|
||||
static const AuditLogEntryType roleCreate = AuditLogEntryType._of(30);
|
||||
static const AuditLogEntryType roleUpdate = AuditLogEntryType._of(31);
|
||||
static const AuditLogEntryType roleDelete = AuditLogEntryType._of(32);
|
||||
static const AuditLogEntryType inviteCreate = AuditLogEntryType._of(40);
|
||||
static const AuditLogEntryType inviteUpdate = AuditLogEntryType._of(41);
|
||||
static const AuditLogEntryType inviteDelete = AuditLogEntryType._of(42);
|
||||
static const AuditLogEntryType webhookCreate = AuditLogEntryType._of(50);
|
||||
static const AuditLogEntryType webhookUpdate = AuditLogEntryType._of(51);
|
||||
static const AuditLogEntryType webhookDelete = AuditLogEntryType._of(52);
|
||||
static const AuditLogEntryType emojiCreate = AuditLogEntryType._of(60);
|
||||
static const AuditLogEntryType emojiUpdate = AuditLogEntryType._of(61);
|
||||
static const AuditLogEntryType emojiDelete = AuditLogEntryType._of(62);
|
||||
static const AuditLogEntryType messageDelete = AuditLogEntryType._of(72);
|
||||
static const AuditLogEntryType guildUpdate = AuditLogEntryType._create(1);
|
||||
static const AuditLogEntryType channelCreate = AuditLogEntryType._create(10);
|
||||
static const AuditLogEntryType channelUpdate = AuditLogEntryType._create(11);
|
||||
static const AuditLogEntryType channelDelete = AuditLogEntryType._create(12);
|
||||
static const AuditLogEntryType channelOverwriteCreate = AuditLogEntryType._create(13);
|
||||
static const AuditLogEntryType channelOverwriteUpdate = AuditLogEntryType._create(14);
|
||||
static const AuditLogEntryType channelOverwriteDelete = AuditLogEntryType._create(15);
|
||||
static const AuditLogEntryType memberKick = AuditLogEntryType._create(20);
|
||||
static const AuditLogEntryType memberPrune = AuditLogEntryType._create(21);
|
||||
static const AuditLogEntryType memberBanAdd = AuditLogEntryType._create(22);
|
||||
static const AuditLogEntryType memberBanRemove = AuditLogEntryType._create(23);
|
||||
static const AuditLogEntryType memberUpdate = AuditLogEntryType._create(24);
|
||||
static const AuditLogEntryType memberRoleUpdate = AuditLogEntryType._create(25);
|
||||
static const AuditLogEntryType roleCreate = AuditLogEntryType._create(30);
|
||||
static const AuditLogEntryType roleUpdate = AuditLogEntryType._create(31);
|
||||
static const AuditLogEntryType roleDelete = AuditLogEntryType._create(32);
|
||||
static const AuditLogEntryType inviteCreate = AuditLogEntryType._create(40);
|
||||
static const AuditLogEntryType inviteUpdate = AuditLogEntryType._create(41);
|
||||
static const AuditLogEntryType inviteDelete = AuditLogEntryType._create(42);
|
||||
static const AuditLogEntryType webhookCreate = AuditLogEntryType._create(50);
|
||||
static const AuditLogEntryType webhookUpdate = AuditLogEntryType._create(51);
|
||||
static const AuditLogEntryType webhookDelete = AuditLogEntryType._create(52);
|
||||
static const AuditLogEntryType emojiCreate = AuditLogEntryType._create(60);
|
||||
static const AuditLogEntryType emojiUpdate = AuditLogEntryType._create(61);
|
||||
static const AuditLogEntryType emojiDelete = AuditLogEntryType._create(62);
|
||||
static const AuditLogEntryType messageDelete = AuditLogEntryType._create(72);
|
||||
|
||||
const AuditLogEntryType._of(int value) : super(value);
|
||||
AuditLogEntryType(int value) : super(value);
|
||||
const AuditLogEntryType._create(int value) : super(value);
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
|
|
|
@ -2,42 +2,55 @@ part of nyxx;
|
|||
|
||||
/// A channel.
|
||||
/// Abstract base class that defines the base methods and/or properties for all Discord channel types.
|
||||
abstract class Channel extends SnowflakeEntity {
|
||||
class Channel extends SnowflakeEntity {
|
||||
/// The channel's type.
|
||||
/// https://discordapp.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
ChannelType type;
|
||||
final ChannelType type;
|
||||
|
||||
/// Reference to client instance
|
||||
Nyxx client;
|
||||
final Nyxx client;
|
||||
|
||||
Channel._new(Map<String, dynamic> raw, int type, this.client)
|
||||
: this.type = ChannelType._create(type),
|
||||
super(Snowflake(raw["id"] as String));
|
||||
super(Snowflake(raw["id"]));
|
||||
|
||||
factory Channel._deserialize(Map<String, dynamic> raw, Nyxx client) {
|
||||
final type = raw["d"]["type"] as int;
|
||||
factory Channel._deserialize(Map<String, dynamic> raw, Nyxx client, [Guild? guild]) {
|
||||
final type = raw["type"] as int;
|
||||
|
||||
final guild = raw["d"]["guild_id"] != null ? client.guilds[Snowflake(raw["d"]["guild_id"])] : null;
|
||||
Guild? channelGuild;
|
||||
|
||||
if(guild != null) {
|
||||
channelGuild = guild;
|
||||
} else if(raw["guild_id"] != null) {
|
||||
channelGuild = client.guilds[Snowflake(raw["guild_id"])];
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 1:
|
||||
return DMChannel._new(raw["d"] as Map<String, dynamic>, client);
|
||||
return DMChannel._new(raw, client);
|
||||
break;
|
||||
case 3:
|
||||
return GroupDMChannel._new(raw["d"] as Map<String, dynamic>, client);
|
||||
return GroupDMChannel._new(raw, client);
|
||||
break;
|
||||
case 0:
|
||||
case 5:
|
||||
return TextChannel._new(raw["d"] as Map<String, dynamic>, guild!, client);
|
||||
if(channelGuild == null) {
|
||||
return CachelessTextChannel._new(raw, Snowflake(raw["guild_id"]), client);
|
||||
}
|
||||
|
||||
return CacheTextChannel._new(raw, channelGuild, client);
|
||||
break;
|
||||
case 2:
|
||||
return VoiceChannel._new(raw["d"] as Map<String, dynamic>, guild!, client);
|
||||
if(channelGuild == null) {
|
||||
return CachelessVoiceChannel._new(raw, Snowflake(raw["guild_id"]), client);
|
||||
}
|
||||
|
||||
return CacheVoiceChannel._new(raw, channelGuild, client);
|
||||
break;
|
||||
case 4:
|
||||
return CategoryChannel._new(raw["d"] as Map<String, dynamic>, guild!, client);
|
||||
return CategoryChannel._new(raw, channelGuild == null ? Snowflake(raw["guild_id"]) : channelGuild.id, client);
|
||||
break;
|
||||
default:
|
||||
return _InternalChannel._new(raw["d"] as Map<String, dynamic>, type, client);
|
||||
return _InternalChannel._new(raw, type, client);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
5
nyxx/lib/src/core/channel/DummyTextChannel.dart
Normal file
5
nyxx/lib/src/core/channel/DummyTextChannel.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
part of nyxx;
|
||||
|
||||
class DummyTextChannel extends Channel with MessageChannel, ISend implements ITextChannel {
|
||||
DummyTextChannel._new(Map<String, dynamic> raw, int type, Nyxx client) : super._new(raw, type, client);
|
||||
}
|
81
nyxx/lib/src/core/channel/ITextChannel.dart
Normal file
81
nyxx/lib/src/core/channel/ITextChannel.dart
Normal file
|
@ -0,0 +1,81 @@
|
|||
part of nyxx;
|
||||
|
||||
/// Represents text channel. Can be either [CachelessTextChannel] or [DMChannel] or [GroupDMChannel]
|
||||
abstract class ITextChannel implements Channel, MessageChannel {
|
||||
/// Returns message with given [id]. Allows to force fetch message from api
|
||||
/// with [ignoreCache] property. By default it checks if message is in cache and fetches from api if not.
|
||||
@override
|
||||
Future<Message?> getMessage(Snowflake id, {bool ignoreCache = false});
|
||||
|
||||
/// Sends message to channel. Performs `toString()` on thing passed to [content]. Allows to send embeds with [embed] field.
|
||||
///
|
||||
/// ```
|
||||
/// await channel.send(content: "Very nice message!");
|
||||
/// ```
|
||||
///
|
||||
/// Can be used in combination with [Emoji]. Just run `toString()` on [Emoji] instance:
|
||||
/// ```
|
||||
/// final emoji = guild.emojis.findOne((e) => e.name.startsWith("dart"));
|
||||
/// await channel.send(content: "Dart is superb! ${emoji.toString()}");
|
||||
/// ```
|
||||
/// Embeds can be sent very easily:
|
||||
/// ```
|
||||
/// var embed = EmbedBuilder()
|
||||
/// ..title = "Example Title"
|
||||
/// ..addField(name: "Memory usage", value: "${ProcessInfo.currentRss / 1024 / 1024}MB");
|
||||
///
|
||||
/// await chan.send(embed: embed);
|
||||
/// ```
|
||||
///
|
||||
/// Method also allows to send file and optional [content] with [embed].
|
||||
/// Use `expandAttachment(String file)` method to expand file names in embed
|
||||
///
|
||||
/// ```
|
||||
/// await channel.send(files: [new File("kitten.png"), new File("kitten.jpg")], content: "Kittens ^-^"]);
|
||||
/// ```
|
||||
/// ```
|
||||
/// var embed = new nyxx.EmbedBuilder()
|
||||
/// ..title = "Example Title"
|
||||
/// ..thumbnailUrl = "${attach("kitten.jpg")}";
|
||||
///
|
||||
/// channel.send(files: [new File("kitten.jpg")], embed: embed, content: "HEJKA!");
|
||||
/// ```
|
||||
@override
|
||||
Future<Message> send(
|
||||
{dynamic content,
|
||||
List<AttachmentBuilder>? files,
|
||||
EmbedBuilder? embed,
|
||||
bool? tts,
|
||||
AllowedMentions? allowedMentions,
|
||||
MessageBuilder? builder});
|
||||
|
||||
/// Starts typing.
|
||||
@override
|
||||
Future<void> startTyping();
|
||||
|
||||
/// Loops `startTyping` until `stopTypingLoop` is called.
|
||||
@override
|
||||
void startTypingLoop();
|
||||
|
||||
/// Stops a typing loop if one is running.
|
||||
@override
|
||||
void stopTypingLoop();
|
||||
|
||||
/// Bulk removes many messages by its ids. [messagesIds] is list of messages ids to delete.
|
||||
///
|
||||
/// ```
|
||||
/// var toDelete = chan.messages.keys.take(5);
|
||||
/// await chan.bulkRemoveMessages(toDelete);
|
||||
/// ```
|
||||
@override
|
||||
Future<void> bulkRemoveMessages(Iterable<Message> messagesIds);
|
||||
|
||||
/// Gets several [Message] objects from API. Only one of [after], [before], [around] can be specified,
|
||||
/// otherwise, it will throw.
|
||||
///
|
||||
/// ```
|
||||
/// var messages = await chan.getMessages(limit: 100, after: Snowflake("222078108977594368"));
|
||||
/// ```
|
||||
@override
|
||||
Stream<Message> getMessages({int limit = 50, Snowflake? after, Snowflake? before, Snowflake? around});
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
part of nyxx;
|
||||
|
||||
/// Provides abstraction of messages for [TextChannel], [DMChannel] and [GroupDMChannel].
|
||||
/// Provides abstraction of messages for [CachelessTextChannel], [DMChannel] and [GroupDMChannel].
|
||||
/// Implements iterator which allows to use message object in for loops to access
|
||||
/// messages sequentially.
|
||||
///
|
||||
|
@ -11,34 +11,35 @@ part of nyxx;
|
|||
/// print(message.author.id);
|
||||
/// }
|
||||
/// ```
|
||||
class MessageChannel extends Channel with IterableMixin<Message>, ISend, Disposable {
|
||||
Timer? _typing;
|
||||
abstract class MessageChannel implements Channel, ISend, Disposable {
|
||||
/// Reference to client
|
||||
@override
|
||||
Nyxx get client;
|
||||
|
||||
/// Sent when a new message is received.
|
||||
late final Stream<MessageReceivedEvent> onMessage;
|
||||
late final Stream<MessageReceivedEvent> onMessage = client.onMessageReceived.where((event) => event.message.channel == this);
|
||||
|
||||
/// Emitted when user starts typing.
|
||||
late final Stream<TypingEvent> onTyping;
|
||||
late final Stream<TypingEvent> onTyping = client.onTyping.where((event) => event.channel == this);
|
||||
|
||||
/// Emitted when channel pins are updated.
|
||||
late final Stream<ChannelPinsUpdateEvent> pinsUpdated = client.onChannelPinsUpdate.where((event) => event.channel == this);
|
||||
|
||||
/// A collection of messages sent to this channel.
|
||||
late final MessageCache messages;
|
||||
late final MessageCache messages = MessageCache._new(client._options.messageCacheSize);
|
||||
|
||||
/// File upload limit for channel
|
||||
// Used to create infinite typing loop
|
||||
Timer? _typing;
|
||||
|
||||
/// File upload limit for channel in bytes. If channel is [CachelessGuildChannel] returns default value.
|
||||
int get fileUploadLimit {
|
||||
if (this is GuildChannel) {
|
||||
return (this as GuildChannel).guild.fileUploadLimit;
|
||||
if (this is CacheGuildChannel) {
|
||||
return (this as CacheGuildChannel).guild.fileUploadLimit;
|
||||
}
|
||||
|
||||
return 8 * 1024 * 1024;
|
||||
}
|
||||
|
||||
MessageChannel._new(Map<String, dynamic> raw, int type, Nyxx client) : super._new(raw, type, client) {
|
||||
this.messages = MessageCache._new(client._options.messageCacheSize);
|
||||
|
||||
onTyping = client.onTyping.where((event) => event.channel == this);
|
||||
onMessage = client.onMessageReceived.where((event) => event.message.channel == this);
|
||||
}
|
||||
|
||||
/// Returns message with given [id]. Allows to force fetch message from api
|
||||
/// with [ignoreCache] property. By default it checks if message is in cache and fetches from api if not.
|
||||
Future<Message?> getMessage(Snowflake id, {bool ignoreCache = false}) async {
|
||||
|
@ -48,16 +49,14 @@ class MessageChannel extends Channel with IterableMixin<Message>, ISend, Disposa
|
|||
if (response is HttpResponseError) {
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
var msg = Message._deserialize((response as HttpResponseSuccess).jsonBody as Map<String, dynamic>, client);
|
||||
return messages._cacheMessage(msg);
|
||||
|
||||
return messages._cacheMessage(
|
||||
Message._deserialize((response as HttpResponseSuccess).jsonBody as Map<String, dynamic>, client));
|
||||
}
|
||||
|
||||
return messages[id];
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
/// Sends message to channel. Performs `toString()` on thing passed to [content]. Allows to send embeds with [embed] field.
|
||||
///
|
||||
/// ```
|
||||
|
@ -93,6 +92,7 @@ class MessageChannel extends Channel with IterableMixin<Message>, ISend, Disposa
|
|||
/// await e.message.channel
|
||||
/// .send(files: [new File("kitten.jpg")], embed: embed, content: "HEJKA!");
|
||||
/// ```
|
||||
@override
|
||||
Future<Message> send(
|
||||
{dynamic content,
|
||||
List<AttachmentBuilder>? files,
|
||||
|
@ -121,7 +121,7 @@ class MessageChannel extends Channel with IterableMixin<Message>, ISend, Disposa
|
|||
if (files != null && files.isNotEmpty) {
|
||||
for (final file in files) {
|
||||
if (file._bytes.length > fileUploadLimit) {
|
||||
return Future.error("File with name: [${file._name}] is too big!");
|
||||
return Future.error(ArgumentError("File with name: [${file._name}] is too big!"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -134,9 +134,9 @@ class MessageChannel extends Channel with IterableMixin<Message>, ISend, Disposa
|
|||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return Message._deserialize(response.jsonBody as Map<String, dynamic>, client);
|
||||
} else {
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
/// Starts typing.
|
||||
|
@ -191,7 +191,7 @@ class MessageChannel extends Channel with IterableMixin<Message>, ISend, Disposa
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
/// Returns iterator for messages cache
|
||||
Iterator<Message> get iterator => messages.values.iterator;
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
part of nyxx;
|
||||
|
||||
/// Represents channel with another user.
|
||||
class DMChannel extends MessageChannel {
|
||||
class DMChannel extends Channel with MessageChannel, ISend implements ITextChannel {
|
||||
/// The recipient.
|
||||
late User recipient;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
part of nyxx;
|
||||
|
||||
/// Represents group DM channel.
|
||||
class GroupDMChannel extends MessageChannel {
|
||||
class GroupDMChannel extends Channel with MessageChannel, ISend implements ITextChannel {
|
||||
/// The recipients of channel.
|
||||
late final List<User> recipients;
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
part of nyxx;
|
||||
|
||||
/// Represents guild group channel.
|
||||
class CategoryChannel extends Channel with GuildChannel {
|
||||
CategoryChannel._new(Map<String, dynamic> raw, Guild guild, Nyxx client) : super._new(raw, 4, client) {
|
||||
_initialize(raw, guild);
|
||||
}
|
||||
class CategoryChannel extends CachelessGuildChannel {
|
||||
CategoryChannel._new(Map<String, dynamic> raw, Snowflake guildId, Nyxx client) : super._new(raw, 4, guildId, client);
|
||||
}
|
||||
|
|
|
@ -1,57 +1,208 @@
|
|||
part of nyxx;
|
||||
|
||||
/// Represents channel which is part of guild.
|
||||
mixin GuildChannel implements Channel, GuildEntity {
|
||||
/// Can be represented by [CachelessGuildChannel] or [CacheGuildChannel]
|
||||
abstract class IGuildChannel extends Channel {
|
||||
/// The channel"s name.
|
||||
late String name;
|
||||
String get name;
|
||||
|
||||
/// Relative position of channel in context of channel list
|
||||
int get position;
|
||||
|
||||
/// Id of [Guild] that the channel is in.
|
||||
Snowflake get guildId;
|
||||
|
||||
/// Id of parent channel
|
||||
Snowflake? get parentChannelId;
|
||||
|
||||
/// Indicates if channel is nsfw
|
||||
bool get isNsfw;
|
||||
|
||||
/// Returns list of [CacheMember] objects who can see this channel
|
||||
List<PermissionsOverrides> get permissionOverrides;
|
||||
|
||||
IGuildChannel._new(Map<String, dynamic> raw, int type, Nyxx client) : super._new(raw, type, client);
|
||||
|
||||
/// Fetches and returns all channel"s [Invite]s
|
||||
///
|
||||
/// ```
|
||||
/// var invites = await chan.getChannelInvites();
|
||||
/// ```
|
||||
Stream<InviteWithMeta> getChannelInvites();
|
||||
|
||||
/// Allows to set or edit permissions for channel. [id] can be either User or Role
|
||||
/// Throws if [id] isn't [User] or [Role]
|
||||
Future<void> editChannelPermissions(PermissionsBuilder perms, SnowflakeEntity id, {String? auditReason});
|
||||
|
||||
/// Allows to edit or set channel permission overrides.
|
||||
Future<void> editChannelPermissionOverrides(PermissionOverrideBuilder permissionBuilder, {String? auditReason});
|
||||
|
||||
/// Deletes permission overwrite for given User or Role [id]
|
||||
/// Throws if [id] isn't [User] or [Role]
|
||||
Future<void> deleteChannelPermission(SnowflakeEntity id, {String? auditReason});
|
||||
|
||||
/// Creates new [Invite] for [Channel] and returns it"s instance
|
||||
///
|
||||
/// ```
|
||||
/// var invite = await channel.createInvite(maxUses: 2137);
|
||||
/// ```
|
||||
Future<Invite> createInvite({int? maxAge, int? maxUses, bool? temporary, bool? unique, String? auditReason});
|
||||
}
|
||||
|
||||
/// Guild channel which does not have access to cache.
|
||||
abstract class CachelessGuildChannel extends IGuildChannel {
|
||||
/// The channel"s name.
|
||||
@override
|
||||
late final String name;
|
||||
|
||||
/// The channel's position in the channel list.
|
||||
@override
|
||||
late final int position;
|
||||
|
||||
/// Id of [Guild] that the channel is in.
|
||||
@override
|
||||
final Snowflake guildId;
|
||||
|
||||
/// Id of parent channel
|
||||
@override
|
||||
late final Snowflake? parentChannelId;
|
||||
|
||||
/// Indicates if channel is nsfw
|
||||
@override
|
||||
late final bool isNsfw;
|
||||
|
||||
/// Returns list of [CacheMember] objects who can see this channel
|
||||
@override
|
||||
late final List<PermissionsOverrides> permissionOverrides;
|
||||
|
||||
CachelessGuildChannel._new(Map<String, dynamic> raw, int type, this.guildId, Nyxx client) : super._new(raw, type, client) {
|
||||
this.name = raw["name"] as String;
|
||||
this.position = raw["position"] as int;
|
||||
|
||||
this.parentChannelId = raw["parent_id"] != null ? Snowflake(raw["parent_id"]) : null;
|
||||
this.isNsfw = raw["nsfw"] as bool? ?? false;
|
||||
|
||||
this.permissionOverrides = [
|
||||
if (raw["permission_overwrites"] != null)
|
||||
for (var obj in raw["permission_overwrites"])
|
||||
PermissionsOverrides._new(obj as Map<String, dynamic>)
|
||||
];
|
||||
}
|
||||
|
||||
/// Fetches and returns all channel"s [Invite]s
|
||||
///
|
||||
/// ```
|
||||
/// var invites = await chan.getChannelInvites();
|
||||
/// ```
|
||||
@override
|
||||
Stream<InviteWithMeta> getChannelInvites() async* {
|
||||
final response = await client._http._execute(BasicRequest._new("/channels/$id/invites"));
|
||||
|
||||
if (response is HttpResponseError) {
|
||||
yield* Stream.error(response);
|
||||
}
|
||||
|
||||
final bodyValues = (response as HttpResponseSuccess).jsonBody.values.first;
|
||||
|
||||
for (final val in bodyValues as Iterable<Map<String, dynamic>>) {
|
||||
yield InviteWithMeta._new(val, client);
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows to set permissions for channel. [entity] can be either [User] or [Role]
|
||||
/// Throws if [entity] isn't [User] or [Role]
|
||||
@override
|
||||
Future<void> editChannelPermissions(PermissionsBuilder perms, SnowflakeEntity entity, {String? auditReason}) async {
|
||||
if (entity is! IRole && entity is! User) {
|
||||
return Future.error(ArgumentError("The `id` property must be either Role or User"));
|
||||
}
|
||||
|
||||
final permSet = perms._build();
|
||||
|
||||
await client._http._execute(BasicRequest._new("/channels/${this.id}/permissions/${entity.id.toString()}",
|
||||
method: "PUT", body: {
|
||||
"type" : entity is IRole ? "role" : "member",
|
||||
"allow" : permSet.allow,
|
||||
"deny" : permSet.deny
|
||||
}, auditLog: auditReason));
|
||||
}
|
||||
|
||||
@override
|
||||
/// Allows to edit or set channel permission overrides.
|
||||
Future<void> editChannelPermissionOverrides(PermissionOverrideBuilder permissionBuilder, {String? auditReason}) async {
|
||||
final permSet = permissionBuilder._build();
|
||||
|
||||
await client._http._execute(BasicRequest._new("/channels/${this.id}/permissions/${permissionBuilder.id.toString()}",
|
||||
method: "PUT", body: {
|
||||
"type" : permissionBuilder.type,
|
||||
"allow" : permSet.allow,
|
||||
"deny" : permSet.deny
|
||||
}, auditLog: auditReason));
|
||||
}
|
||||
|
||||
/// Deletes permission overwrite for given User or Role [id]
|
||||
/// Throws if [id] isn't [User] or [Role]
|
||||
@override
|
||||
Future<void> deleteChannelPermission(SnowflakeEntity id, {String? auditReason}) async {
|
||||
if (id is! Role && id is! User) {
|
||||
throw ArgumentError("`id` property must be either Role or User");
|
||||
}
|
||||
|
||||
await client._http
|
||||
._execute(BasicRequest._new("/channels/${this.id}/permissions/$id", method: "PUT", auditLog: auditReason));
|
||||
}
|
||||
|
||||
/// Creates new [Invite] for [Channel] and returns it's instance
|
||||
///
|
||||
/// ```
|
||||
/// final invite = await channel.createInvite(maxUses: 2137);
|
||||
/// ```
|
||||
@override
|
||||
Future<Invite> createInvite({int? maxAge, int? maxUses, bool? temporary, bool? unique, String? auditReason}) async {
|
||||
final body = {
|
||||
if (maxAge != null) "max_age": maxAge,
|
||||
if (maxUses != null) "max_uses": maxUses,
|
||||
if (temporary != null) "temporary": temporary,
|
||||
if (unique != null) "unique": unique,
|
||||
};
|
||||
|
||||
final response = await client._http
|
||||
._execute(BasicRequest._new("/channels/$id/invites", method: "POST", body: body, auditLog: auditReason));
|
||||
|
||||
if (response is HttpResponseError) {
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
return InviteWithMeta._new((response as HttpResponseSuccess).jsonBody as Map<String, dynamic>, client);
|
||||
}
|
||||
}
|
||||
|
||||
/// Guild channel which does have access to cache.
|
||||
abstract class CacheGuildChannel extends CachelessGuildChannel {
|
||||
/// The guild that the channel is in.
|
||||
late Guild guild;
|
||||
|
||||
/// The channel"s position in the channel list.
|
||||
late int position;
|
||||
|
||||
/// Parent channel id
|
||||
CategoryChannel? parentChannel;
|
||||
|
||||
/// Indicates if channel is nsfw
|
||||
late bool nsfw;
|
||||
/// Returns list of [CacheMember] objects who can see this channel
|
||||
Iterable<IMember> get users => this.guild.members.values.where((member) => member is CacheMember && this.effectivePermissions(member).hasPermission(PermissionsConstants.viewChannel));
|
||||
|
||||
/// Permissions overwrites for channel.
|
||||
late final List<PermissionsOverrides> permissions;
|
||||
|
||||
/// Returns list of [Member] objects who can see this channel
|
||||
Iterable<Member> get users => this
|
||||
.guild
|
||||
.members
|
||||
.values
|
||||
.where((member) => this.effectivePermissions(member).hasPermission(PermissionsConstants.viewChannel));
|
||||
|
||||
// Initializes Guild channel
|
||||
void _initialize(Map<String, dynamic> raw, Guild guild) {
|
||||
this.name = raw["name"] as String;
|
||||
this.position = raw["position"] as int;
|
||||
this.guild = guild;
|
||||
|
||||
if (raw["parent_id"] != null) {
|
||||
this.parentChannel = client.channels[Snowflake(raw["parent_id"])] as CategoryChannel?;
|
||||
CacheGuildChannel._new(Map<String, dynamic> raw, int type, this.guild, Nyxx client) : super._new(raw, type, guild.id, client) {
|
||||
if(this.parentChannelId != null) {
|
||||
this.parentChannel = guild.channels[this.parentChannelId!] as CategoryChannel?;
|
||||
}
|
||||
|
||||
this.nsfw = raw["nsfw"] as bool? ?? false;
|
||||
|
||||
this.permissions = [
|
||||
if (raw["permission_overwrites"] != null)
|
||||
for (var obj in raw["permission_overwrites"]) PermissionsOverrides._new(obj as Map<String, dynamic>)
|
||||
];
|
||||
}
|
||||
|
||||
/// Returns effective permissions for [member] to this channel including channel overrides.
|
||||
Permissions effectivePermissions(Member member) {
|
||||
if (member.guild != this.guild) return Permissions.empty();
|
||||
Permissions effectivePermissions(CacheMember member) {
|
||||
if (member.guild != this.guild) {
|
||||
return Permissions.empty();
|
||||
}
|
||||
|
||||
if (member.guild.owner == member) return Permissions.fromInt(PermissionsConstants.allPermissions);
|
||||
if (member.guild.owner == member) {
|
||||
return Permissions.fromInt(PermissionsConstants.allPermissions);
|
||||
}
|
||||
|
||||
var rawMemberPerms = member.effectivePermissions.raw;
|
||||
|
||||
|
@ -69,91 +220,29 @@ mixin GuildChannel implements Channel, GuildEntity {
|
|||
|
||||
/// Returns effective permissions for [role] to this channel including channel overrides.
|
||||
Permissions effectivePermissionForRole(Role role) {
|
||||
if (role.guild != this.guild) return Permissions.empty();
|
||||
if (role.guild != this.guild) {
|
||||
return Permissions.empty();
|
||||
}
|
||||
|
||||
var permissions = role.permissions.raw | guild.everyoneRole.permissions.raw;
|
||||
var permissions = role.permissions.raw | (guild.everyoneRole as Role).permissions.raw;
|
||||
|
||||
// TODO: NNBD: try-catch in where
|
||||
try {
|
||||
final overEveryone = this.permissions.firstWhere((f) => f.id == guild.everyoneRole.id);
|
||||
final overEveryone = this.permissionOverrides.firstWhere((f) => f.id == guild.everyoneRole.id);
|
||||
|
||||
permissions &= ~overEveryone.deny;
|
||||
permissions |= overEveryone.allow;
|
||||
// ignore: avoid_catches_without_on_clauses, empty_catches
|
||||
} on Error {}
|
||||
// ignore: avoid_catches_without_on_clauses, empty_catches
|
||||
} on Exception {}
|
||||
|
||||
try {
|
||||
final overRole = this.permissions.firstWhere((f) => f.id == role.id);
|
||||
final overRole = this.permissionOverrides.firstWhere((f) => f.id == role.id);
|
||||
|
||||
permissions &= ~overRole.deny;
|
||||
permissions |= overRole.allow;
|
||||
// ignore: avoid_catches_without_on_clauses, empty_catches
|
||||
} on Error {}
|
||||
// ignore: avoid_catches_without_on_clauses, empty_catches
|
||||
} on Exception {}
|
||||
|
||||
return Permissions.fromInt(permissions);
|
||||
}
|
||||
|
||||
/// Creates new [Invite] for [Channel] and returns it"s instance
|
||||
///
|
||||
/// ```
|
||||
/// var inv = await chan.createInvite(maxUses: 2137);
|
||||
/// ```
|
||||
Future<Invite> createInvite({int? maxAge, int? maxUses, bool? temporary, bool? unique, String? auditReason}) async {
|
||||
final body = {
|
||||
if (maxAge != null) "max_age": maxAge,
|
||||
if (maxAge != null) "max_uses": maxUses,
|
||||
if (maxAge != null) "temporary": temporary,
|
||||
if (maxAge != null) "unique": unique,
|
||||
};
|
||||
|
||||
final response = await client._http
|
||||
._execute(BasicRequest._new("/channels/$id/invites", method: "POST", body: body, auditLog: auditReason));
|
||||
|
||||
if (response is HttpResponseError) {
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
return InviteWithMeta._new((response as HttpResponseSuccess).jsonBody as Map<String, dynamic>, client);
|
||||
}
|
||||
|
||||
/// Fetches and returns all channel"s [Invite]s
|
||||
///
|
||||
/// ```
|
||||
/// var invites = await chan.getChannelInvites();
|
||||
/// ```
|
||||
Stream<InviteWithMeta> getChannelInvites() async* {
|
||||
final response = await client._http._execute(BasicRequest._new("/channels/$id/invites"));
|
||||
|
||||
if (response is HttpResponseError) {
|
||||
yield* Stream.error(response);
|
||||
}
|
||||
|
||||
final bodyValues = (response as HttpResponseSuccess).jsonBody.values.first;
|
||||
|
||||
for (final val in bodyValues as Iterable<Map<String, dynamic>>) {
|
||||
yield InviteWithMeta._new(val, client);
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows to set permissions for channel. [id] can be either User or Role
|
||||
/// Throws if [id] isn't [User] or [Role]
|
||||
Future<void> editChannelPermission(PermissionsBuilder perms, SnowflakeEntity id, {String? auditReason}) {
|
||||
if (id is! Role || id is! User) {
|
||||
throw Exception("The `id` property must be either Role or User");
|
||||
}
|
||||
|
||||
return client._http._execute(BasicRequest._new("/channels/${this.id}/permissions/${id.toString()}",
|
||||
method: "PUT", body: perms._build()._build(), auditLog: auditReason));
|
||||
}
|
||||
|
||||
/// Deletes permission overwrite for given User or Role [id]
|
||||
/// Throws if [id] isn't [User] or [Role]
|
||||
Future<void> deleteChannelPermission(SnowflakeEntity id, {String? auditReason}) async {
|
||||
if (id is! Role || id is! User) {
|
||||
throw Exception("`id` property must be either Role or User");
|
||||
}
|
||||
|
||||
return client._http
|
||||
._execute(BasicRequest._new("/channels/${this.id}/permissions/$id", method: "PUT", auditLog: auditReason));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
part of nyxx;
|
||||
|
||||
/// [TextChannel] represents single text channel on [Guild].
|
||||
/// Inhertits from [MessageChannel] and mixes [GuildChannel].
|
||||
class TextChannel extends MessageChannel with GuildChannel implements Mentionable {
|
||||
/// Emitted when channel pins are updated.
|
||||
late final Stream<ChannelPinsUpdateEvent> pinsUpdated;
|
||||
|
||||
/// [ITextChannel] in context of [Guild].
|
||||
abstract class GuildTextChannel implements Channel, CachelessGuildChannel, ITextChannel {
|
||||
/// The channel's topic.
|
||||
String? topic;
|
||||
|
||||
@override
|
||||
late final String? topic;
|
||||
|
||||
/// The channel's mention string.
|
||||
String get mention => "<#${this.id}>";
|
||||
|
@ -18,31 +12,28 @@ class TextChannel extends MessageChannel with GuildChannel implements Mentionabl
|
|||
late final int slowModeThreshold;
|
||||
|
||||
/// Returns url to this channel.
|
||||
String get url => "https://discordapp.com/channels/${this.guild.id.toString()}"
|
||||
String get url => "https://discordapp.com/channels/${this.guildId.toString()}"
|
||||
"/${this.id.toString()}";
|
||||
|
||||
TextChannel._new(Map<String, dynamic> raw, Guild guild, Nyxx client) : super._new(raw, 0, client) {
|
||||
_initialize(raw, guild);
|
||||
|
||||
/* Constructor????? */
|
||||
void _initialize(Map<String, dynamic> raw, Snowflake guildId, Nyxx client) {
|
||||
this.topic = raw["topic"] as String?;
|
||||
this.slowModeThreshold = raw["rate_limit_per_user"] as int? ?? 0;
|
||||
|
||||
pinsUpdated = client.onChannelPinsUpdate.where((event) => event.channel == this);
|
||||
}
|
||||
|
||||
/// Edits the channel.
|
||||
Future<TextChannel> edit({String? name, String? topic, int? position, int? slowModeTreshold}) async {
|
||||
Future<GuildTextChannel> edit({String? name, String? topic, int? position, int? slowModeThreshold}) async {
|
||||
final body = <String, dynamic>{
|
||||
if (name != null) "name": name,
|
||||
if (topic != null) "topic": topic,
|
||||
if (position != null) "position": position,
|
||||
if (slowModeTreshold != null) "rate_limit_per_user": slowModeTreshold,
|
||||
if (slowModeThreshold != null) "rate_limit_per_user": slowModeThreshold,
|
||||
};
|
||||
|
||||
final response = await client._http._execute(BasicRequest._new("/channels/${this.id}", method: "PATCH", body: body));
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return TextChannel._new(response.jsonBody as Map<String, dynamic>, this.guild, client);
|
||||
return CachelessTextChannel._new(response.jsonBody as Map<String, dynamic>, guildId, client);
|
||||
}
|
||||
|
||||
return Future.error(response);
|
||||
|
@ -65,11 +56,11 @@ class TextChannel extends MessageChannel with GuildChannel implements Mentionabl
|
|||
/// Valid file types for [avatarFile] are jpeg, gif and png.
|
||||
///
|
||||
/// ```
|
||||
/// var webhook = await chan.createWebhook("!a Send nudes kek6407");
|
||||
/// final webhook = await channnel.createWebhook("!a Send nudes kek6407");
|
||||
/// ```
|
||||
Future<Webhook> createWebhook(String name, {File? avatarFile, String auditReason = ""}) async {
|
||||
Future<Webhook> createWebhook(String name, {File? avatarFile, String? auditReason}) async {
|
||||
if (name.isEmpty || name.length > 80) {
|
||||
return Future.error("Webhook's name cannot be shorter than 1 character and longer than 80 characters");
|
||||
return Future.error(ArgumentError("Webhook name cannot be shorter than 1 character and longer than 80 characters"));
|
||||
}
|
||||
|
||||
final body = <String, dynamic>{"name": name};
|
||||
|
@ -91,7 +82,7 @@ class TextChannel extends MessageChannel with GuildChannel implements Mentionabl
|
|||
return Future.error(response);
|
||||
}
|
||||
|
||||
/// Returns pinned [Message]s for [Channel].
|
||||
/// Returns pinned [Message]s for channel.
|
||||
Stream<Message> getPinnedMessages() async* {
|
||||
final response = await client._http._execute(BasicRequest._new("/channels/$id/pins"));
|
||||
|
||||
|
@ -109,3 +100,21 @@ class TextChannel extends MessageChannel with GuildChannel implements Mentionabl
|
|||
/// Returns mention to channel
|
||||
String toString() => this.mention;
|
||||
}
|
||||
|
||||
/// [CachelessTextChannel] represents single text channel on [Guild].
|
||||
/// Inhertits from [MessageChannel] and mixes [CacheGuildChannel].
|
||||
class CachelessTextChannel extends CachelessGuildChannel with GuildTextChannel, MessageChannel, ISend implements Mentionable, ITextChannel {
|
||||
CachelessTextChannel._new(Map<String, dynamic> raw, Snowflake guildId, Nyxx client) : super._new(raw, 0, guildId, client) {
|
||||
_initialize(raw, guildId, client);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// [CachelessTextChannel] represents single text channel on [Guild].
|
||||
/// Inhertits from [MessageChannel] and mixes [CacheGuildChannel].
|
||||
class CacheTextChannel extends CacheGuildChannel with GuildTextChannel, MessageChannel, ISend implements Mentionable, ITextChannel {
|
||||
CacheTextChannel._new(Map<String, dynamic> raw, Guild guild, Nyxx client) : super._new(raw, 0, guild, client) {
|
||||
_initialize(raw, guild.id, client);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,24 +1,17 @@
|
|||
part of nyxx;
|
||||
|
||||
/// Represents VoiceChannel within [Guild]
|
||||
class VoiceChannel extends Channel with GuildChannel {
|
||||
abstract class VoiceChannel implements Channel {
|
||||
/// The channel's bitrate.
|
||||
int? bitrate;
|
||||
late final int? bitrate;
|
||||
|
||||
/// The channel's user limit.
|
||||
int? userLimit;
|
||||
|
||||
VoiceChannel._new(Map<String, dynamic> raw, Guild guild, Nyxx client) : super._new(raw, 2, client) {
|
||||
_initialize(raw, guild);
|
||||
late final int? userLimit;
|
||||
|
||||
void _initialize(Map<String, dynamic> raw) {
|
||||
this.bitrate = raw["bitrate"] as int?;
|
||||
this.userLimit = raw["user_limit"] as int?;
|
||||
}
|
||||
|
||||
/// Allows to get [VoiceState]s of users connected to this channel
|
||||
Iterable<VoiceState> get connectedUsers => this.guild.voiceStates.values.where((e) => e.channel?.id == this.id);
|
||||
|
||||
/// Edits the channel.
|
||||
Future<VoiceChannel> edit({String? name, int? bitrate, int? position, int? userLimit, String? auditReason}) async {
|
||||
final body = <String, dynamic>{
|
||||
if (name != null) "name": name,
|
||||
|
@ -31,9 +24,51 @@ class VoiceChannel extends Channel with GuildChannel {
|
|||
._execute(BasicRequest._new("/channels/${this.id}", method: "PATCH", body: body, auditLog: auditReason));
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return VoiceChannel._new(response.jsonBody as Map<String, dynamic>, this.guild, client);
|
||||
if(this is CacheGuildChannel) {
|
||||
return CacheVoiceChannel._new(response.jsonBody as Map<String, dynamic>, (this as CacheGuildChannel).guild, client);
|
||||
}
|
||||
|
||||
return CachelessVoiceChannel._new(response.jsonBody as Map<String, dynamic>, (this as CachelessVoiceChannel).guildId, client);
|
||||
}
|
||||
|
||||
return Future.error(response);
|
||||
}
|
||||
}
|
||||
|
||||
class CachelessVoiceChannel extends CachelessGuildChannel with VoiceChannel {
|
||||
CachelessVoiceChannel._new(Map<String, dynamic> raw, Snowflake guildId, Nyxx client) : super._new(raw, 2, guildId, client) {
|
||||
_initialize(raw);
|
||||
}
|
||||
|
||||
/// Connects client to channel
|
||||
void connect({bool selfMute = false, bool selfDeafen = false}) {
|
||||
try {
|
||||
final shard = this.client.shardManager.shards.firstWhere((element) => element.guilds.contains(this.guildId));
|
||||
|
||||
shard.changeVoiceState(this.guildId, this.id, selfMute: selfMute, selfDeafen: selfDeafen);
|
||||
} on Error {
|
||||
throw InvalidShardException._new("Cannot find shard for this channel!");
|
||||
}
|
||||
}
|
||||
|
||||
/// Disconnects use from channel.
|
||||
void disconnect() {
|
||||
try {
|
||||
final shard = this.client.shardManager.shards.firstWhere((element) => element.guilds.contains(this.guildId));
|
||||
|
||||
shard.changeVoiceState(this.guildId, null);
|
||||
} on Error {
|
||||
throw InvalidShardException._new("Cannot find shard for this channel!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents VoiceChannel within [Guild]
|
||||
class CacheVoiceChannel extends CacheGuildChannel with VoiceChannel {
|
||||
/// Allows to get [VoiceState]s of users connected to this channel
|
||||
Iterable<VoiceState> get connectedUsers => this.guild.voiceStates.values.where((e) => e.channel?.id == this.id);
|
||||
|
||||
CacheVoiceChannel._new(Map<String, dynamic> raw, Guild guild, Nyxx client) : super._new(raw, 2, guild, client) {
|
||||
_initialize(raw);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,7 +129,7 @@ class Embed implements Convertable<EmbedBuilder> {
|
|||
..thumbnailUrl = this.thumbnail?.url
|
||||
..imageUrl = this.image?.url
|
||||
..author = this.author?.toBuilder()
|
||||
.._fields = this.fields.map((field) => field.toBuilder()).toList();
|
||||
..fields = this.fields.map((field) => field.toBuilder()).toList();
|
||||
|
||||
@override
|
||||
bool operator ==(other) => other is EmbedVideo ? other.url == this.url : false;
|
||||
|
|
|
@ -13,9 +13,9 @@ class ClientUser extends User {
|
|||
this.mfa = data["mfa_enabled"] as bool;
|
||||
}
|
||||
|
||||
/// Allows to get [Member] objects for all guilds for bot user.
|
||||
Map<Guild, Member> getMembership() {
|
||||
final membershipCollection = <Guild, Member>{};
|
||||
/// Allows to get [CacheMember] objects for all guilds for bot user.
|
||||
Map<Guild, IMember> getMembership() {
|
||||
final membershipCollection = <Guild, IMember>{};
|
||||
|
||||
for (final guild in client.guilds.values) {
|
||||
final member = guild.members[this.id];
|
||||
|
@ -31,7 +31,7 @@ class ClientUser extends User {
|
|||
/// Edits current user. This changes user's username - not per guild nickname.
|
||||
Future<User> edit({String? username, File? avatar, String? encodedAvatar}) async {
|
||||
if (username == null && (avatar == null || encodedAvatar == null)) {
|
||||
return Future.error("Cannot edit user with null values");
|
||||
return Future.error(ArgumentError("Cannot edit user with null null arguments"));
|
||||
}
|
||||
|
||||
final body = <String, dynamic>{
|
||||
|
|
|
@ -5,7 +5,7 @@ part of nyxx;
|
|||
///
|
||||
/// ---------
|
||||
///
|
||||
/// [channels] property is Map of [Channel]s but it can be cast to specific Channel subclasses. Example with getting all [TextChannel]s in [Guild]:
|
||||
/// [channels] property is Map of [Channel]s but it can be cast to specific Channel subclasses. Example with getting all [CachelessTextChannel]s in [Guild]:
|
||||
/// ```
|
||||
/// var textChannels = channels.where((channel) => channel is MessageChannel) as List<TextChannel>;
|
||||
/// ```
|
||||
|
@ -27,19 +27,19 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
late String? discoverySplash;
|
||||
|
||||
/// System channel where system messages are sent
|
||||
late final TextChannel? systemChannel;
|
||||
late final CachelessTextChannel? systemChannel;
|
||||
|
||||
/// enabled guild features
|
||||
late final List<String> features;
|
||||
late final Iterable<GuildFeature> features;
|
||||
|
||||
/// The guild's afk channel ID, null if not set.
|
||||
late VoiceChannel? afkChannel;
|
||||
|
||||
|
||||
/// The guild's voice region.
|
||||
late String region;
|
||||
|
||||
/// The channel ID for the guild's widget if enabled.
|
||||
late final GuildChannel? embedChannel;
|
||||
late final CacheGuildChannel? embedChannel;
|
||||
|
||||
/// The guild's AFK timeout.
|
||||
late final int afkTimeout;
|
||||
|
@ -63,22 +63,22 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
late final int systemChannelFlags;
|
||||
|
||||
/// Channel where "PUBLIC" guilds display rules and/or guidelines
|
||||
late final GuildChannel? rulesChannel;
|
||||
late final IGuildChannel? rulesChannel;
|
||||
|
||||
/// The guild owner's ID
|
||||
late final User? owner;
|
||||
|
||||
/// The guild's members.
|
||||
late final Cache<Snowflake, Member> members;
|
||||
late final Cache<Snowflake, IMember> members;
|
||||
|
||||
/// The guild's channels.
|
||||
late final ChannelCache channels;
|
||||
|
||||
/// The guild's roles.
|
||||
late final Cache<Snowflake, Role> roles;
|
||||
late final Cache<Snowflake, IRole> roles;
|
||||
|
||||
/// Guild custom emojis
|
||||
late final Cache<Snowflake, GuildEmoji> emojis;
|
||||
late final Cache<Snowflake, IGuildEmoji> emojis;
|
||||
|
||||
/// Boost level of guild
|
||||
late final PremiumTier premiumTier;
|
||||
|
@ -92,7 +92,7 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
|
||||
/// the id of the channel where admins and moderators
|
||||
/// of "PUBLIC" guilds receive notices from Discord
|
||||
late final GuildChannel? publicUpdatesChannel;
|
||||
late final IGuildChannel? publicUpdatesChannel;
|
||||
|
||||
/// Permission of current(bot) user in this guild
|
||||
Permissions? currentUserPermissions;
|
||||
|
@ -104,12 +104,12 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
String get url => "https://discordapp.com/channels/${this.id.toString()}";
|
||||
|
||||
/// Getter for @everyone role
|
||||
Role get everyoneRole => roles.values.firstWhere((r) => r.name == "@everyone");
|
||||
IRole get everyoneRole => roles.values.firstWhere((r) => (r as Role).name == "@everyone");
|
||||
|
||||
/// Returns member object for bot user
|
||||
Member? get selfMember => members[client.self.id];
|
||||
IMember? get selfMember => members[client.self.id];
|
||||
|
||||
/// Upload limit for this guild in bytes
|
||||
/// File upload limit for channel in bytes.
|
||||
int get fileUploadLimit {
|
||||
const megabyte = 1024 * 1024;
|
||||
|
||||
|
@ -124,6 +124,9 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
return 8 * megabyte;
|
||||
}
|
||||
|
||||
/// Returns this guilds shard
|
||||
Shard get shard => client.shardManager.shards.firstWhere((_shard) => _shard.guilds.contains(this.id));
|
||||
|
||||
Guild._new(this.client, Map<String, dynamic> raw, [this.available = true, bool guildCreate = false])
|
||||
: super(Snowflake(raw["id"] as String)) {
|
||||
if (!this.available) return;
|
||||
|
@ -145,7 +148,7 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
if (raw["roles"] != null) {
|
||||
this.roles = _SnowflakeCache<Role>();
|
||||
raw["roles"].forEach((o) {
|
||||
final role = Role._new(o as Map<String, dynamic>, this, client);
|
||||
final role = Role._new(o as Map<String, dynamic>, this.id, client);
|
||||
this.roles[role.id] = role;
|
||||
});
|
||||
}
|
||||
|
@ -153,23 +156,23 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
this.emojis = _SnowflakeCache();
|
||||
if (raw["emojis"] != null) {
|
||||
raw["emojis"].forEach((dynamic o) {
|
||||
final emoji = GuildEmoji._new(o as Map<String, dynamic>, this, client);
|
||||
final emoji = GuildEmoji._new(o as Map<String, dynamic>, this.id, client);
|
||||
this.emojis[emoji.id] = emoji;
|
||||
});
|
||||
}
|
||||
|
||||
if (raw.containsKey("embed_channel_id")) {
|
||||
this.embedChannel = client.channels[Snowflake(raw["embed_channel_id"])] as GuildChannel;
|
||||
this.embedChannel = client.channels[Snowflake(raw["embed_channel_id"])] as CacheGuildChannel;
|
||||
}
|
||||
|
||||
if (raw["system_channel_id"] != null) {
|
||||
final id = Snowflake(raw["system_channel_id"] as String);
|
||||
if (this.channels.hasKey(id)) {
|
||||
this.systemChannel = this.channels[id] as TextChannel;
|
||||
this.systemChannel = this.channels[id] as CachelessTextChannel;
|
||||
}
|
||||
}
|
||||
|
||||
this.features = (raw["features"] as List<dynamic>).cast<String>();
|
||||
this.features = (raw["features"] as List<dynamic>).map((e) => GuildFeature.from(e.toString()));
|
||||
|
||||
if (raw["permissions"] != null) {
|
||||
this.currentUserPermissions = Permissions.fromInt(raw["permissions"] as int);
|
||||
|
@ -178,7 +181,7 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
if (raw["afk_channel_id"] != null) {
|
||||
final id = Snowflake(raw["afk_channel_id"] as String);
|
||||
if (this.channels.hasKey(id)) {
|
||||
this.afkChannel = this.channels[id] as VoiceChannel;
|
||||
this.afkChannel = this.channels[id] as CacheVoiceChannel;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,15 +195,7 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
if (!guildCreate) return;
|
||||
|
||||
raw["channels"].forEach((o) {
|
||||
late GuildChannel channel;
|
||||
|
||||
if (o["type"] == 0 || o["type"] == 5 || o["type"] == 6) {
|
||||
channel = TextChannel._new(o as Map<String, dynamic>, this, client);
|
||||
} else if (o["type"] == 2) {
|
||||
channel = VoiceChannel._new(o as Map<String, dynamic>, this, client);
|
||||
} else if (o["type"] == 4) {
|
||||
channel = CategoryChannel._new(o as Map<String, dynamic>, this, client);
|
||||
}
|
||||
final channel = Channel._deserialize(o as Map<String, dynamic>, this.client, this);
|
||||
|
||||
this.channels[channel.id] = channel;
|
||||
client.channels[channel.id] = channel;
|
||||
|
@ -208,7 +203,7 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
|
||||
if (client._options.cacheMembers) {
|
||||
raw["members"].forEach((o) {
|
||||
final member = Member._standard(o as Map<String, dynamic>, this, client);
|
||||
final member = CacheMember._standard(o as Map<String, dynamic>, this, client);
|
||||
this.members[member.id] = member;
|
||||
client.users[member.id] = member;
|
||||
});
|
||||
|
@ -237,11 +232,11 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
}
|
||||
|
||||
if (raw["rules_channel_id"] != null) {
|
||||
this.rulesChannel = this.channels[Snowflake(raw["rules_channel_id"])] as GuildChannel?;
|
||||
this.rulesChannel = this.channels[Snowflake(raw["rules_channel_id"])] as IGuildChannel;
|
||||
}
|
||||
|
||||
if (raw["public_updates_channel_id"] != null) {
|
||||
this.publicUpdatesChannel = this.channels[Snowflake(raw["public_updates_channel_id"])] as GuildChannel?;
|
||||
this.publicUpdatesChannel = this.channels[Snowflake(raw["public_updates_channel_id"])] as IGuildChannel?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -293,7 +288,7 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
final response = await client._http._execute(BasicRequest._new("/guilds/$id/emojis/${emojiId.toString()}"));
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, this, client);
|
||||
return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, this.id, client);
|
||||
}
|
||||
|
||||
return Future.error(response);
|
||||
|
@ -308,11 +303,11 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
/// ```
|
||||
Future<GuildEmoji> createEmoji(String name, {List<Role>? roles, File? image, List<int>? imageBytes}) async {
|
||||
if (image != null && await image.length() > 256000) {
|
||||
return Future.error("Emojis and animated emojis have a maximum file size of 256kb.");
|
||||
return Future.error(ArgumentError("Emojis and animated emojis have a maximum file size of 256kb."));
|
||||
}
|
||||
|
||||
if (image == null && imageBytes == null) {
|
||||
return Future.error("Both imageData and file fields cannot be null");
|
||||
return Future.error(ArgumentError("Both imageData and file fields cannot be null"));
|
||||
}
|
||||
|
||||
final body = <String, dynamic>{
|
||||
|
@ -325,7 +320,7 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
._execute(BasicRequest._new("/guilds/${this.id.toString()}/emojis", method: "POST", body: body));
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, this, client);
|
||||
return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, this.id, client);
|
||||
}
|
||||
|
||||
return Future.error(response);
|
||||
|
@ -351,8 +346,8 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
final response = await client._http._execute(BasicRequest._new("/guilds/$id/prune",
|
||||
method: "POST",
|
||||
auditLog: auditReason,
|
||||
queryParams: {
|
||||
"days": days.toString(),
|
||||
queryParams: { "days": days.toString() },
|
||||
body: {
|
||||
if (includeRoles != null) "include_roles": includeRoles.map((e) => e.id.toString())
|
||||
}));
|
||||
|
||||
|
@ -393,7 +388,7 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
}
|
||||
|
||||
/// Change guild owner.
|
||||
Future<Guild> changeOwner(Member member, {String? auditReason}) async {
|
||||
Future<Guild> changeOwner(CacheMember member, {String? auditReason}) async {
|
||||
final response = await client._http._execute(
|
||||
BasicRequest._new("/guilds/$id", method: "PATCH", auditLog: auditReason, body: {"owner_id": member.id}));
|
||||
|
||||
|
@ -410,10 +405,10 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
|
||||
/// Creates invite in first channel possible
|
||||
Future<Invite> createInvite({int maxAge = 0, int maxUses = 0, bool temporary = false, bool unique = false, String? auditReason}) async {
|
||||
final channel = this.channels.first as GuildChannel?;
|
||||
final channel = this.channels.first as CacheGuildChannel?;
|
||||
|
||||
if (channel == null) {
|
||||
return Future.error("Cannot get any channel to create invite to");
|
||||
return Future.error(ArgumentError("Cannot get any channel to create invite to"));
|
||||
}
|
||||
|
||||
return channel.createInvite(
|
||||
|
@ -494,13 +489,13 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
BasicRequest._new("/guilds/$id/roles", method: "POST", auditLog: auditReason, body: roleBuilder._build()));
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return Role._new(response.jsonBody as Map<String, dynamic>, this, client);
|
||||
return Role._new(response.jsonBody as Map<String, dynamic>, this.id, client);
|
||||
}
|
||||
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
/// Adds [Role] to [Member]
|
||||
/// Adds [Role] to [CacheMember]
|
||||
///
|
||||
/// ```
|
||||
/// var role = guild.roles.values.first;
|
||||
|
@ -508,10 +503,10 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
///
|
||||
/// await guild.addRoleToMember(member, role);
|
||||
/// ```
|
||||
Future<void> addRoleToMember(Member user, Role role) async =>
|
||||
Future<void> addRoleToMember(CacheMember user, Role role) async =>
|
||||
client._http._execute(BasicRequest._new("/guilds/$id/members/${user.id}/roles/${role.id}", method: "PUT"));
|
||||
|
||||
/// Returns list of available [VoiceChannel]s
|
||||
/// Returns list of available [CacheVoiceChannel]s
|
||||
Stream<VoiceRegion> getVoiceRegions() async* {
|
||||
final response = await client._http._execute(BasicRequest._new("/guilds/$id/regions"));
|
||||
|
||||
|
@ -530,7 +525,7 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
/// ```
|
||||
/// var chan = await guild.createChannel("Super duper channel", ChannelType.text, nsfw: true);
|
||||
/// ```
|
||||
Future<GuildChannel> createChannel(String name, ChannelType type,
|
||||
Future<CacheGuildChannel> createChannel(String name, ChannelType type,
|
||||
{int? bitrate,
|
||||
String? topic,
|
||||
CategoryChannel? parent,
|
||||
|
@ -540,11 +535,11 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
String? auditReason}) async {
|
||||
// Checks to avoid API panic
|
||||
if (type == ChannelType.dm || type == ChannelType.groupDm) {
|
||||
return Future.error("Cannot create DM channel.");
|
||||
return Future.error(ArgumentError("Cannot create DM channel."));
|
||||
}
|
||||
|
||||
if (type == ChannelType.category && parent != null) {
|
||||
return Future.error("Cannot create Category Channel which have parent channel.");
|
||||
return Future.error(ArgumentError("Cannot create Category Channel which have parent channel."));
|
||||
}
|
||||
|
||||
// Construct body
|
||||
|
@ -566,18 +561,9 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
return Future.error(response);
|
||||
}
|
||||
|
||||
final raw = (response as HttpResponseSuccess).jsonBody;
|
||||
final raw = (response as HttpResponseSuccess).jsonBody as Map<String, dynamic>;
|
||||
|
||||
switch (type._value) {
|
||||
case 0:
|
||||
return TextChannel._new(raw as Map<String, dynamic>, this, client);
|
||||
case 4:
|
||||
return CategoryChannel._new(raw as Map<String, dynamic>, this, client);
|
||||
case 2:
|
||||
return VoiceChannel._new(raw as Map<String, dynamic>, this, client);
|
||||
default:
|
||||
return Future.error("Cannot create DM channel.");
|
||||
}
|
||||
return Channel._deserialize(raw, this.client, this) as CacheGuildChannel;
|
||||
}
|
||||
|
||||
/// Moves channel. Allows to move channel by absolute about with [absolute] or relatively with [relative] parameter.
|
||||
|
@ -586,7 +572,7 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
/// // This moves channel 2 places up
|
||||
/// await guild.moveChannel(chan, relative: -2);
|
||||
/// ```
|
||||
Future<void> moveChannel(GuildChannel channel, {int? absolute, int? relative, String? auditReason}) async {
|
||||
Future<void> moveChannel(CacheGuildChannel channel, {int? absolute, int? relative, String? auditReason}) async {
|
||||
var newPosition = 0;
|
||||
|
||||
if (relative != null) {
|
||||
|
@ -594,10 +580,10 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
} else if (absolute != null) {
|
||||
newPosition = absolute;
|
||||
} else {
|
||||
return Future.error("Cannot move channel by zero places");
|
||||
return Future.error(ArgumentError("Cannot move channel by zero places"));
|
||||
}
|
||||
|
||||
return client._http._execute(BasicRequest._new("/guilds/${this.id}/channels",
|
||||
await client._http._execute(BasicRequest._new("/guilds/${this.id}/channels",
|
||||
method: "PATCH", auditLog: auditReason, body: {"id": channel.id.toString(), "position": newPosition}));
|
||||
}
|
||||
|
||||
|
@ -606,8 +592,8 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
///
|
||||
/// await guild.ban(member);
|
||||
/// ```
|
||||
Future<void> ban(Member member, {int deleteMessageDays = 0, String? auditReason}) async =>
|
||||
client._http._execute(BasicRequest._new("/guilds/${this.id}/bans/${member.id.toString()}",
|
||||
Future<void> ban(SnowflakeEntity user, {int deleteMessageDays = 0, String? auditReason}) async =>
|
||||
client._http._execute(BasicRequest._new("/guilds/${this.id}/bans/${user.id.toString()}",
|
||||
method: "PUT", auditLog: auditReason, body: {"delete-message-days": deleteMessageDays}));
|
||||
|
||||
/// Kicks user from guild. Member is removed from guild and he is able to rejoin
|
||||
|
@ -615,8 +601,8 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
/// ```
|
||||
/// await guild.kick(member);
|
||||
/// ```
|
||||
Future<void> kick(Member member, {String? auditReason}) async =>
|
||||
client._http._execute(BasicRequest._new("/guilds/${this.id.toString()}/members/${member.id.toString()}",
|
||||
Future<void> kick(SnowflakeEntity user, {String? auditReason}) async =>
|
||||
client._http._execute(BasicRequest._new("/guilds/${this.id.toString()}/members/${user.id.toString()}",
|
||||
method: "DELTE", auditLog: auditReason));
|
||||
|
||||
/// Unbans a user by ID.
|
||||
|
@ -628,7 +614,7 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
{String? name,
|
||||
int? verificationLevel,
|
||||
int? notificationLevel,
|
||||
VoiceChannel? afkChannel,
|
||||
CacheVoiceChannel? afkChannel,
|
||||
int? afkTimeout,
|
||||
String? icon,
|
||||
String? auditReason}) async {
|
||||
|
@ -651,27 +637,27 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
return Future.error(response);
|
||||
}
|
||||
|
||||
/// Gets a [Member] object. Caches fetched member if not cached.
|
||||
/// Gets a [CacheMember] object. Caches fetched member if not cached.
|
||||
///
|
||||
/// ```
|
||||
/// var member = guild.getMember(user);
|
||||
/// ```
|
||||
Future<Member> getMember(User user) async => getMemberById(user.id);
|
||||
Future<IMember> getMember(User user) async => getMemberById(user.id);
|
||||
|
||||
/// Gets a [Member] object by id. Caches fetched member if not cached.
|
||||
/// Gets a [CacheMember] object by id. Caches fetched member if not cached.
|
||||
///
|
||||
/// ```
|
||||
/// var member = guild.getMember(Snowflake("302359795648954380"));
|
||||
/// ```
|
||||
Future<Member> getMemberById(Snowflake id) async {
|
||||
Future<IMember> getMemberById(Snowflake id) async {
|
||||
if (this.members.hasKey(id)) {
|
||||
return this.members[id] as Member;
|
||||
return this.members[id]!;
|
||||
}
|
||||
|
||||
final response = await client._http._execute(BasicRequest._new("/guilds/${this.id}/members/${id.toString()}"));
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return Member._standard(response.jsonBody as Map<String, dynamic>, this, client);
|
||||
return CacheMember._standard(response.jsonBody as Map<String, dynamic>, this, client);
|
||||
}
|
||||
|
||||
return Future.error(response);
|
||||
|
@ -680,7 +666,7 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
/// Allows to fetch guild members. In future will be restricted with `Priviliged Intents`.
|
||||
/// [after] is used to continue from specified user id.
|
||||
/// By default limits to one user - use [limit] paramter to change that behavior.
|
||||
Stream<Member> getMembers({int limit = 1, Snowflake? after}) async* {
|
||||
Stream<CacheMember> getMembers({int limit = 1, Snowflake? after}) async* {
|
||||
final request = this.client._http._execute(BasicRequest._new("/guilds/${this.id.toString()}/members",
|
||||
queryParams: {"limit": limit.toString(), if (after != null) "after": after.toString()}));
|
||||
|
||||
|
@ -689,13 +675,13 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
}
|
||||
|
||||
for (final rawMember in (request as HttpResponseSuccess).jsonBody as List<dynamic>) {
|
||||
yield Member._standard(rawMember as Map<String, dynamic>, this, client);
|
||||
yield CacheMember._standard(rawMember as Map<String, dynamic>, this, client);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a [Stream] of [Member] objects whose username or nickname starts with a provided string.
|
||||
/// Returns a [Stream] of [CacheMember] objects whose username or nickname starts with a provided string.
|
||||
/// By default limits to one entry - can be changed with [limit] parameter.
|
||||
Stream<Member> searchMembers(String query, {int limit = 1}) async* {
|
||||
Stream<CacheMember> searchMembers(String query, {int limit = 1}) async* {
|
||||
final response = await client._http._execute(BasicRequest._new("/guilds/${this.id}/members/search",
|
||||
queryParams: {"query": query, "limit": limit.toString()}));
|
||||
|
||||
|
@ -704,25 +690,24 @@ class Guild extends SnowflakeEntity implements Disposable {
|
|||
}
|
||||
|
||||
for (final Map<String, dynamic> member in (response as HttpResponseSuccess).jsonBody) {
|
||||
yield Member._standard(member, this, client);
|
||||
yield CacheMember._standard(member, this, client);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a [Stream] of [Member] objects whose username or nickname starts with a provided string.
|
||||
/// Returns a [Stream] of [CacheMember] objects whose username or nickname starts with a provided string.
|
||||
/// By default limits to one entry - can be changed with [limit] parameter.
|
||||
Stream<Member> searchMembersGateway(String query, {int limit = 0}) async* {
|
||||
Stream<IMember> searchMembersGateway(String query, {int limit = 0}) async* {
|
||||
final nonce = "$query${id.toString()}";
|
||||
this.shard.requestMembers(this.id, query: query, limit: limit, nonce: nonce);
|
||||
|
||||
this.client.shard.requestMembers(this.id, query: query, limit: limit, nonce: nonce);
|
||||
|
||||
final first = (await this.client.shard.onMemberChunk.take(1).toList()).first;
|
||||
final first = (await this.shard.onMemberChunk.take(1).toList()).first;
|
||||
|
||||
for (final member in first.members) {
|
||||
yield member;
|
||||
}
|
||||
|
||||
if (first.chunkCount > 1) {
|
||||
await for (final event in this.client.shard.onMemberChunk.where((event) => event.nonce == nonce).take(first.chunkCount - 1)) {
|
||||
await for (final event in this.shard.onMemberChunk.where((event) => event.nonce == nonce).take(first.chunkCount - 1)) {
|
||||
for (final member in event.members) {
|
||||
yield member;
|
||||
}
|
||||
|
|
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;
|
||||
|
|
|
@ -1,7 +1,41 @@
|
|||
part of nyxx;
|
||||
|
||||
/// Represents [Guild] role.
|
||||
/// Interface allows basic operations on member but does not guarantee data to be valid or available
|
||||
class IRole extends SnowflakeEntity {
|
||||
/// Reference to client
|
||||
final Nyxx client;
|
||||
|
||||
/// Id of role's [Guild]
|
||||
final Snowflake guildId;
|
||||
|
||||
IRole._new(Snowflake id, this.guildId, this.client) : super(id);
|
||||
|
||||
/// Edits the role.
|
||||
Future<Role> edit(RoleBuilder role, {String? auditReason}) async {
|
||||
final response = await client._http._execute(BasicRequest._new("/guilds/${this.guildId}/roles/$id",
|
||||
method: "PATCH", body: role._build(), auditLog: auditReason));
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return Role._new(response.jsonBody as Map<String, dynamic>, this.guildId, client);
|
||||
}
|
||||
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
/// Deletes the role.
|
||||
Future<void> delete({String? auditReason}) =>
|
||||
client._http
|
||||
._execute(BasicRequest._new("/guilds/${this.guildId}/roles/$id", method: "DELETE", auditLog: auditReason));
|
||||
|
||||
/// Adds role to user.
|
||||
Future<void> addToUser(User user, {String? auditReason}) =>
|
||||
client._http._execute(
|
||||
BasicRequest._new("/guilds/${this.guildId}/members/${user.id}/roles/$id", method: "PUT", auditLog: auditReason));
|
||||
}
|
||||
|
||||
/// Represents a Discord guild role, which is used to assign priority, permissions, and a color to guild members
|
||||
class Role extends SnowflakeEntity implements Mentionable, GuildEntity {
|
||||
class Role extends IRole implements Mentionable, GuildEntity {
|
||||
/// The role's name.
|
||||
late final String name;
|
||||
|
||||
|
@ -22,24 +56,19 @@ class Role extends SnowflakeEntity implements Mentionable, GuildEntity {
|
|||
|
||||
/// The role's guild.
|
||||
@override
|
||||
late final Guild guild;
|
||||
late final Guild? guild;
|
||||
|
||||
/// The role's permissions.
|
||||
late final Permissions permissions;
|
||||
|
||||
/// Returns all members which have this role assigned
|
||||
Iterable<Member> get members => guild.members.find((m) => m.roles.contains(this));
|
||||
|
||||
@override
|
||||
|
||||
/// Mention of role. If role cannot be mentioned it returns name of role (@name)
|
||||
@override
|
||||
String get mention => mentionable ? "<@&${this.id}>" : "@$name";
|
||||
|
||||
/// Additional role data like if role is managed by integration or role is from server boosting.
|
||||
late final RoleTags? roleTags;
|
||||
|
||||
/// Reference to [Nyxx] instance
|
||||
Nyxx client;
|
||||
|
||||
Role._new(Map<String, dynamic> raw, this.guild, this.client) : super(Snowflake(raw["id"] as String)) {
|
||||
Role._new(Map<String, dynamic> raw, Snowflake guildId, Nyxx client) : super._new(Snowflake(raw["id"]), guildId, client) {
|
||||
this.name = raw["name"] as String;
|
||||
this.position = raw["position"] as int;
|
||||
this.hoist = raw["hoist"] as bool;
|
||||
|
@ -47,31 +76,38 @@ class Role extends SnowflakeEntity implements Mentionable, GuildEntity {
|
|||
this.mentionable = raw["mentionable"] as bool? ?? false;
|
||||
this.permissions = Permissions.fromInt(raw["permissions"] as int);
|
||||
this.color = DiscordColor.fromInt(raw["color"] as int);
|
||||
}
|
||||
|
||||
/// Edits the role.
|
||||
Future<Role> edit(RoleBuilder role, {String? auditReason}) async {
|
||||
final response = await client._http._execute(BasicRequest._new("/guilds/${this.guild.id}/roles/$id",
|
||||
method: "PATCH", body: role._build(), auditLog: auditReason));
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return Role._new(response.jsonBody as Map<String, dynamic>, this.guild, client);
|
||||
if(raw["tags"] != null) {
|
||||
this.roleTags = RoleTags._new(raw["tags"] as Map<String, dynamic>);
|
||||
} else {
|
||||
this.roleTags = null;
|
||||
}
|
||||
|
||||
return Future.error(response);
|
||||
this.guild = client.guilds[this.guildId];
|
||||
}
|
||||
|
||||
/// Deletes the role.
|
||||
Future<void> delete({String? auditReason}) =>
|
||||
client._http
|
||||
._execute(BasicRequest._new("/guilds/${this.guild.id}/roles/$id", method: "DELETE", auditLog: auditReason));
|
||||
|
||||
/// Adds role to user.
|
||||
Future<void> addToUser(User user, {String? auditReason}) =>
|
||||
client._http._execute(
|
||||
BasicRequest._new("/guilds/${guild.id}/members/${user.id}/roles/$id", method: "PUT", auditLog: auditReason));
|
||||
|
||||
/// Returns a mention of role. If role cannot be mentioned it returns name of role.
|
||||
@override
|
||||
String toString() => mention;
|
||||
}
|
||||
|
||||
/// Additional [Role] role tags which hold optional data about role
|
||||
class RoleTags {
|
||||
/// Holds [Snowflake] of bot id if role is for bot user
|
||||
late final Snowflake? botId;
|
||||
|
||||
/// True if role is for server nitro boosting
|
||||
late final bool nitroRole;
|
||||
|
||||
/// Holds [Snowflake] of integration if role is part of twitch/other integration
|
||||
late final Snowflake? integrationId;
|
||||
|
||||
/// Returns true if role is for bot.
|
||||
bool get isBotRole => botId != null;
|
||||
|
||||
RoleTags._new(Map<String, dynamic> raw) {
|
||||
this.botId = raw["bot_id"] != null ? Snowflake(raw["bot_id"]) : null;
|
||||
this.nitroRole = raw["premium_subscriber"] != null ? raw["premium_subscriber"] as bool : false;
|
||||
this.integrationId = raw["integration_id"] != null ? Snowflake(raw["integration_id"]) : null;
|
||||
}
|
||||
}
|
|
@ -3,7 +3,10 @@ part of nyxx;
|
|||
/// Type of webhook. Either [incoming] if it its normal webhook executable with token,
|
||||
/// or [channelFollower] if its discord internal webhook
|
||||
class WebhookType extends IEnum<int> {
|
||||
/// Incoming Webhooks can post messages to channels with a generated token
|
||||
static const WebhookType incoming = WebhookType._create(1);
|
||||
|
||||
/// Channel Follower Webhooks are internal webhooks used with Channel Following to post new messages into channels
|
||||
static const WebhookType channelFollower = WebhookType._create(2);
|
||||
|
||||
const WebhookType._create(int? value) : super(value ?? 0);
|
||||
|
@ -25,11 +28,11 @@ class Webhook extends SnowflakeEntity implements IMessageAuthor {
|
|||
/// The webhook's name.
|
||||
late final String? name;
|
||||
|
||||
/// The webhook's token.
|
||||
late final String? token;
|
||||
/// The webhook's token. Defaults to empty string
|
||||
late final String token;
|
||||
|
||||
/// The webhook's channel, if this is accessed using a normal client and the client has that channel in it's cache.
|
||||
late final TextChannel? channel;
|
||||
late final GuildTextChannel? channel;
|
||||
|
||||
/// The webhook's guild, if this is accessed using a normal client and the client has that guild in it's cache.
|
||||
late final Guild? guild;
|
||||
|
@ -37,13 +40,15 @@ class Webhook extends SnowflakeEntity implements IMessageAuthor {
|
|||
/// The user, if this is accessed using a normal client.
|
||||
late final User? user;
|
||||
|
||||
// TODO: Create data class
|
||||
/// Webhook type
|
||||
late final WebhookType type;
|
||||
|
||||
/// Webhooks avatar hash
|
||||
late final String? avatarHash;
|
||||
|
||||
/// Default webhook avatar id
|
||||
int get defaultAvatarId => 0;
|
||||
|
||||
@override
|
||||
String get username => this.name.toString();
|
||||
|
||||
|
@ -62,12 +67,12 @@ class Webhook extends SnowflakeEntity implements IMessageAuthor {
|
|||
|
||||
Webhook._new(Map<String, dynamic> raw, this.client) : super(Snowflake(raw["id"] as String)) {
|
||||
this.name = raw["name"] as String?;
|
||||
this.token = raw["token"] as String?;
|
||||
this.token = raw["token"] as String? ?? "";
|
||||
this.avatarHash = raw["avatar"] as String?;
|
||||
this.type = WebhookType.from(raw["type"] as int);
|
||||
|
||||
if (raw["channel_id"] != null) {
|
||||
this.channel = client.channels[Snowflake(raw["channel_id"] as String)] as TextChannel?;
|
||||
this.channel = client.channels[Snowflake(raw["channel_id"] as String)] as GuildTextChannel?;
|
||||
}
|
||||
|
||||
if (raw["guild_id"] != null) {
|
||||
|
@ -79,17 +84,62 @@ class Webhook extends SnowflakeEntity implements IMessageAuthor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Executes webhook. Webhooks can send multiple embeds in one messsage using [embeds].
|
||||
///
|
||||
/// [wait] - waits for server confirmation of message send before response,
|
||||
/// and returns the created message body (defaults to false; when false a message that is not save does not return an error)
|
||||
Future<Message> execute(
|
||||
{dynamic content,
|
||||
List<AttachmentBuilder>? files,
|
||||
List<EmbedBuilder>? embeds,
|
||||
bool? tts,
|
||||
AllowedMentions? allowedMentions,
|
||||
bool? wait,
|
||||
String? avatarUrl}) async {
|
||||
allowedMentions ??= client._options.allowedMentions;
|
||||
|
||||
final reqBody = {
|
||||
if (content != null) "content": content.toString(),
|
||||
if (allowedMentions != null) "allowed_mentions": allowedMentions._build(),
|
||||
if(embeds != null) "embeds" : [
|
||||
for(final e in embeds)
|
||||
e._build()
|
||||
],
|
||||
if (content != null && tts != null) "tts": tts,
|
||||
if(avatarUrl != null) "avatar_url" : avatarUrl,
|
||||
};
|
||||
|
||||
final queryParams = { if(wait != null) "wait" : wait };
|
||||
|
||||
_HttpResponse response;
|
||||
|
||||
if (files != null && files.isNotEmpty) {
|
||||
response = await client._http
|
||||
._execute(MultipartRequest._new("/webhooks/${this.id.toString()}/${this.token}", files, method: "POST", fields: reqBody, queryParams: queryParams));
|
||||
} else {
|
||||
response = await client._http
|
||||
._execute(BasicRequest._new("/webhooks/${this.id.toString()}/${this.token}", body: reqBody, method: "POST", queryParams: queryParams));
|
||||
}
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return Message._deserialize(response.jsonBody as Map<String, dynamic>, client);
|
||||
}
|
||||
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
@override
|
||||
String avatarURL({String format = "webp", int size = 128}) {
|
||||
if (this.avatarHash != null) {
|
||||
return "https://cdn.${Constants.cdnHost}/avatars/${this.id}/${this.avatarHash}.$format?size=$size";
|
||||
}
|
||||
|
||||
return "https://cdn.${Constants.cdnHost}/embed/avatars/0.png?size=$size";
|
||||
return "https://cdn.${Constants.cdnHost}/embed/avatars/$defaultAvatarId.png?size=$size";
|
||||
}
|
||||
|
||||
/// Edits the webhook.
|
||||
Future<Webhook> edit({String? name, TextChannel? channel, File? avatar, String? encodedAvatar, String? auditReason}) async {
|
||||
Future<Webhook> edit(
|
||||
{String? name, ITextChannel? channel, File? avatar, String? encodedAvatar, String? auditReason}) async {
|
||||
final body = <String, dynamic>{
|
||||
if (name != null) "name": name,
|
||||
if (channel != null) "channel_id": channel.id.toString()
|
||||
|
@ -111,7 +161,7 @@ class Webhook extends SnowflakeEntity implements IMessageAuthor {
|
|||
|
||||
/// Deletes the webhook.
|
||||
Future<void> delete({String? auditReason}) =>
|
||||
client._http._execute(BasicRequest._new("/webhooks/$id/$token", method: "DELETE", auditLog: auditReason));
|
||||
client._http._execute(BasicRequest._new("/webhooks/$id/$token", method: "DELETE", auditLog: auditReason));
|
||||
|
||||
/// Returns a string representation of this object.
|
||||
@override
|
||||
|
|
|
@ -3,15 +3,12 @@ part of nyxx;
|
|||
/// Represents emoji. Subclasses provides abstraction to custom emojis(like [GuildEmoji]).
|
||||
abstract class Emoji {
|
||||
/// Emojis name.
|
||||
String name;
|
||||
|
||||
Emoji(this.name);
|
||||
final String? name;
|
||||
|
||||
Emoji._new(this.name);
|
||||
|
||||
// TODO: Emojis stuff
|
||||
factory Emoji._deserialize(Map<String, dynamic> raw) {
|
||||
if (raw["id"] != null) {
|
||||
return GuildEmoji._partial(raw);
|
||||
}
|
||||
|
||||
return UnicodeEmoji(raw["name"] as String);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,24 +1,57 @@
|
|||
part of nyxx;
|
||||
|
||||
abstract class IGuildEmoji extends Emoji implements SnowflakeEntity {
|
||||
/// True if emoji is partial.
|
||||
final bool partial;
|
||||
|
||||
/// Snowflake id of emoji
|
||||
@override
|
||||
late final Snowflake id;
|
||||
|
||||
@override
|
||||
DateTime get createdAt => id.timestamp;
|
||||
|
||||
IGuildEmoji._new(Map<String, dynamic> raw, this.partial) : super._new(raw["name"] as String?) {
|
||||
this.id = Snowflake(raw["id"] as String);
|
||||
}
|
||||
}
|
||||
|
||||
class PartialGuildEmoji extends IGuildEmoji {
|
||||
PartialGuildEmoji._new(Map<String, dynamic> raw) : super._new(raw, true);
|
||||
|
||||
/// Encodes Emoji to API format
|
||||
@override
|
||||
String encode() => "$id";
|
||||
|
||||
/// Formats Emoji to message format
|
||||
@override
|
||||
String format() => "<:$id>";
|
||||
|
||||
/// Returns cdn url to emoji
|
||||
String get cdnUrl => "https://cdn.discordapp.com/emojis/${this.id}.png";
|
||||
|
||||
/// Returns encoded string ready to send via message.
|
||||
@override
|
||||
String toString() => format();
|
||||
}
|
||||
|
||||
/// Emoji object. Handles Unicode emojis and custom ones.
|
||||
/// Always check if object is partial via [partial] field before accessing fields or methods,
|
||||
/// due any of field can be null or empty
|
||||
class GuildEmoji extends Emoji implements SnowflakeEntity, GuildEntity {
|
||||
class GuildEmoji extends IGuildEmoji implements SnowflakeEntity, GuildEntity {
|
||||
/// Reference tp [Nyxx] object
|
||||
Nyxx? client;
|
||||
Nyxx client;
|
||||
|
||||
/// Emojis guild
|
||||
@override
|
||||
late final Guild? guild;
|
||||
|
||||
/// Emoji guild
|
||||
late final Guild guild;
|
||||
|
||||
/// Emojis guild id
|
||||
@override
|
||||
late final Snowflake guildId;
|
||||
|
||||
/// Snowflake id of emoji
|
||||
late final Snowflake id;
|
||||
|
||||
/// Roles this emoji is whitelisted to
|
||||
late final List<Role> roles;
|
||||
/// Roles which can use this emote
|
||||
late final Iterable<IRole> roles;
|
||||
|
||||
/// whether this emoji must be wrapped in colons
|
||||
late final bool requireColons;
|
||||
|
@ -29,43 +62,25 @@ class GuildEmoji extends Emoji implements SnowflakeEntity, GuildEntity {
|
|||
/// whether this emoji is animated
|
||||
late final bool animated;
|
||||
|
||||
/// True if emoji is partial.
|
||||
/// Always check before accessing fields or methods, due any of field can be null or empty
|
||||
late final bool partial;
|
||||
|
||||
/// Creates full emoji object
|
||||
GuildEmoji._new(Map<String, dynamic> raw, this.guild, this.client) : super("") {
|
||||
this.id = Snowflake(raw["id"] as String);
|
||||
GuildEmoji._new(Map<String, dynamic> raw, this.guildId, this.client) : super._new(raw, false) {
|
||||
this.guild = client.guilds[this.guildId];
|
||||
|
||||
this.name = raw["name"] as String;
|
||||
this.requireColons = raw["require_colons"] as bool? ?? false;
|
||||
this.managed = raw["managed"] as bool? ?? false;
|
||||
this.animated = raw["animated"] as bool? ?? false;
|
||||
|
||||
this.roles = [];
|
||||
if (raw["roles"] != null) {
|
||||
for (final roleId in raw["roles"]) {
|
||||
final role = this.guild.roles[Snowflake(roleId)];
|
||||
|
||||
if (role != null) {
|
||||
this.roles.add(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.partial = false;
|
||||
}
|
||||
|
||||
/// Creates partial object - only [id] and [name]
|
||||
GuildEmoji._partial(Map<String, dynamic> raw) : super(raw["name"] as String) {
|
||||
this.id = Snowflake(raw["id"] as String);
|
||||
this.partial = true;
|
||||
this.roles = [
|
||||
if (raw["roles"] != null)
|
||||
for (final roleId in raw["roles"])
|
||||
IRole._new(Snowflake(roleId), this.guildId, client)
|
||||
];
|
||||
}
|
||||
|
||||
/// Allows to edit emoji
|
||||
Future<GuildEmoji> edit({String? name, List<Snowflake>? roles}) async {
|
||||
if (name == null && roles == null) {
|
||||
return Future.error("Both name and roles fields cannot be null");
|
||||
return Future.error(ArgumentError("Both name and roles fields cannot be null"));
|
||||
}
|
||||
|
||||
final body = <String, dynamic>{
|
||||
|
@ -73,20 +88,20 @@ class GuildEmoji extends Emoji implements SnowflakeEntity, GuildEntity {
|
|||
if (roles != null) "roles": roles.map((r) => r.toString())
|
||||
};
|
||||
|
||||
final response = await client?._http._execute(
|
||||
BasicRequest._new("/guilds/${guild.id.toString()}/emojis/${this.id.toString()}", method: "PATCH", body: body));
|
||||
final response = await client._http._execute(
|
||||
BasicRequest._new("/guilds/${this.guildId}/emojis/${this.id.toString()}", method: "PATCH", body: body));
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, guild, client);
|
||||
return GuildEmoji._new(response.jsonBody as Map<String, dynamic>, this.guildId, client);
|
||||
}
|
||||
|
||||
return Future.error(response ?? "Cannot perform http request if emoji is partial");
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
/// Deletes emoji
|
||||
Future<void> delete() async =>
|
||||
client?._http._execute(
|
||||
BasicRequest._new("/guilds/${this.guild.id.toString()}/emojis/${this.id.toString()}", method: "DELETE"));
|
||||
client._http._execute(
|
||||
BasicRequest._new("/guilds/${this.guildId}/emojis/${this.id.toString()}", method: "DELETE"));
|
||||
|
||||
/// Encodes Emoji to API format
|
||||
@override
|
||||
|
|
|
@ -8,14 +8,15 @@ class DMMessage extends Message {
|
|||
/// Returns clickable url to this message.
|
||||
@override
|
||||
String get url => "https://discordapp.com/channels/@me"
|
||||
"/${this.channel.id}/${this.id}";
|
||||
"/${this.channelId}/${this.id}";
|
||||
|
||||
DMMessage._new(Map<String, dynamic> raw, Nyxx client) : super._new(raw, client) {
|
||||
final user = client.users[Snowflake(raw["author"]["id"] as String)];
|
||||
final user = client.users[Snowflake(raw["author"]["id"])];
|
||||
|
||||
if (user == null) {
|
||||
final authorData = raw["author"] as Map<String, dynamic>;
|
||||
this.author = User._new(authorData, client);
|
||||
this.client.users.add(this.author.id, this.author);
|
||||
} else {
|
||||
this.author = user;
|
||||
}
|
||||
|
@ -26,7 +27,10 @@ class DMMessage extends Message {
|
|||
class GuildMessage extends Message implements GuildEntity {
|
||||
/// The message's guild.
|
||||
@override
|
||||
late final Guild guild;
|
||||
late final Guild? guild;
|
||||
|
||||
/// Id of message's guild
|
||||
late final Snowflake guildId;
|
||||
|
||||
/// Reference to original message if this message cross posts other message
|
||||
late final MessageReference? crosspostReference;
|
||||
|
@ -36,10 +40,10 @@ class GuildMessage extends Message implements GuildEntity {
|
|||
|
||||
/// Returns clickable url to this message.
|
||||
@override
|
||||
String get url => "https://discordapp.com/channels/${this.guild.id}"
|
||||
"/${this.channel.id}/${this.id}";
|
||||
String get url => "https://discordapp.com/channels/${this.guildId}"
|
||||
"/${this.channelId}/${this.id}";
|
||||
|
||||
/// The message's author. Can be instance of [Member] or [Webhook]
|
||||
/// The message's author. Can be instance of [CacheMember] or [Webhook]
|
||||
@override
|
||||
late final IMessageAuthor author;
|
||||
|
||||
|
@ -47,33 +51,34 @@ class GuildMessage extends Message implements GuildEntity {
|
|||
/// True if message is sent by a webhook
|
||||
bool get isByWebhook => author is Webhook;
|
||||
|
||||
/// A list of IDs for the role mentions in the message.
|
||||
late List<Role> roleMentions;
|
||||
/// Role mentions in this message
|
||||
late final List<IRole> roleMentions;
|
||||
|
||||
GuildMessage._new(Map<String, dynamic> raw, Nyxx client) : super._new(raw, client) {
|
||||
if (raw["message_reference"] != null) {
|
||||
this.crosspostReference = MessageReference._new(raw["message_reference"] as Map<String, dynamic>, client);
|
||||
}
|
||||
|
||||
this.guild = client.guilds[Snowflake(raw["guild_id"])]!;
|
||||
this.guildId = Snowflake(raw["guild_id"]);
|
||||
this.guild = client.guilds[this.guildId];
|
||||
|
||||
if (raw["webhook_id"] != null) {
|
||||
this.author = Webhook._new(raw["author"] as Map<String, dynamic>, client);
|
||||
} else if (raw["author"] != null) {
|
||||
final member = this.guild.members[Snowflake(raw["author"]["id"] as String)];
|
||||
final member = this.guild?.members[Snowflake(raw["author"]["id"])];
|
||||
|
||||
if (member == null) {
|
||||
if (raw["member"] == null) {
|
||||
this.author = User._new(raw["author"] as Map<String, dynamic>, client);
|
||||
this.client.users[this.author.id] = this.author as User;
|
||||
} else {
|
||||
final authorData = raw["author"] as Map<String, dynamic>;
|
||||
final memberData = raw["member"] as Map<String, dynamic>;
|
||||
|
||||
final author =
|
||||
Member._fromUser(authorData, memberData, client.guilds[Snowflake(raw["guild_id"])] as Guild, client);
|
||||
|
||||
final author = this.guild == null ? CachelessMember._fromUser(authorData, memberData, guildId, client) : CacheMember._fromUser(authorData, memberData, this.guild!, client);
|
||||
|
||||
client.users[author.id] = author;
|
||||
guild.members[author.id] = author;
|
||||
guild?.members[author.id] = author;
|
||||
this.author = author;
|
||||
}
|
||||
} else {
|
||||
|
@ -83,7 +88,8 @@ class GuildMessage extends Message implements GuildEntity {
|
|||
|
||||
this.roleMentions = [
|
||||
if (raw["mention_roles"] != null)
|
||||
for (var r in raw["mention_roles"]) this.guild.roles[Snowflake(r)] as Role
|
||||
for (var roleId in raw["mention_roles"])
|
||||
this.guild == null ? IRole ._new(Snowflake(roleId), guildId, client) : guild!.roles[Snowflake(roleId)]!
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -91,7 +97,7 @@ class GuildMessage extends Message implements GuildEntity {
|
|||
/// This endpoint requires the "DISCOVERY" feature to be present for the guild.
|
||||
Future<void> crosspost() async =>
|
||||
this.client._http._execute(BasicRequest._new(
|
||||
"/channels/${this.channel.id.toString()}/messages/${this.id.toString()}/crosspost",
|
||||
"/channels/${this.channelId.toString()}/messages/${this.id.toString()}/crosspost",
|
||||
method: "POST"));
|
||||
}
|
||||
|
||||
|
@ -105,15 +111,18 @@ abstract class Message extends SnowflakeEntity implements Disposable {
|
|||
late String content;
|
||||
|
||||
/// Channel in which message was sent
|
||||
late final MessageChannel channel;
|
||||
late final ITextChannel channel;
|
||||
|
||||
/// Id of channel in which message was sent
|
||||
late final Snowflake channelId;
|
||||
|
||||
/// The timestamp of when the message was last edited, null if not edited.
|
||||
late final DateTime? editedTimestamp;
|
||||
|
||||
/// The message's author. Can be instance of [Member]
|
||||
/// The message's author. Can be instance of [CacheMember]
|
||||
IMessageAuthor get author;
|
||||
|
||||
/// The mentions in the message. [User] value of this map can be [Member]
|
||||
/// The mentions in the message. [User] value of this map can be [CacheMember]
|
||||
late List<User> mentions;
|
||||
|
||||
/// A collection of the embeds in the message.
|
||||
|
@ -151,10 +160,18 @@ abstract class Message extends SnowflakeEntity implements Disposable {
|
|||
return DMMessage._new(raw, client);
|
||||
}
|
||||
|
||||
Message._new(Map<String, dynamic> raw, this.client) : super(Snowflake(raw["id"] as String)) {
|
||||
Message._new(Map<String, dynamic> raw, this.client) : super(Snowflake(raw["id"])) {
|
||||
this.content = raw["content"] as String;
|
||||
|
||||
this.channelId = Snowflake(raw["channel_id"]);
|
||||
final channel = client.channels[this.channelId] as ITextChannel?;
|
||||
|
||||
this.channel = client.channels[Snowflake(raw["channel_id"] as String)] as MessageChannel;
|
||||
if(channel == null) {
|
||||
// TODO: channel stuff
|
||||
this.channel = DummyTextChannel._new({"id" : raw["channel_id"]}, -1, client);
|
||||
} else {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
this.pinned = raw["pinned"] as bool;
|
||||
this.tts = raw["tts"] as bool;
|
||||
|
@ -194,23 +211,44 @@ abstract class Message extends SnowflakeEntity implements Disposable {
|
|||
@override
|
||||
String toString() => this.content;
|
||||
|
||||
// TODO: Manage message flags better
|
||||
/// Suppresses embeds in message. Can be executed in other users messages.
|
||||
Future<Message> suppressEmbeds() async {
|
||||
final body = <String, dynamic>{
|
||||
"flags" : 1 << 2
|
||||
};
|
||||
|
||||
final response = await client._http
|
||||
._execute(BasicRequest._new("/channels/${this.channelId}/messages/${this.id}", method: "PATCH", body: body));
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return Message._deserialize(response.jsonBody as Map<String, dynamic>, client);
|
||||
}
|
||||
|
||||
return Future.error(response);
|
||||
}
|
||||
|
||||
/// Edits the message.
|
||||
///
|
||||
/// Throws an [Exception] if the HTTP request errored.
|
||||
/// Message.edit("My edited content!");
|
||||
Future<Message> edit({dynamic content, EmbedBuilder? embed, AllowedMentions? allowedMentions}) async {
|
||||
Future<Message> edit({dynamic content, EmbedBuilder? embed, AllowedMentions? allowedMentions, MessageEditBuilder? builder}) async {
|
||||
if (builder != null) {
|
||||
content = builder._content;
|
||||
embed = builder.embed;
|
||||
allowedMentions = builder.allowedMentions;
|
||||
}
|
||||
|
||||
if (this.author.id != client.self.id) {
|
||||
return Future.error("Cannot edit someones message");
|
||||
return Future.error(ArgumentError("Cannot edit someones message"));
|
||||
}
|
||||
|
||||
final body = <String, dynamic>{
|
||||
if (content != null) "content": content.toString(),
|
||||
if (embed != null) "embed": embed._build(),
|
||||
if (allowedMentions != null) "allowed_mentions": allowedMentions._build()
|
||||
if (allowedMentions != null) "allowed_mentions": allowedMentions._build(),
|
||||
|
||||
};
|
||||
|
||||
final response = await client._http
|
||||
._execute(BasicRequest._new("/channels/${this.channel.id}/messages/${this.id}", method: "PATCH", body: body));
|
||||
._execute(BasicRequest._new("/channels/${this.channelId}/messages/${this.id}", method: "PATCH", body: body));
|
||||
|
||||
if (response is HttpResponseSuccess) {
|
||||
return Message._deserialize(response.jsonBody as Map<String, dynamic>, client);
|
||||
|
@ -222,46 +260,58 @@ abstract class Message extends SnowflakeEntity implements Disposable {
|
|||
/// Add reaction to message.
|
||||
Future<void> createReaction(Emoji emoji) =>
|
||||
client._http._execute(BasicRequest._new(
|
||||
"/channels/${this.channel.id}/messages/${this.id}/reactions/${emoji.encode()}/@me",
|
||||
"/channels/${this.channelId}/messages/${this.id}/reactions/${emoji.encode()}/@me",
|
||||
method: "PUT"));
|
||||
|
||||
/// Deletes reaction of bot. Emoji as ":emoji_name:"
|
||||
Future<void> deleteReaction(Emoji emoji) =>
|
||||
client._http._execute(BasicRequest._new(
|
||||
"/channels/${this.channel.id}/messages/${this.id}/reactions/${emoji.encode()}/@me",
|
||||
"/channels/${this.channelId}/messages/${this.id}/reactions/${emoji.encode()}/@me",
|
||||
method: "DELETE"));
|
||||
|
||||
/// Deletes reaction of given user.
|
||||
Future<void> deleteUserReaction(Emoji emoji, String userId) =>
|
||||
client._http._execute(BasicRequest._new(
|
||||
"/channels/${this.channel.id}/messages/${this.id}/reactions/${emoji.encode()}/$userId",
|
||||
"/channels/${this.channelId}/messages/${this.id}/reactions/${emoji.encode()}/$userId",
|
||||
method: "DELETE"));
|
||||
|
||||
/// Deletes all reactions
|
||||
Future<void> deleteAllReactions() =>
|
||||
client._http
|
||||
._execute(BasicRequest._new("/channels/${this.channel.id}/messages/${this.id}/reactions", method: "DELETE"));
|
||||
._execute(BasicRequest._new("/channels/${this.channelId}/messages/${this.id}/reactions", method: "DELETE"));
|
||||
|
||||
/// Deletes the message.
|
||||
///
|
||||
/// Throws an [Exception] if the HTTP request errored.
|
||||
Future<void> delete({String? auditReason}) =>
|
||||
client._http._execute(
|
||||
BasicRequest._new("/channels/${this.channel.id}/messages/${this.id}", method: "DELETE", auditLog: auditReason));
|
||||
BasicRequest._new("/channels/${this.channelId}/messages/${this.id}", method: "DELETE", auditLog: auditReason));
|
||||
|
||||
/// Pins [Message] in current [Channel]
|
||||
Future<void> pinMessage() =>
|
||||
client._http._execute(BasicRequest._new("/channels/${channel.id}/pins/$id", method: "PUT"));
|
||||
client._http._execute(BasicRequest._new("/channels/${this.channelId}/pins/$id", method: "PUT"));
|
||||
|
||||
/// Unpins [Message] in current [Channel]
|
||||
Future<void> unpinMessage() =>
|
||||
client._http._execute(BasicRequest._new("/channels/${channel.id}/pins/$id", method: "DELETE"));
|
||||
client._http._execute(BasicRequest._new("/channels/${this.channelId}/pins/$id", method: "DELETE"));
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is Message) {
|
||||
return other.content == this.content || other.embeds.any((e) => this.embeds.any((f) => e == f));
|
||||
return this.id == other.id;
|
||||
}
|
||||
|
||||
if(other is Snowflake) {
|
||||
return this.id == other;
|
||||
}
|
||||
|
||||
if(other is int) {
|
||||
return this.id == other;
|
||||
}
|
||||
|
||||
if(other is String) {
|
||||
return this.id == other;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -19,7 +19,9 @@ class Reaction {
|
|||
if (rawEmoji["id"] == null) {
|
||||
this.emoji = UnicodeEmoji(rawEmoji["name"] as String);
|
||||
} else {
|
||||
this.emoji = GuildEmoji._partial(rawEmoji);
|
||||
|
||||
//TODO: EMOJIS STUUF
|
||||
//this.emoji = PartialGuildEmoji._new(rawEmoji);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,17 +2,18 @@ part of nyxx;
|
|||
|
||||
/// Represents unicode emoji. Contains only emoji code.
|
||||
class UnicodeEmoji extends Emoji {
|
||||
UnicodeEmoji(String code) : super(code);
|
||||
/// Create unicode emoji from given code
|
||||
UnicodeEmoji(String name) : super._new(name);
|
||||
|
||||
/// Returns Emoji
|
||||
String get code => this.name;
|
||||
String get code => this.name!;
|
||||
|
||||
/// Returns runes of emoji
|
||||
Runes get runes => this.name.runes;
|
||||
Runes get runes => this.name!.runes;
|
||||
|
||||
/// Encodes Emoji so that can be used in messages.
|
||||
@override
|
||||
String encode() => this.name;
|
||||
String encode() => this.name!;
|
||||
|
||||
/// Returns encoded string ready to send via message.
|
||||
@override
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,35 @@
|
|||
part of nyxx;
|
||||
|
||||
/// A user with [Guild] context.
|
||||
class Member extends User implements GuildEntity {
|
||||
/// Represents [Guild] member. Can be either [CachelessMember] or [CacheMember] depending on client config and cache state.
|
||||
/// Interface allows basic operations on member but does not guarantee data to be valid or available
|
||||
abstract class IMember extends User {
|
||||
IMember._new(Map<String, dynamic> raw, Nyxx client) : super._new(raw, client);
|
||||
|
||||
/// Checks if member has specified role. Returns true if user is assigned to given role.
|
||||
bool hasRole(bool Function(IRole role) func);
|
||||
|
||||
/// Bans the member and optionally deletes [deleteMessageDays] days worth of messages.
|
||||
Future<void> ban({int? deleteMessageDays, String? reason, String? auditReason});
|
||||
|
||||
/// Adds role to user
|
||||
///
|
||||
/// ```
|
||||
/// var r = guild.roles.values.first;
|
||||
/// await member.addRole(r);
|
||||
/// ```
|
||||
Future<void> addRole(IRole role, {String? auditReason});
|
||||
|
||||
/// Removes [role] from user.
|
||||
Future<void> removeRole(IRole role, {String? auditReason});
|
||||
/// Kicks the member from guild
|
||||
Future<void> kick({String? auditReason});
|
||||
|
||||
/// Edits members. Allows to move user in voice channel, mute or deaf, change nick, roles.
|
||||
Future<void> edit({String? nick, List<IRole>? roles, bool? mute, bool? deaf, VoiceChannel? channel, String? auditReason});
|
||||
}
|
||||
|
||||
/// Stateless [IMember] instance. Does not have reference to guild.
|
||||
class CachelessMember extends IMember {
|
||||
/// The members nickname, null if not set.
|
||||
String? nickname;
|
||||
|
||||
|
@ -14,56 +42,27 @@ class Member extends User implements GuildEntity {
|
|||
/// Weather or not the member is muted.
|
||||
late final bool mute;
|
||||
|
||||
/// A list of [Role]s the member has.
|
||||
late List<Role> roles;
|
||||
/// Id of guild
|
||||
final Snowflake guildId;
|
||||
|
||||
/// The highest displayed role that a member has, null if they dont have any
|
||||
Role? hoistedRole;
|
||||
/// Roles of member. It will contain instance of [IRole] if [CachelessMember] or instance of [Role] if instance of [CacheMember]
|
||||
late Iterable<IRole> roles;
|
||||
|
||||
@override
|
||||
/// Highest role of member
|
||||
late IRole? hoistedRole;
|
||||
|
||||
/// The guild that the member is a part of.
|
||||
Guild guild;
|
||||
factory CachelessMember._standard(Map<String, dynamic> data, Snowflake guildId, Nyxx client) =>
|
||||
CachelessMember._new(data, data["user"] as Map<String, dynamic>, guildId, client);
|
||||
|
||||
/// Returns highest role for member
|
||||
Role get highestRole => roles.isEmpty ? guild.everyoneRole : roles.reduce((f, s) => f.position > s.position ? f : s);
|
||||
factory CachelessMember._fromUser(Map<String, dynamic> dataUser, Map<String, dynamic> dataMember, Snowflake guildId, Nyxx client) =>
|
||||
CachelessMember._new(dataMember, dataUser, guildId, client);
|
||||
|
||||
/// Color of highest role of user
|
||||
DiscordColor get color => highestRole.color;
|
||||
|
||||
/// Voice state of member
|
||||
VoiceState? get voiceState => guild.voiceStates[this.id];
|
||||
|
||||
/// Returns total permissions of user.
|
||||
Permissions get effectivePermissions {
|
||||
if (this == guild.owner) {
|
||||
return Permissions.all();
|
||||
}
|
||||
|
||||
var total = guild.everyoneRole.permissions.raw;
|
||||
for (final role in roles) {
|
||||
total |= role.permissions.raw;
|
||||
|
||||
if (PermissionsUtils.isApplied(total, PermissionsConstants.administrator)) {
|
||||
return Permissions.fromInt(PermissionsConstants.allPermissions);
|
||||
}
|
||||
}
|
||||
|
||||
return Permissions.fromInt(total);
|
||||
}
|
||||
|
||||
factory Member._standard(Map<String, dynamic> data, Guild guild, Nyxx client) =>
|
||||
Member._new(data, data["user"] as Map<String, dynamic>, guild, client);
|
||||
|
||||
factory Member._fromUser(Map<String, dynamic> dataUser, Map<String, dynamic> dataMember, Guild guild, Nyxx client) =>
|
||||
Member._new(dataMember, dataUser, guild, client);
|
||||
|
||||
Member._new(Map<String, dynamic> raw, Map<String, dynamic> user, this.guild, Nyxx client) : super._new(user, client) {
|
||||
CachelessMember._new(Map<String, dynamic> raw, Map<String, dynamic> userRaw, this.guildId, Nyxx client) : super._new(userRaw, client) {
|
||||
this.nickname = raw["nick"] as String?;
|
||||
this.deaf = raw["deaf"] as bool;
|
||||
this.mute = raw["mute"] as bool;
|
||||
|
||||
this.roles = [for (var id in raw["roles"]) guild.roles[Snowflake(id)] as Role];
|
||||
this.roles = [for (var id in raw["roles"]) IRole._new(Snowflake(id), this.guildId, client)];
|
||||
|
||||
if (raw["hoisted_role"] != null && roles.isNotEmpty) {
|
||||
// TODO: NNBD: try-catch in where
|
||||
|
@ -83,7 +82,7 @@ class Member extends User implements GuildEntity {
|
|||
}
|
||||
}
|
||||
|
||||
bool _updateMember(String? nickname, List<Role> roles) {
|
||||
bool _updateMember(String? nickname, List<IRole> roles) {
|
||||
var changed = false;
|
||||
|
||||
if (this.nickname != nickname) {
|
||||
|
@ -100,44 +99,50 @@ class Member extends User implements GuildEntity {
|
|||
}
|
||||
|
||||
/// Checks if member has specified role. Returns true if user is assigned to given role.
|
||||
bool hasRole(bool Function(Role role) func) => this.roles.any(func);
|
||||
@override
|
||||
bool hasRole(bool Function(IRole role) func) => this.roles.any(func);
|
||||
|
||||
/// Bans the member and optionally deletes [deleteMessageDays] days worth of messages.
|
||||
@override
|
||||
Future<void> ban({int? deleteMessageDays, String? reason, String? auditReason}) async {
|
||||
final body = <String, dynamic>{
|
||||
if (deleteMessageDays != null) "delete-message-days": deleteMessageDays,
|
||||
if (reason != null) "reason": reason
|
||||
};
|
||||
|
||||
return client._http._execute(BasicRequest._new("/guilds/${this.guild.id}/bans/${this.id}",
|
||||
await client._http._execute(BasicRequest._new("/guilds/${this.guildId}/bans/${this.id}",
|
||||
method: "PUT", auditLog: auditReason, body: body));
|
||||
}
|
||||
|
||||
/// Adds role to user
|
||||
/// Adds role to user.
|
||||
///
|
||||
/// ```
|
||||
/// var r = guild.roles.values.first;
|
||||
/// await member.addRole(r);
|
||||
/// ```
|
||||
Future<void> addRole(Role role, {String? auditReason}) =>
|
||||
client._http._execute(BasicRequest._new("/guilds/${guild.id}/members/${this.id}/roles/${role.id}",
|
||||
method: "PUT", auditLog: auditReason));
|
||||
@override
|
||||
Future<void> addRole(IRole role, {String? auditReason}) =>
|
||||
client._http._execute(BasicRequest._new("/guilds/$guildId/members/${this.id}/roles/${role.id}",
|
||||
method: "PUT", auditLog: auditReason));
|
||||
|
||||
/// Removes [role] from user.
|
||||
Future<void> removeRole(Role role, {String? auditReason}) =>
|
||||
client._http._execute(BasicRequest._new(
|
||||
"/guilds/${this.guild.id.toString()}/members/${this.id.toString()}/roles/${role.id.toString()}",
|
||||
method: "DELETE",
|
||||
auditLog: auditReason));
|
||||
@override
|
||||
Future<void> removeRole(IRole role, {String? auditReason}) =>
|
||||
client._http._execute(BasicRequest._new(
|
||||
"/guilds/${this.guildId.toString()}/members/${this.id.toString()}/roles/${role.id.toString()}",
|
||||
method: "DELETE",
|
||||
auditLog: auditReason));
|
||||
|
||||
/// Kicks the member from guild
|
||||
@override
|
||||
Future<void> kick({String? auditReason}) =>
|
||||
client._http._execute(
|
||||
BasicRequest._new("/guilds/${this.guild.id}/members/${this.id}", method: "DELETE", auditLog: auditReason));
|
||||
client._http._execute(
|
||||
BasicRequest._new("/guilds/${this.guildId}/members/${this.id}", method: "DELETE", auditLog: auditReason));
|
||||
|
||||
/// Edits members. Allows to move user in voice channel, mute or deaf, change nick, roles.
|
||||
@override
|
||||
Future<void> edit(
|
||||
{String? nick, List<Role>? roles, bool? mute, bool? deaf, VoiceChannel? channel, String? auditReason}) {
|
||||
{String? nick, List<IRole>? roles, bool? mute, bool? deaf, VoiceChannel? channel, String? auditReason}) {
|
||||
final body = <String, dynamic>{
|
||||
if (nick != null) "nick": nick,
|
||||
if (roles != null) "roles": roles.map((f) => f.id.toString()).toList(),
|
||||
|
@ -146,10 +151,66 @@ class Member extends User implements GuildEntity {
|
|||
if (channel != null) "channel_id": channel.id.toString()
|
||||
};
|
||||
|
||||
return client._http._execute(BasicRequest._new("/guilds/${this.guild.id.toString()}/members/${this.id.toString()}",
|
||||
return client._http._execute(BasicRequest._new("/guilds/${this.guildId.toString()}/members/${this.id.toString()}",
|
||||
method: "PATCH", auditLog: auditReason, body: body));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => super.toString();
|
||||
}
|
||||
|
||||
/// A user with [Guild] context.
|
||||
class CacheMember extends CachelessMember implements GuildEntity {
|
||||
/// The guild that the member is a part of.
|
||||
@override
|
||||
Guild guild;
|
||||
|
||||
/// Returns highest role for member
|
||||
IRole get highestRole => this.roles.isEmpty ? guild.everyoneRole : roles.reduce((f, s) => (f as Role).position > (s as Role).position ? f : s);
|
||||
|
||||
/// Color of highest role of user
|
||||
DiscordColor get color => (highestRole as Role).color;
|
||||
|
||||
/// Voice state of member
|
||||
VoiceState? get voiceState => guild.voiceStates[this.id];
|
||||
|
||||
/// Returns total permissions of user.
|
||||
Permissions get effectivePermissions {
|
||||
if (this == guild.owner) {
|
||||
return Permissions.all();
|
||||
}
|
||||
|
||||
var total = (guild.everyoneRole as Role).permissions.raw;
|
||||
for (final role in roles) {
|
||||
if(role is! Role) {
|
||||
continue;
|
||||
}
|
||||
|
||||
total |= role.permissions.raw;
|
||||
|
||||
if (PermissionsUtils.isApplied(total, PermissionsConstants.administrator)) {
|
||||
return Permissions.fromInt(PermissionsConstants.allPermissions);
|
||||
}
|
||||
}
|
||||
|
||||
return Permissions.fromInt(total);
|
||||
}
|
||||
|
||||
// TODO: Remove duplicate
|
||||
@override
|
||||
bool _updateMember(String? nickname, List<IRole> roles) => super._updateMember(nickname, roles);
|
||||
|
||||
factory CacheMember._standard(Map<String, dynamic> data, Guild guild, Nyxx client) =>
|
||||
CacheMember._new(data, data["user"] as Map<String, dynamic>, guild, client);
|
||||
|
||||
factory CacheMember._fromUser(Map<String, dynamic> dataUser, Map<String, dynamic> dataMember, Guild guild, Nyxx client) =>
|
||||
CacheMember._new(dataMember, dataUser, guild, client);
|
||||
|
||||
CacheMember._new(Map<String, dynamic> raw, Map<String, dynamic> user, this.guild, Nyxx client) : super._new(raw, user, guild.id, client) {
|
||||
this.roles = [
|
||||
for(final role in this.roles)
|
||||
// TODO: NNBD
|
||||
this.guild.roles[role.id]!
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ class VoiceState {
|
|||
Guild? guild;
|
||||
|
||||
/// Channel id user is connected
|
||||
VoiceChannel? channel;
|
||||
CacheVoiceChannel? channel;
|
||||
|
||||
/// Whether this user is muted by the server
|
||||
late final bool deaf;
|
||||
|
@ -29,7 +29,7 @@ class VoiceState {
|
|||
|
||||
VoiceState._new(Map<String, dynamic> raw, Nyxx client, [Guild? guild]) {
|
||||
if (raw["channel_id"] != null) {
|
||||
this.channel = client.channels[Snowflake(raw["channel_id"])] as VoiceChannel?;
|
||||
this.channel = client.channels[Snowflake(raw["channel_id"])] as CacheVoiceChannel?;
|
||||
}
|
||||
|
||||
this.deaf = raw["deaf"] as bool;
|
||||
|
|
|
@ -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!";
|
||||
}
|
|
@ -3,15 +3,15 @@ part of nyxx;
|
|||
// TODO: Decide what about guild store channels
|
||||
/// Sent when a channel is created.
|
||||
class ChannelCreateEvent {
|
||||
/// The channel that was created, either a [GuildChannel], [DMChannel], or [GroupDMChannel].
|
||||
/// The channel that was created, either a [CacheGuildChannel], [DMChannel], or [GroupDMChannel].
|
||||
late final Channel channel;
|
||||
|
||||
ChannelCreateEvent._new(Map<String, dynamic> raw, Nyxx client) {
|
||||
this.channel = Channel._deserialize(raw, client);
|
||||
this.channel = Channel._deserialize(raw["d"] as Map<String, dynamic>, client);
|
||||
|
||||
client.channels[channel.id] = channel;
|
||||
if (this.channel is GuildChannel) {
|
||||
(this.channel as GuildChannel).guild.channels[channel.id] = channel;
|
||||
if (this.channel is CacheGuildChannel) {
|
||||
(this.channel as CacheGuildChannel).guild.channels[channel.id] = channel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,8 @@ class ChannelDeleteEvent {
|
|||
}
|
||||
|
||||
client.channels.remove(channelSnowflake);
|
||||
if (this.channel is GuildChannel) {
|
||||
(this.channel as GuildChannel).guild.channels.remove(channelSnowflake);
|
||||
if (this.channel is CacheGuildChannel) {
|
||||
(this.channel as CacheGuildChannel).guild.channels.remove(channelSnowflake);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class ChannelDeleteEvent {
|
|||
/// Fired when channel"s pinned messages are updated
|
||||
class ChannelPinsUpdateEvent {
|
||||
/// Channel where pins were updated
|
||||
TextChannel? channel;
|
||||
CacheTextChannel? channel;
|
||||
|
||||
/// ID of channel pins were updated
|
||||
late final Snowflake channelId;
|
||||
|
@ -57,7 +57,7 @@ class ChannelPinsUpdateEvent {
|
|||
this.lastPingTimestamp = DateTime.parse(raw["d"]["last_pin_timestamp"] as String);
|
||||
}
|
||||
|
||||
this.channel = client.channels[Snowflake(raw["d"]["channel_id"])] as TextChannel;
|
||||
this.channel = client.channels[Snowflake(raw["d"]["channel_id"])] as CacheTextChannel?;
|
||||
|
||||
if (raw["d"]["guild_id"] != null) {
|
||||
this.guildId = Snowflake(raw["d"]["guild_id"]);
|
||||
|
@ -71,12 +71,12 @@ class ChannelUpdateEvent {
|
|||
late final Channel updatedChannel;
|
||||
|
||||
ChannelUpdateEvent._new(Map<String, dynamic> raw, Nyxx client) {
|
||||
this.updatedChannel = Channel._deserialize(raw, client);
|
||||
this.updatedChannel = Channel._deserialize(raw["d"] as Map<String, dynamic>, client);
|
||||
|
||||
client.channels[this.updatedChannel.id] = updatedChannel;
|
||||
|
||||
if (this.updatedChannel is GuildChannel) {
|
||||
(this.updatedChannel as GuildChannel).guild.channels[this.updatedChannel.id] = updatedChannel;
|
||||
if (this.updatedChannel is CacheGuildChannel) {
|
||||
(this.updatedChannel as CacheGuildChannel).guild.channels[this.updatedChannel.id] = updatedChannel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 Member? member;
|
||||
late final IMember? member;
|
||||
|
||||
/// User if user is updated. Will be null if member is not null.
|
||||
late final User? user;
|
||||
|
@ -101,16 +97,16 @@ class GuildMemberUpdateEvent {
|
|||
return;
|
||||
}
|
||||
|
||||
final member = guild.members[Snowflake(raw["d"]["user"]["id"])];
|
||||
this.member = guild.members[Snowflake(raw["d"]["user"]["id"])];
|
||||
|
||||
if (member == null) {
|
||||
if (this.member == null || this.member is! CacheMember) {
|
||||
return;
|
||||
}
|
||||
|
||||
final nickname = raw["d"]["nickname"] as String?;
|
||||
final roles = (raw["d"]["roles"] as List<dynamic>).map((str) => guild.roles[Snowflake(str)]!).toList();
|
||||
|
||||
if (member._updateMember(nickname, roles)) {
|
||||
if ((this.member as CacheMember)._updateMember(nickname, roles)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -122,7 +118,7 @@ class GuildMemberUpdateEvent {
|
|||
/// Sent when a member joins a guild.
|
||||
class GuildMemberAddEvent {
|
||||
/// The member that joined.
|
||||
late final Member? member;
|
||||
late final CacheMember? member;
|
||||
|
||||
GuildMemberAddEvent._new(Map<String, dynamic> raw, Nyxx client) {
|
||||
final guild = client.guilds[Snowflake(raw["d"]["guild_id"])];
|
||||
|
@ -131,7 +127,7 @@ class GuildMemberAddEvent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.member = Member._standard(raw["d"] as Map<String, dynamic>, guild, client);
|
||||
this.member = CacheMember._standard(raw["d"] as Map<String, dynamic>, guild, client);
|
||||
|
||||
guild.members[member!.id] = member!;
|
||||
if (!client.users.hasKey(member!.id)) {
|
||||
|
@ -205,20 +201,26 @@ class GuildBanRemoveEvent {
|
|||
/// Fired when emojis are updated
|
||||
class GuildEmojisUpdateEvent {
|
||||
/// New list of changes emojis
|
||||
late final Map<Snowflake, GuildEmoji> emojis;
|
||||
final Map<Snowflake, GuildEmoji> emojis = {};
|
||||
|
||||
/// Id of guild where event happend
|
||||
late final Snowflake guildId;
|
||||
|
||||
/// Instance of guild if available
|
||||
late final Guild? guild;
|
||||
|
||||
GuildEmojisUpdateEvent._new(Map<String, dynamic> json, Nyxx client) {
|
||||
if (client.ready) {
|
||||
final guild = client.guilds[Snowflake(json["d"]["guild_id"])];
|
||||
this.guildId = Snowflake(json["d"]["guild_id"]);
|
||||
this.guild = client.guilds[this.guildId];
|
||||
|
||||
for(final rawEmoji in json["d"]["emojis"]) {
|
||||
final emoji = GuildEmoji._new(rawEmoji as Map<String, dynamic>, guildId, client);
|
||||
|
||||
this.emojis[emoji.id] = emoji;
|
||||
|
||||
emojis = {};
|
||||
if (guild != null) {
|
||||
json["d"]["emojis"].forEach((o) {
|
||||
final emoji = GuildEmoji._new(o as Map<String, dynamic>, guild, client);
|
||||
|
||||
guild.emojis[emoji.id] = emoji;
|
||||
emojis[emoji.id] = emoji;
|
||||
});
|
||||
guild!.emojis[emoji.id] = emoji;
|
||||
emojis[emoji.id] = emoji;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -227,56 +229,69 @@ class GuildEmojisUpdateEvent {
|
|||
/// Sent when a role is created.
|
||||
class RoleCreateEvent {
|
||||
/// The role that was created.
|
||||
Role? role;
|
||||
late final Role role;
|
||||
|
||||
/// Id of guild where event happend
|
||||
late final Snowflake guildId;
|
||||
|
||||
/// Instance of [Guild] if available
|
||||
late final Guild? guild;
|
||||
|
||||
RoleCreateEvent._new(Map<String, dynamic> json, Nyxx client) {
|
||||
if (client.ready) {
|
||||
final guild = client.guilds[Snowflake(json["d"]["guild_id"])];
|
||||
this.guildId = Snowflake(json["d"]["guild_id"]);
|
||||
this.guild = client.guilds[this.guildId];
|
||||
|
||||
if (guild != null) {
|
||||
this.role = Role._new(json["d"]["role"] as Map<String, dynamic>, guild, client);
|
||||
|
||||
guild.roles[role!.id] = role!;
|
||||
}
|
||||
this.role = Role._new(json["d"]["role"] as Map<String, dynamic>, guildId, client);
|
||||
if (guild != null) {
|
||||
guild!.roles[role.id] = role;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent when a role is deleted.
|
||||
class RoleDeleteEvent {
|
||||
/// The role that was deleted.
|
||||
Role? role;
|
||||
/// The role that was deleted, if available
|
||||
IRole? role;
|
||||
|
||||
/// Id of tole that was deleted
|
||||
late final Snowflake roleId;
|
||||
|
||||
/// Id of guild where event happend
|
||||
late final Snowflake guildId;
|
||||
|
||||
/// Instance of [Guild] if available
|
||||
late final Guild? guild;
|
||||
|
||||
RoleDeleteEvent._new(Map<String, dynamic> json, Nyxx client) {
|
||||
final guild = client.guilds[Snowflake(json["d"]["guild_id"])];
|
||||
this.guildId = Snowflake(json["d"]["guild_id"]);
|
||||
this.guild = client.guilds[this.guildId];
|
||||
|
||||
this.roleId = Snowflake(json["d"]["role_id"]);
|
||||
if (guild != null) {
|
||||
this.role = guild.roles[Snowflake(json["d"]["role_id"])];
|
||||
guild.roles.remove(role!.id);
|
||||
this.role = guild!.roles[this.roleId];
|
||||
guild!.roles.remove(role!.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sent when a role is updated.
|
||||
class RoleUpdateEvent {
|
||||
/// The role prior to the update.
|
||||
Role? oldRole;
|
||||
|
||||
/// The role after the update.
|
||||
Role? newRole;
|
||||
late final Role role;
|
||||
/// Id of guild where event happend
|
||||
late final Snowflake guildId;
|
||||
|
||||
/// Instance of [Guild] if available
|
||||
late final Guild? guild;
|
||||
|
||||
RoleUpdateEvent._new(Map<String, dynamic> json, Nyxx client) {
|
||||
final guild = client.guilds[Snowflake(json["d"]["guild_id"])];
|
||||
this.guildId = Snowflake(json["d"]["guild_id"]);
|
||||
this.guild = client.guilds[this.guildId];
|
||||
|
||||
this.role = Role._new(json["d"]["role"] as Map<String, dynamic>, guildId, client);
|
||||
|
||||
if (guild != null) {
|
||||
this.oldRole = guild.roles[Snowflake(json["d"]["role"]["id"])];
|
||||
this.newRole = Role._new(json["d"]["role"] as Map<String, dynamic>, guild, client);
|
||||
|
||||
if (oldRole != null) {
|
||||
oldRole!.guild.roles[oldRole!.id] = newRole!;
|
||||
} else {
|
||||
guild.roles.add(newRole!.id, newRole!);
|
||||
}
|
||||
this.guild!.roles[role.id] = role;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ part of nyxx;
|
|||
/// You can use the `chunk_index` and `chunk_count` to calculate how many chunks are left for your request.
|
||||
class MemberChunkEvent {
|
||||
/// Guild members
|
||||
late final Iterable<Member> members;
|
||||
late final Iterable<IMember> members;
|
||||
|
||||
/// Id of guild
|
||||
late final Snowflake guildId;
|
||||
|
@ -27,7 +27,10 @@ class MemberChunkEvent {
|
|||
/// Nonce is used to identify events.
|
||||
String? nonce;
|
||||
|
||||
MemberChunkEvent._new(Map<String, dynamic> raw, Nyxx client) {
|
||||
/// Id of shard where chunk was received
|
||||
final int shardId;
|
||||
|
||||
MemberChunkEvent._new(Map<String, dynamic> raw, Nyxx client, this.shardId) {
|
||||
this.chunkIndex = raw["d"]["chunk_index"] as int;
|
||||
this.chunkCount = raw["d"]["chunk_count"] as int;
|
||||
|
||||
|
@ -44,7 +47,7 @@ class MemberChunkEvent {
|
|||
|
||||
this.members = [
|
||||
for (var memberRaw in raw["d"]["members"])
|
||||
Member._standard(memberRaw as Map<String, dynamic>, this.guild!, client)
|
||||
CacheMember._standard(memberRaw as Map<String, dynamic>, this.guild!, client)
|
||||
];
|
||||
|
||||
// TODO: Thats probably redundant
|
||||
|
|
|
@ -104,7 +104,7 @@ abstract class MessageReactionEvent {
|
|||
if (json["d"]["emoji"]["id"] == null) {
|
||||
this.emoji = UnicodeEmoji(json["d"]["emoji"]["name"] as String);
|
||||
} else {
|
||||
this.emoji = GuildEmoji._partial(json["d"]["emoji"] as Map<String, dynamic>);
|
||||
this.emoji = PartialGuildEmoji._new(json["d"]["emoji"] as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -225,7 +225,8 @@ class MessageReactionRemoveEmojiEvent {
|
|||
if (json["d"]["emoji"]["id"] == null) {
|
||||
this.emoji = UnicodeEmoji(json["d"]["emoji"]["name"] as String);
|
||||
} else {
|
||||
this.emoji = GuildEmoji._partial(json["d"]["emoji"] as Map<String, dynamic>);
|
||||
// TODO: emojis stuff
|
||||
//this.emoji = GuildEmoji._partial(json["d"]["emoji"] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
if (this.message != null) {
|
||||
|
|
|
@ -3,7 +3,7 @@ part of nyxx;
|
|||
/// Sent when a user starts typing.
|
||||
class TypingEvent {
|
||||
/// The channel that the user is typing in.
|
||||
MessageChannel? channel;
|
||||
ITextChannel? channel;
|
||||
|
||||
/// Id of the channel that the user is typing in.
|
||||
late final Snowflake channelId;
|
||||
|
@ -19,7 +19,7 @@ class TypingEvent {
|
|||
|
||||
TypingEvent._new(Map<String, dynamic> json, Nyxx client) {
|
||||
this.channelId = Snowflake(json["d"]["channel_id"]);
|
||||
this.channel = client.channels[channelId] as MessageChannel?;
|
||||
this.channel = client.channels[channelId] as ITextChannel?;
|
||||
|
||||
this.userId = Snowflake(json["d"]["user_id"]);
|
||||
this.user = client.users[this.userId];
|
||||
|
|
|
@ -2,7 +2,6 @@ part of nyxx;
|
|||
|
||||
/// Emitted when guild's voice server changes
|
||||
class VoiceServerUpdateEvent {
|
||||
|
||||
/// Raw websocket event payload
|
||||
final Map<String, dynamic> raw;
|
||||
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
part of nyxx;
|
||||
|
||||
/// Thrown when cannot convert provided data to [Snowflake]
|
||||
class InvalidSnowflakeException implements Exception {
|
||||
final dynamic _invalidSnowflake;
|
||||
|
||||
InvalidSnowflakeException._new(this._invalidSnowflake);
|
||||
|
||||
@override
|
||||
String toString() => "InvalidSnowflakeException: Cannot parse [$_invalidSnowflake] to Snowflake";
|
||||
}
|
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();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
part of nyxx;
|
||||
|
||||
//TODO: Consider what should be here and what should not
|
||||
/// Could be either [User], [Member] or [Webhook].
|
||||
/// Could be either [User], [CacheMember] or [Webhook].
|
||||
/// [Webhook] will have most of field missing.
|
||||
abstract class IMessageAuthor implements SnowflakeEntity {
|
||||
/// User name
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
101
nyxx/lib/src/internal/shard/shardHandler.dart
Normal file
101
nyxx/lib/src/internal/shard/shardHandler.dart
Normal file
|
@ -0,0 +1,101 @@
|
|||
part of nyxx;
|
||||
|
||||
// Decodes zlib compresses string into string json
|
||||
Map<String, dynamic> _decodeBytes(dynamic rawPayload, ZLibDecoder decoder) {
|
||||
if (rawPayload is String) {
|
||||
return jsonDecode(rawPayload) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
// print("Size: ${(rawPayload as List<int>).length} bytes");
|
||||
|
||||
final decoded = decoder.convert(rawPayload as List<int>);
|
||||
final rawStr = utf8.decode(decoded);
|
||||
return jsonDecode(rawStr) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/*
|
||||
Protocol used to communicate with shard isolate.
|
||||
First message delivered to shardHandler will be init message with gateway uri
|
||||
|
||||
* DATA - sent along with data received from websocket
|
||||
* DISCONNECTED - sent when shard disconnects
|
||||
* ERROR - sent when error occurs
|
||||
|
||||
* INIT - inits ws connection
|
||||
* CONNECT - sent when ws connection is established. additional data can contain if reconnected.
|
||||
* SEND - sent along with data to send via websocket
|
||||
*/
|
||||
Future<void> _shardHandler(SendPort shardPort) async {
|
||||
/// Port init
|
||||
final receivePort = ReceivePort();
|
||||
final receiveStream = receivePort.asBroadcastStream();
|
||||
|
||||
final sendPort = receivePort.sendPort;
|
||||
shardPort.send(sendPort);
|
||||
|
||||
/// Initial data init
|
||||
final initData = await receiveStream.first;
|
||||
final gatewayUri = Constants.gatewayUri(initData["gatewayUrl"] as String);
|
||||
|
||||
transport.WebSocket? _socket;
|
||||
StreamSubscription? _socketSubscription;
|
||||
|
||||
transport_vm.configureWTransportForVM();
|
||||
|
||||
Future<void> terminate() async {
|
||||
await _socketSubscription?.cancel();
|
||||
await _socket?.close(1000);
|
||||
shardPort.send({ "cmd" : "TERMINATE_OK" });
|
||||
}
|
||||
|
||||
if(!Platform.isWindows) {
|
||||
// ignore: unawaited_futures
|
||||
ProcessSignal.sigterm.watch().forEach((event) async {
|
||||
await terminate();
|
||||
});
|
||||
}
|
||||
|
||||
// ignore: unawaited_futures
|
||||
ProcessSignal.sigint.watch().forEach((event) async {
|
||||
await terminate();
|
||||
});
|
||||
|
||||
// Attempts to connect to ws
|
||||
Future<void> _connect() async {
|
||||
await transport.WebSocket.connect(gatewayUri).then((ws) {
|
||||
final zlibDecoder = ZLibDecoder(); // Create zlib decoder specific to this connection. New connection should get new zlib context
|
||||
|
||||
_socket = ws;
|
||||
_socketSubscription = _socket!.listen((data) {
|
||||
shardPort.send({ "cmd" : "DATA", "jsonData" : _decodeBytes(data, zlibDecoder) });
|
||||
}, onDone: () async {
|
||||
shardPort.send({ "cmd" : "DISCONNECTED", "errorCode" : _socket!.closeCode, "errorReason" : _socket!.closeReason });
|
||||
}, cancelOnError: true, onError: (err) => shardPort.send({ "cmd" : "ERROR", "error": err.toString(), "errorCode" : _socket!.closeCode, "errorReason" : _socket!.closeReason }));
|
||||
|
||||
shardPort.send({ "cmd" : "CONNECT_ACK" });
|
||||
}, onError: (err, __) => shardPort.send({ "cmd" : "ERROR", "error": err.toString(), "errorCode" : _socket!.closeCode, "errorReason" : _socket!.closeReason }));
|
||||
}
|
||||
|
||||
// Connects
|
||||
await _connect();
|
||||
|
||||
await for(final message in receiveStream) {
|
||||
final cmd = message["cmd"];
|
||||
|
||||
if(cmd == "SEND") {
|
||||
if(_socket?.closeCode == null) {
|
||||
_socket?.add(jsonEncode(message["data"]));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if(cmd == "CONNECT") {
|
||||
await _socketSubscription?.cancel();
|
||||
await _socket?.close(1000);
|
||||
await _connect();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,11 +33,11 @@ class EmbedBuilder implements Builder {
|
|||
EmbedAuthorBuilder? author;
|
||||
|
||||
/// Embed custom fields;
|
||||
late final List<EmbedFieldBuilder> _fields;
|
||||
late final List<EmbedFieldBuilder> fields;
|
||||
|
||||
/// Creates clean instance [EmbedBuilder]
|
||||
EmbedBuilder() {
|
||||
_fields = [];
|
||||
fields = [];
|
||||
}
|
||||
|
||||
/// Adds author to embed.
|
||||
|
@ -60,46 +60,57 @@ class EmbedBuilder implements Builder {
|
|||
Function(EmbedFieldBuilder field)? builder,
|
||||
EmbedFieldBuilder? field}) {
|
||||
if (field != null) {
|
||||
_fields.add(field);
|
||||
fields.add(field);
|
||||
return;
|
||||
}
|
||||
|
||||
if (builder != null) {
|
||||
final tmp = EmbedFieldBuilder();
|
||||
builder(tmp);
|
||||
_fields.add(tmp);
|
||||
fields.add(tmp);
|
||||
return;
|
||||
}
|
||||
|
||||
_fields.add(EmbedFieldBuilder(name, content, inline));
|
||||
fields.add(EmbedFieldBuilder(name, content, inline));
|
||||
}
|
||||
|
||||
/// Total lenght of all text fields of embed
|
||||
/// Replaces field where [name] witch provided new field.
|
||||
void replaceField({dynamic? name,
|
||||
dynamic? content,
|
||||
bool inline = false,
|
||||
Function(EmbedFieldBuilder field)? builder,
|
||||
EmbedFieldBuilder? field}) {
|
||||
|
||||
this.fields.removeWhere((element) => element.name == name);
|
||||
this.addField(name: name, content: content, inline: inline, builder: builder, field: field);
|
||||
}
|
||||
|
||||
/// Total length of all text fields of embed
|
||||
int get length =>
|
||||
(this.title?.length ?? 0) +
|
||||
(this.description?.length ?? 0) +
|
||||
(this.footer?.length ?? 0) +
|
||||
(this.author?.length ?? 0) +
|
||||
(_fields.isEmpty ? 0 : _fields.map((embed) => embed.length).reduce((f, s) => f + s));
|
||||
(fields.isEmpty ? 0 : fields.map((embed) => embed.length).reduce((f, s) => f + s));
|
||||
|
||||
@override
|
||||
|
||||
/// Builds object to Map() instance;
|
||||
Map<String, dynamic> _build() {
|
||||
if (this.title != null && this.title!.length > 256) {
|
||||
throw Exception("Embed title is too long (256 characters limit)");
|
||||
throw EmbedBuilderArgumentException._new("Embed title is too long (256 characters limit)");
|
||||
}
|
||||
|
||||
if (this.description != null && this.description!.length > 2048) {
|
||||
throw Exception("Embed description is too long (2048 characters limit)");
|
||||
throw EmbedBuilderArgumentException._new("Embed description is too long (2048 characters limit)");
|
||||
}
|
||||
|
||||
if (this._fields.length > 25) {
|
||||
throw Exception("Embed cannot contain more than 25 fields");
|
||||
if (this.fields.length > 25) {
|
||||
throw EmbedBuilderArgumentException._new("Embed cannot contain more than 25 fields");
|
||||
}
|
||||
|
||||
if (this.length > 6000) {
|
||||
throw Exception("Total length of embed cannot exceed 6000 characters");
|
||||
throw EmbedBuilderArgumentException._new("Total length of embed cannot exceed 6000 characters");
|
||||
}
|
||||
|
||||
return <String, dynamic>{
|
||||
|
@ -113,7 +124,7 @@ class EmbedBuilder implements Builder {
|
|||
if (imageUrl != null) "image": <String, dynamic>{"url": imageUrl},
|
||||
if (thumbnailUrl != null) "thumbnail": <String, dynamic>{"url": thumbnailUrl},
|
||||
if (author != null) "author": author!._build(),
|
||||
if (_fields.isNotEmpty) "fields": _fields.map((builder) => builder._build()).toList()
|
||||
if (fields.isNotEmpty) "fields": fields.map((builder) => builder._build()).toList()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
part of nyxx;
|
||||
|
||||
/// Allows to create pre built custom messages which can be passed to classes which inherits from [ISend].
|
||||
class MessageBuilder {
|
||||
final _content = StringBuffer();
|
||||
/// Message builder for editing messages.
|
||||
class MessageEditBuilder {
|
||||
/// Clear character which can be used to skip first line in message body or sanitize message content
|
||||
static const clearCharacter = "";
|
||||
|
||||
/// Embed to include in message
|
||||
EmbedBuilder? embed;
|
||||
|
||||
/// Set to true if message should be TTS
|
||||
bool? tts;
|
||||
|
||||
/// List of files to send with message
|
||||
List<AttachmentBuilder>? files;
|
||||
|
||||
/// [AllowedMentions] object to control mentions in message
|
||||
AllowedMentions? allowedMentions;
|
||||
|
||||
final _content = StringBuffer();
|
||||
|
||||
/// Clears current content of message and sets new
|
||||
set content(Object content) {
|
||||
_content.clear();
|
||||
|
@ -31,27 +28,8 @@ class MessageBuilder {
|
|||
builder(embed!);
|
||||
}
|
||||
|
||||
/// Add attachment
|
||||
void addAttachment(AttachmentBuilder attachment) {
|
||||
if (this.files == null) this.files = [];
|
||||
|
||||
this.files!.add(attachment);
|
||||
}
|
||||
|
||||
/// Add attachment from specified file
|
||||
void addFileAttachment(File file, {String? name, bool spoiler = false}) {
|
||||
addAttachment(AttachmentBuilder.file(file, name: name, spoiler: spoiler));
|
||||
}
|
||||
|
||||
/// Add attachment from specified bytes
|
||||
void addBytesAttachment(List<int> bytes, String name, {bool spoiler = false}) {
|
||||
addAttachment(AttachmentBuilder.bytes(bytes, name, spoiler: spoiler));
|
||||
}
|
||||
|
||||
/// Add attachment at specified path
|
||||
void addPathAttachment(String path, {String? name, bool spoiler = false}) {
|
||||
addAttachment(AttachmentBuilder.path(path, name: name, spoiler: spoiler));
|
||||
}
|
||||
/// Appends clear character. Can be used to skip first line in message body.
|
||||
void appendClearCharacter() => _content.writeln(clearCharacter);
|
||||
|
||||
/// Appends empty line to message
|
||||
void appendNewLine() => _content.writeln();
|
||||
|
@ -81,6 +59,37 @@ class MessageBuilder {
|
|||
void appendWithDecoration(Object text, MessageDecoration decoration) {
|
||||
_content.write("$decoration$text$decoration");
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows to create pre built custom messages which can be passed to classes which inherits from [ISend].
|
||||
class MessageBuilder extends MessageEditBuilder {
|
||||
/// Set to true if message should be TTS
|
||||
bool? tts;
|
||||
|
||||
/// List of files to send with message
|
||||
List<AttachmentBuilder>? files;
|
||||
|
||||
/// Add attachment
|
||||
void addAttachment(AttachmentBuilder attachment) {
|
||||
if (this.files == null) this.files = [];
|
||||
|
||||
this.files!.add(attachment);
|
||||
}
|
||||
|
||||
/// Add attachment from specified file
|
||||
void addFileAttachment(File file, {String? name, bool spoiler = false}) {
|
||||
addAttachment(AttachmentBuilder.file(file, name: name, spoiler: spoiler));
|
||||
}
|
||||
|
||||
/// Add attachment from specified bytes
|
||||
void addBytesAttachment(List<int> bytes, String name, {bool spoiler = false}) {
|
||||
addAttachment(AttachmentBuilder.bytes(bytes, name, spoiler: spoiler));
|
||||
}
|
||||
|
||||
/// Add attachment at specified path
|
||||
void addPathAttachment(String path, {String? name, bool spoiler = false}) {
|
||||
addAttachment(AttachmentBuilder.path(path, name: name, spoiler: spoiler));
|
||||
}
|
||||
|
||||
/// Sends message
|
||||
Future<Message?> send(ISend entity) => entity.send(builder: this);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue