@stoatx/client 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # @stoatx/client
2
2
 
3
- A high-performance, fully-typed, and memory-efficient client library for the Revolt API. Built from the ground up for the Stoatx ecosystem.
3
+ A high-performance, fully-typed, and memory-efficient client library for the Stoat API. Built from the ground up for the Stoatx ecosystem.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@stoatx/client.svg?style=flat-square)](https://www.npmjs.com/package/@stoatx/client)
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-Strict-blue.svg?style=flat-square)](https://www.typescriptlang.org/)
7
7
 
8
- `@stoatx/client` provides a robust object-oriented wrapper around Revolt's REST and WebSocket APIs. It features an intelligent caching system, automatic memory sweeping, and strict event typings to make building bots as frictionless as possible.
8
+ `@stoatx/client` provides a robust object-oriented wrapper around Stoat's REST and WebSocket APIs. It features an intelligent caching system, automatic memory sweeping, and strict event typings to make building bots as frictionless as possible.
9
9
 
10
10
  ## Features
11
11
 
@@ -102,7 +102,7 @@ if (channel && channel.isText()) {
102
102
  ## Building Command Handlers
103
103
 
104
104
  While you can use `@stoatx/client` directly, it truly shines when paired with a command handler. Because the events are strictly typed using generics, you can easily build type-safe event registries.
105
- We recommend using the Stoatx Handler for a seamless experience, but you can also create your own custom command handler leveraging the client's event system.
105
+ We recommend using the `Stoatx` for a seamless experience, but you can also create your own custom command handler leveraging the client's event system.
106
106
 
107
107
  ```typescript
108
108
  // Example of event listening
package/dist/index.cjs CHANGED
@@ -53,6 +53,7 @@ __export(index_exports, {
53
53
  Server: () => Server,
54
54
  ServerChannelManager: () => ServerChannelManager,
55
55
  ServerManager: () => ServerManager,
56
+ StoatAPIError: () => StoatAPIError,
56
57
  SweeperManager: () => SweeperManager,
57
58
  TextChannel: () => TextChannel,
58
59
  UnknownChannel: () => UnknownChannel,
@@ -174,7 +175,6 @@ var Message = class extends Base {
174
175
  this.channelId = data.channel;
175
176
  const timestamp = (0, import_ulid.decodeTime)(this.id);
176
177
  if (timestamp) {
177
- console.log(timestamp);
178
178
  this.createdAt = new Date(timestamp);
179
179
  }
180
180
  if (data.attachments) {
@@ -450,7 +450,7 @@ var GatewayManager = class {
450
450
  if (payload.users) {
451
451
  for (const rawUser of payload.users) {
452
452
  this.client.users._add(rawUser);
453
- if (rawUser.relation === "User" && !this.client.user) {
453
+ if (rawUser.relationship === "User" && !this.client.user) {
454
454
  this.client.user = new ClientUser(this.client, rawUser);
455
455
  }
456
456
  }
@@ -617,7 +617,8 @@ var GatewayManager = class {
617
617
  }
618
618
  reconnect() {
619
619
  if (!this.token) {
620
- return this.client.emit("error", new Error("RECONNECT_FAILED: No token available."));
620
+ this.client.emit("error", new Error("RECONNECT_FAILED: No token available."));
621
+ return;
621
622
  }
622
623
  let waitTime = Math.pow(2, this.reconnectAttempts) * 1e3;
623
624
  const jitter = waitTime * 0.2 * Math.random();
@@ -651,6 +652,46 @@ var AsyncBucket = class {
651
652
  resetAt = 0;
652
653
  queue = Promise.resolve();
653
654
  };
655
+ var StoatAPIError = class _StoatAPIError extends Error {
656
+ statusCode;
657
+ apiType;
658
+ location;
659
+ rawData;
660
+ method;
661
+ path;
662
+ constructor(statusCode, data, method, path) {
663
+ let errorMessage = "Unknown API Error";
664
+ let type = "Unknown";
665
+ let location = "Unknown";
666
+ if (typeof data === "object" && data !== null) {
667
+ if ("type" in data || "location" in data) {
668
+ type = String(data.type || type);
669
+ location = String(data.location || location);
670
+ errorMessage = `Type: ${type} (Location: ${location})`;
671
+ } else if ("message" in data) {
672
+ errorMessage = String(data.message);
673
+ } else {
674
+ try {
675
+ errorMessage = JSON.stringify(data);
676
+ } catch {
677
+ errorMessage = "Unparseable Error Object";
678
+ }
679
+ }
680
+ } else if (typeof data === "string" && data.trim() !== "") {
681
+ errorMessage = data;
682
+ }
683
+ const routeInfo = method && path ? ` on ${method.toUpperCase()} ${path}` : "";
684
+ super(`${errorMessage}${routeInfo}`);
685
+ this.name = `StoatAPIError[${statusCode}]`;
686
+ this.statusCode = statusCode;
687
+ this.apiType = type;
688
+ this.location = location;
689
+ this.rawData = data;
690
+ this.method = method;
691
+ this.path = path;
692
+ Object.setPrototypeOf(this, _StoatAPIError.prototype);
693
+ }
694
+ };
654
695
  var RESTManager = class {
655
696
  constructor(client) {
656
697
  this.client = client;
@@ -710,22 +751,23 @@ var RESTManager = class {
710
751
  bucket.remaining = Number(remainingHeader);
711
752
  bucket.resetAt = Date.now() + Number(resetAfterHeader);
712
753
  }
754
+ const textBody = await response.body.text();
755
+ let data;
756
+ try {
757
+ data = JSON.parse(textBody);
758
+ } catch {
759
+ data = textBody;
760
+ }
713
761
  if (response.statusCode === 429) {
714
- const data2 = await response.body.json();
715
- const retryMs = data2.retry_after || Number(resetAfterHeader) || 5e3;
762
+ const retryMs = typeof data === "object" && data?.retry_after ? data.retry_after : Number(resetAfterHeader) || 5e3;
716
763
  this.client.emit("debug", `Hit 429 on [${method}:${endpoint}]. Retrying in ${retryMs}ms.`);
717
764
  bucket.remaining = 0;
718
765
  bucket.resetAt = Date.now() + retryMs;
719
766
  await sleep(retryMs);
720
767
  return this.execute(method, endpoint, body, bucket);
721
768
  }
722
- const data = await response.body.json();
723
769
  if (response.statusCode >= 400) {
724
- let errorMessage = "Unknown Error";
725
- if (typeof data === "object" && data !== null && "message" in data) {
726
- errorMessage = String(data.message);
727
- }
728
- throw new Error(`[Stoat API Error ${response.statusCode}]: ${errorMessage}`);
770
+ throw new StoatAPIError(response.statusCode, data, method, endpoint);
729
771
  }
730
772
  return data;
731
773
  }
@@ -745,7 +787,14 @@ var RESTManager = class {
745
787
  body: formData
746
788
  });
747
789
  if (!response.ok) {
748
- throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
790
+ let errData;
791
+ const errText = await response.text();
792
+ try {
793
+ errData = JSON.parse(errText);
794
+ } catch {
795
+ errData = errText;
796
+ }
797
+ throw new StoatAPIError(response.status, errData, "POST", "/attachments");
749
798
  }
750
799
  const data = await response.json();
751
800
  return data.id;
@@ -845,11 +894,17 @@ var util2 = __toESM(require("util"), 1);
845
894
 
846
895
  // src/managers/BaseManager.ts
847
896
  var BaseManager = class {
897
+ cache;
898
+ client;
848
899
  constructor(client, limit = Infinity) {
849
900
  this.client = client;
850
901
  this.cache = new Collection(limit);
902
+ Object.defineProperty(this, "client", {
903
+ value: client,
904
+ enumerable: false,
905
+ writable: false
906
+ });
851
907
  }
852
- cache;
853
908
  /**
854
909
  * Transforms raw data into a Structure, patches if existing, and saves to cache.
855
910
  * @internal
@@ -1558,16 +1613,28 @@ var MemberRoleManager = class {
1558
1613
  }
1559
1614
  };
1560
1615
 
1616
+ // src/utils/Constants.ts
1617
+ var StoatCDN = "https://cdn.stoatusercontent.com";
1618
+
1561
1619
  // src/structures/Member.ts
1562
1620
  var Member = class extends Base {
1621
+ // The server ID the member is in
1563
1622
  serverId;
1623
+ // The nickname the member has in the server, if any.
1564
1624
  nickname = null;
1625
+ // The avatar the member has in the server, if any.
1565
1626
  avatar = null;
1566
- roleIds = [];
1627
+ /** @internal */
1628
+ _roles = [];
1629
+ // The date the member joined the server
1567
1630
  joinedAt;
1631
+ // The date the member's timeout expires, or null if not timed out
1568
1632
  timeout = null;
1633
+ // Whatever the user can talk in Voice Chat
1569
1634
  canPublish = false;
1635
+ // Whatever the user can hear in Voice Chat
1570
1636
  canRecieve = false;
1637
+ // Member roles manager
1571
1638
  roles;
1572
1639
  constructor(client, data) {
1573
1640
  super(client, { _id: data.user._id });
@@ -1579,11 +1646,17 @@ var Member = class extends Base {
1579
1646
  _patch(data) {
1580
1647
  if (data.nickname !== void 0) this.nickname = data.nickname;
1581
1648
  if (data.avatar !== void 0) this.avatar = data.avatar;
1582
- if (data.roles !== void 0) this.roleIds = data.roles;
1649
+ if (data.roles !== void 0) this._roles = data.roles;
1583
1650
  if (data.timeout !== void 0) this.timeout = data.timeout ? new Date(data.timeout) : null;
1584
1651
  if (data.can_publish !== void 0) this.canPublish = data.canPublish;
1585
1652
  if (data.can_recieve !== void 0) this.canRecieve = data.canRecieve;
1586
1653
  }
1654
+ /**
1655
+ * Get member role IDs
1656
+ */
1657
+ get roleIds() {
1658
+ return this._roles;
1659
+ }
1587
1660
  /** Gets the global User object for this member */
1588
1661
  get user() {
1589
1662
  return this.client.users.cache.get(this.id);
@@ -1592,18 +1665,12 @@ var Member = class extends Base {
1592
1665
  get server() {
1593
1666
  return this.client.servers.cache.get(this.serverId);
1594
1667
  }
1595
- /** Resolves the array of role strings into actual Role objects */
1596
- get roleObjects() {
1597
- const server = this.server;
1598
- if (!server) return [];
1599
- return this.roleIds.map((id) => server.roles.cache.get(id)).filter((role) => role !== void 0);
1600
- }
1601
1668
  /** Calculates the member's total permissions using BigInt */
1602
1669
  get permissions() {
1603
1670
  const server = this.server;
1604
1671
  if (!server) return 0n;
1605
1672
  let totalPerms = server.defaultPermissions ?? 0n;
1606
- for (const role of this.roleObjects) {
1673
+ for (const role of this.roles.cache.values()) {
1607
1674
  totalPerms |= BigInt(role.permissions);
1608
1675
  }
1609
1676
  if (server.ownerId === this.id) {
@@ -1611,40 +1678,76 @@ var Member = class extends Base {
1611
1678
  }
1612
1679
  return totalPerms;
1613
1680
  }
1614
- /** Checks if the member has a specific permission */
1615
- hasPermission(permission) {
1616
- return Permissions.has(this.permissions, permission);
1681
+ /** Get avatar URL for this member, or null if they don't have one.
1682
+ * @example
1683
+ * // Get a member's avatar URL
1684
+ * const avatarURL = member.avatarURL;
1685
+ * console.log(avatarURL); // https://cdn.stoat.chat/attachments/avatars/1234567890/avatar.png
1686
+ */
1687
+ get avatarURL() {
1688
+ if (!this.avatar) return null;
1689
+ return `${StoatCDN}/attachments/avatars/${this.avatar.id}/${this.avatar.filename}`;
1617
1690
  }
1618
1691
  /**
1619
- * Edits this member's nickname, avatar, roles, or timeout.
1692
+ * Ban this member from the server.
1693
+ * @param options The options for this ban
1694
+ * @example
1695
+ * // Ban a member with a reason and delete their messages from the last hour
1696
+ * await member.ban({ reason: "Spamming", deleteMessageSeconds: 3600 });
1620
1697
  */
1621
- async edit(options) {
1698
+ async ban(options) {
1622
1699
  let server = this.server;
1623
1700
  if (!server) server = await this.client.servers.fetch(this.serverId);
1624
- return await server.members.edit(this.id, options);
1701
+ await server.members.ban(this.id, options);
1625
1702
  }
1626
1703
  /**
1627
- * Kicks this member from the server.
1704
+ * Creates a DM channel between the client's user and this member.
1705
+ * @param force If true, forces the creation of a new DM channel even if one already exists.
1706
+ * @returns A promise that resolves to the created DMChannel object.
1707
+ * @throws {Error} If the API request fails.
1708
+ * @example
1709
+ * // Create a DM with this member
1710
+ * const dm = await member.createDM();
1711
+ * console.log(`DM channel ID: ${dm.id}`);
1628
1712
  */
1629
- async kick() {
1713
+ async createDM(force = false) {
1714
+ await this.client.users.createDM(this.id, { force });
1715
+ }
1716
+ /**
1717
+ * Timeout this member for a specified duration.
1718
+ * @param duration The duration of the timeout in milliseconds.
1719
+ * @example
1720
+ * // Timeout a member for 10 minutes (600,000 milliseconds)
1721
+ * await member.setTimeout(600000);
1722
+ */
1723
+ async setTimeout(duration) {
1630
1724
  let server = this.server;
1631
1725
  if (!server) server = await this.client.servers.fetch(this.serverId);
1632
- await server.members.kick(this.id);
1726
+ await server.members.setTimeout(this.id, duration);
1727
+ }
1728
+ /** Checks if the member has a specific permission */
1729
+ hasPermission(permission) {
1730
+ return Permissions.has(this.permissions, permission);
1633
1731
  }
1634
1732
  /**
1635
- * Bans this member from the server.
1636
- * @param options The options for this ban
1733
+ * Edit this member.
1734
+ * @param options The options to edit the member with (nickname, roles, timeout, etc.)
1735
+ * @returns A promise that resolves to the updated Member.
1637
1736
  */
1638
- async ban(options) {
1737
+ async edit(options) {
1639
1738
  let server = this.server;
1640
1739
  if (!server) server = await this.client.servers.fetch(this.serverId);
1641
- await server.members.ban(this.id, options);
1740
+ return await server.members.edit(this.id, options);
1642
1741
  }
1643
- async unban() {
1644
- let server = this.client.servers.cache.get(this.serverId);
1742
+ /**
1743
+ * Kick this member from the server.
1744
+ */
1745
+ async kick() {
1746
+ let server = this.server;
1645
1747
  if (!server) server = await this.client.servers.fetch(this.serverId);
1646
- await server.members.unban(this.id);
1748
+ await server.members.kick(this.id);
1647
1749
  }
1750
+ /** @internal */
1648
1751
  [util4.inspect.custom](depth, options, inspect10) {
1649
1752
  const { client, serverId, ...props } = this;
1650
1753
  return `${this.constructor.name} ${inspect10(
@@ -1682,24 +1785,48 @@ var MemberManager = class extends BaseManager {
1682
1785
  }
1683
1786
  return new Member(this.client, data);
1684
1787
  }
1788
+ /**
1789
+ * Resolve a string or mention to Member
1790
+ * @param member The MemberResolvable to resolve
1791
+ * @returns The resolved Member or undefined if not found
1792
+ */
1685
1793
  resolve(member) {
1686
1794
  if (member instanceof Member) return member;
1687
1795
  if (member instanceof User) return this.cache.get(member.id);
1688
1796
  if (typeof member === "string") return this.cache.get(member.replace(/[<@>]/g, ""));
1689
1797
  return void 0;
1690
1798
  }
1799
+ /**
1800
+ * Resolve a Member to their ID string.
1801
+ * @param member The MemberResolvable to resolve
1802
+ * @returns The resolved ID string
1803
+ * @throws {TypeError} If the provided resolvable is invalid
1804
+ */
1691
1805
  resolveId(member) {
1692
1806
  if (typeof member === "string") return member.replace(/[<@>]/g, "");
1693
1807
  if ("id" in member) return member.id;
1694
1808
  throw new TypeError("Invalid MemberResolvable provided.");
1695
1809
  }
1696
- async fetch(member, force = true) {
1810
+ /**
1811
+ * Fetches a member from the server, or returns the cached version if available and not forced.
1812
+ * @param member The MemberResolvable to fetch
1813
+ * @param force Whether to bypass the cache and fetch fresh data from the API
1814
+ * @returns A promise that resolves to the fetched Member
1815
+ * @throws {Error} If the API request fails or the member is not found
1816
+ * @example
1817
+ * // Fetch a member by ID, using cache if available
1818
+ * const member = await server.members.fetch("1234567890");
1819
+ *
1820
+ * // Fetch a member by mention, bypassing cache
1821
+ * const member = await server.members.fetch("<@1234567890>", true);
1822
+ */
1823
+ async fetch(member, force = false) {
1697
1824
  if (!force) {
1698
1825
  const cached = this.resolve(member);
1699
1826
  if (cached) return cached;
1700
1827
  }
1701
1828
  const id = this.resolveId(member);
1702
- const data = await this.client.rest.get(`/channels/${id}`);
1829
+ const data = await this.client.rest.get(`/servers/${this.server.id}/members/${id}`);
1703
1830
  return this._add(data);
1704
1831
  }
1705
1832
  /**
@@ -1739,6 +1866,14 @@ var MemberManager = class extends BaseManager {
1739
1866
  * Edits a member in the server.
1740
1867
  * @param member The MemberResolvable to edit.
1741
1868
  * @param options The fields to update (nickname, roles, timeout, etc.).
1869
+ * @returns A promise that resolves to the updated Member.
1870
+ * @throws {Error} If the API request fails or the member is not found.
1871
+ * @example
1872
+ * // Change a member's nickname and add a role
1873
+ * const updatedMember = await server.members.edit("1234567890", {
1874
+ * nickname: "New Nickname",
1875
+ * roles: ["roleId1", "roleId2"],
1876
+ * });
1742
1877
  */
1743
1878
  async edit(member, options) {
1744
1879
  const id = this.resolveId(member);
@@ -1765,6 +1900,9 @@ var MemberManager = class extends BaseManager {
1765
1900
  /**
1766
1901
  * Kicks a member from the server.
1767
1902
  * @param member The MemberResolvable to kick.
1903
+ * @example
1904
+ * // Kick a member by ID
1905
+ * await server.members.kick("1234567890");
1768
1906
  */
1769
1907
  async kick(member) {
1770
1908
  const id = this.resolveId(member);
@@ -1775,6 +1913,9 @@ var MemberManager = class extends BaseManager {
1775
1913
  * Bans a member from the server.
1776
1914
  * @param member The MemberResolvable to ban.
1777
1915
  * @param options The ban options
1916
+ * @example
1917
+ * // Ban a member by ID
1918
+ * await server.members.ban("1234567890", { reason: "Spamming", deleteMessageSeconds: 3600 });
1778
1919
  */
1779
1920
  async ban(member, options) {
1780
1921
  const id = this.resolveId(member);
@@ -1787,11 +1928,26 @@ var MemberManager = class extends BaseManager {
1787
1928
  /**
1788
1929
  * Unbans a user from the server
1789
1930
  * @param member The MemberResolvable to unban
1931
+ * @example
1932
+ * // Unban a member by ID
1933
+ * await server.members.unban("1234567890");
1790
1934
  */
1791
1935
  async unban(member) {
1792
1936
  const id = this.resolveId(member);
1793
1937
  await this.client.rest.delete(`/servers/${this.server.id}/bans/${id}`);
1794
1938
  }
1939
+ /**
1940
+ * Timeouts a member in the server for a specified duration.
1941
+ * @param member The MemberResolvable to timeout
1942
+ * @param duration The duration of the timeout in milliseconds
1943
+ * @example
1944
+ * // Timeout a member for 10 minutes
1945
+ * await server.members.setTimeout("1234567890", 10 * 60 * 1000);
1946
+ */
1947
+ async setTimeout(member, duration) {
1948
+ const id = this.resolveId(member);
1949
+ await this.edit(id, { timeout: new Date(Date.now() + duration).toISOString() });
1950
+ }
1795
1951
  [util5.inspect.custom]() {
1796
1952
  return this.cache;
1797
1953
  }
@@ -2156,7 +2312,7 @@ var RoleManager = class extends BaseManager {
2156
2312
  * console.log("Role deleted successfully.");
2157
2313
  *
2158
2314
  * // Delete a role using a Role object
2159
- * const role = await server.roles.fetch("01JE2MM759J5D7CHJF084R7MJ2");
2315
+ * const role = await server.roles.fetch("01JE2MM759J5D7CHJF084R7");
2160
2316
  * await server.roles.delete(role);
2161
2317
  * console.log("Role deleted successfully.");
2162
2318
  *
@@ -2644,6 +2800,38 @@ var UserManager = class extends BaseManager {
2644
2800
  const data = await this.client.rest.patch(`/users/@me`, payload);
2645
2801
  return this._add(data);
2646
2802
  }
2803
+ /**
2804
+ * The DM between the client's user and a user
2805
+ *
2806
+ * @param {string} userId The user id
2807
+ * @returns {?DMChannel}
2808
+ * @private
2809
+ */
2810
+ dmChannel(userId) {
2811
+ return this.client.channels.cache.find((channel) => channel.isDM() && channel.recipients.includes(userId)) ?? null;
2812
+ }
2813
+ /**
2814
+ * Creates a DM channel between the client's user and another user.
2815
+ * @param user The UserResolvable to create a DM with.
2816
+ * @param options Additional options for DM creation.
2817
+ * @param options.force If true, forces the creation of a new DM channel even if one already exists.
2818
+ * @returns A promise that resolves to the created DMChannel object.
2819
+ * @throws {TypeError} If an invalid UserResolvable is provided.
2820
+ * @throws {Error} If the API request fails.
2821
+ * @example
2822
+ * // Create a DM with a user by ID
2823
+ * const dm = await client.users.createDM("1234567890");
2824
+ * console.log(`DM channel ID: ${dm.id}`);
2825
+ */
2826
+ async createDM(user, { force = false } = {}) {
2827
+ const id = this.resolveId(user);
2828
+ if (!force) {
2829
+ const dmChannel = this.dmChannel(id);
2830
+ if (dmChannel) return dmChannel;
2831
+ }
2832
+ const data = await this.client.rest.get(`/users/${id}/dm`);
2833
+ return this.client.channels._add(data);
2834
+ }
2647
2835
  };
2648
2836
 
2649
2837
  // src/managers/SweepManager.ts
@@ -2790,6 +2978,7 @@ var EmbedBuilder = class {
2790
2978
  Server,
2791
2979
  ServerChannelManager,
2792
2980
  ServerManager,
2981
+ StoatAPIError,
2793
2982
  SweeperManager,
2794
2983
  TextChannel,
2795
2984
  UnknownChannel,
@@ -2798,3 +2987,4 @@ var EmbedBuilder = class {
2798
2987
  UserPresence,
2799
2988
  UserRelationship
2800
2989
  });
2990
+ //# sourceMappingURL=index.cjs.map