@kash-88/alerts 1.2.1 → 1.3.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.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/Function/createCustomAlerts.d.ts +16 -0
  3. package/dist/src/Function/createCustomAlerts.js +51 -0
  4. package/dist/src/Function/createMerchandise.d.ts +22 -0
  5. package/dist/src/Function/createMerchandise.js +82 -0
  6. package/dist/src/Function/generateSignature.d.ts +13 -0
  7. package/dist/src/Function/generateSignature.js +18 -0
  8. package/dist/src/Function/getAuthorizeLink.d.ts +13 -0
  9. package/dist/src/Function/getAuthorizeLink.js +27 -0
  10. package/dist/src/Function/getDonationsAlerts.d.ts +11 -0
  11. package/dist/src/Function/getDonationsAlerts.js +30 -0
  12. package/dist/src/Function/getExternal.d.ts +6 -0
  13. package/dist/src/Function/getExternal.js +27 -0
  14. package/dist/src/Function/getOauthToken.d.ts +13 -0
  15. package/dist/src/Function/getOauthToken.js +44 -0
  16. package/dist/src/Function/getPrivateToken.d.ts +10 -0
  17. package/dist/src/Function/getPrivateToken.js +40 -0
  18. package/dist/src/Function/getUser.d.ts +10 -0
  19. package/dist/src/Function/getUser.js +26 -0
  20. package/dist/src/Function/getUserChannel.d.ts +8 -0
  21. package/dist/src/Function/getUserChannel.js +13 -0
  22. package/dist/src/Function/getUserDataFromPromocode.d.ts +11 -0
  23. package/dist/src/Function/getUserDataFromPromocode.js +39 -0
  24. package/dist/src/Function/sendSaleAlert.d.ts +20 -0
  25. package/dist/src/Function/sendSaleAlert.js +81 -0
  26. package/dist/src/Function/updateMerchandise.d.ts +21 -0
  27. package/dist/src/Function/updateMerchandise.js +76 -0
  28. package/dist/src/Function/updateOauthToken.d.ts +13 -0
  29. package/dist/src/Function/updateOauthToken.js +44 -0
  30. package/dist/src/Function/updateOrCreateMerchandise.d.ts +22 -0
  31. package/dist/src/Function/updateOrCreateMerchandise.js +80 -0
  32. package/dist/src/Types/CustomAlerts.d.ts +12 -0
  33. package/dist/src/Types/DonationsAlerts.d.ts +31 -0
  34. package/dist/src/Types/DonationsAlerts.js +1 -0
  35. package/dist/src/Types/Merchandise.d.ts +18 -0
  36. package/dist/src/Types/Merchandise.js +1 -0
  37. package/dist/src/Types/MerchandiseSale.d.ts +14 -0
  38. package/dist/src/Types/MerchandiseSale.js +1 -0
  39. package/dist/src/Types/OAuthScope.d.ts +9 -0
  40. package/dist/src/Types/OAuthScope.js +10 -0
  41. package/dist/src/Types/OAuthToken.d.ts +7 -0
  42. package/dist/src/Types/OAuthToken.js +1 -0
  43. package/dist/src/Types/User.d.ts +9 -0
  44. package/dist/src/Types/User.js +1 -0
  45. package/dist/src/Types/index.d.ts +8 -0
  46. package/dist/src/Types/index.js +2 -0
  47. package/dist/src/WebSocket/CentrifugeClient.d.ts +71 -0
  48. package/dist/src/WebSocket/CentrifugeClient.js +216 -0
  49. package/dist/src/index.d.ts +18 -9
  50. package/dist/src/index.js +18 -9
  51. package/dist/src/utils.d.ts +5 -13
  52. package/dist/src/utils.js +16 -32
  53. package/package.json +22 -18
  54. package/readme.md +502 -81
  55. package/src/Function/createCustomAlerts.ts +65 -0
  56. package/src/Function/createMerchandise.ts +105 -0
  57. package/src/Function/generateSignature.ts +23 -0
  58. package/src/Function/getAuthorizeLink.ts +37 -0
  59. package/src/Function/getDonationsAlerts.ts +37 -0
  60. package/src/Function/getExternal.ts +31 -0
  61. package/src/Function/getOauthToken.ts +54 -0
  62. package/src/Function/getPrivateToken.ts +50 -0
  63. package/src/Function/getUser.ts +32 -0
  64. package/src/Function/getUserChannel.ts +17 -0
  65. package/src/Function/getUserDataFromPromocode.ts +51 -0
  66. package/src/Function/sendSaleAlert.ts +103 -0
  67. package/src/Function/updateMerchandise.ts +98 -0
  68. package/src/Function/updateOauthToken.ts +54 -0
  69. package/src/Function/updateOrCreateMerchandise.ts +103 -0
  70. package/src/Types/CustomAlerts.ts +13 -0
  71. package/src/Types/DonationsAlerts.ts +32 -0
  72. package/src/Types/Merchandise.ts +19 -0
  73. package/src/Types/MerchandiseSale.ts +15 -0
  74. package/src/Types/OAuthScope.ts +10 -0
  75. package/src/Types/OAuthToken.ts +8 -0
  76. package/src/Types/User.ts +10 -0
  77. package/src/Types/index.ts +17 -0
  78. package/src/WebSocket/CentrifugeClient.ts +271 -0
  79. package/src/index.ts +25 -6
  80. package/src/utils.ts +22 -44
  81. package/dist/src/func/getAuthorizeLink.d.ts +0 -18
  82. package/dist/src/func/getAuthorizeLink.js +0 -29
  83. package/dist/src/func/getOauthToken.d.ts +0 -27
  84. package/dist/src/func/getOauthToken.js +0 -42
  85. package/dist/src/func/getPrivateToken.d.ts +0 -29
  86. package/dist/src/func/getPrivateToken.js +0 -46
  87. package/dist/src/func/getUser.d.ts +0 -23
  88. package/dist/src/func/getUser.js +0 -38
  89. package/dist/src/func/getUserChannel.d.ts +0 -19
  90. package/dist/src/func/getUserChannel.js +0 -32
  91. package/dist/src/func/updateAccessToken.d.ts +0 -24
  92. package/dist/src/func/updateAccessToken.js +0 -39
  93. package/dist/src/types.d.ts +0 -33
  94. package/dist/src/ws/CentrifugeClient.d.ts +0 -31
  95. package/dist/src/ws/CentrifugeClient.js +0 -71
  96. package/src/example/getAuthorizeLink.ts +0 -11
  97. package/src/example/getOauthToken.ts +0 -16
  98. package/src/example/getUser.ts +0 -12
  99. package/src/example/getUserChannel.ts +0 -10
  100. package/src/example/updateAccessToken.ts +0 -17
  101. package/src/example/wsExample.ts +0 -50
  102. package/src/func/getAuthorizeLink.ts +0 -33
  103. package/src/func/getOauthToken.ts +0 -46
  104. package/src/func/getPrivateToken.ts +0 -53
  105. package/src/func/getUser.ts +0 -42
  106. package/src/func/getUserChannel.ts +0 -34
  107. package/src/func/updateAccessToken.ts +0 -43
  108. package/src/types.ts +0 -38
  109. package/src/ws/CentrifugeClient.ts +0 -108
  110. /package/dist/src/{types.js → Types/CustomAlerts.js} +0 -0
@@ -0,0 +1,98 @@
1
+ import axios from "axios";
2
+ import { Merchandise } from "@type";
3
+ import generateSignature from "@function/generateSignature.js";
4
+ import { formatAxiosError } from "@utils";
5
+
6
+ /**
7
+ * Update an existing merchandise by its DonationAlerts ID.
8
+ *
9
+ * @param {string} access_token - User access token
10
+ * @param {string} client_secret - API client secret key for signature
11
+ * @param {number} merchandise_id - Unique merchandise ID on DonationAlerts
12
+ * @param {Record<string, string>} title - Object with merchandise titles in different locales (en_US required)
13
+ * @param {string} currency - Currency code (EUR, USD, RUB, BRL, TRY)
14
+ * @param {number} price_user - Revenue added to streamer for each sale
15
+ * @param {number} price_service - Revenue added to DonationAlerts for each sale
16
+ * @param {number} [is_active=0] - 0 or 1, whether merchandise is available for purchase
17
+ * @param {number} [is_percentage=0] - 0 or 1, whether prices are percentages or absolute amounts
18
+ * @param {string} [url] - URL to merchandise's web page
19
+ * @param {string} [img_url] - URL to merchandise's image
20
+ * @param {number} [end_at_ts] - Unix timestamp when merchandise becomes inactive
21
+ *
22
+ * @returns {Promise<Merchandise>} Updated merchandise data
23
+ * @see {@link https://www.donationalerts.com/apidoc#api_v1__merchandises__update_merchandise}
24
+ */
25
+
26
+ export default async function updateMerchandise(
27
+ access_token: string,
28
+ client_secret: string,
29
+ merchandise_id: number,
30
+ title: Record<string, string>,
31
+ currency: string,
32
+ price_user: number,
33
+ price_service: number,
34
+ is_active: number = 0,
35
+ is_percentage: number = 0,
36
+ url?: string,
37
+ img_url?: string,
38
+ end_at_ts?: number
39
+ ): Promise<Merchandise> {
40
+ if (!access_token || typeof access_token !== "string") {
41
+ throw new Error("access_token must be a non-empty string");
42
+ }
43
+ if (!client_secret || typeof client_secret !== "string") {
44
+ throw new Error("client_secret must be a non-empty string");
45
+ }
46
+ if (typeof merchandise_id !== "number" || merchandise_id <= 0) {
47
+ throw new Error("merchandise_id must be a positive number");
48
+ }
49
+ if (!title || Object.keys(title).length === 0) {
50
+ throw new Error("title must be a non-empty object");
51
+ }
52
+ if (!currency || typeof currency !== "string") {
53
+ throw new Error("currency must be a non-empty string");
54
+ }
55
+ if (typeof price_user !== "number" || typeof price_service !== "number") {
56
+ throw new Error("price_user and price_service must be numbers");
57
+ }
58
+
59
+ try {
60
+ const params: Record<string, string | number> = {
61
+ currency,
62
+ price_user,
63
+ price_service,
64
+ is_active,
65
+ is_percentage
66
+ };
67
+
68
+ for (const [locale, value] of Object.entries(title)) {
69
+ params[`title[${locale}]`] = value;
70
+ }
71
+ if (url) params.url = url;
72
+ if (img_url) params.img_url = img_url;
73
+ if (end_at_ts !== undefined) params.end_at_ts = end_at_ts;
74
+
75
+ const signature = generateSignature(params, client_secret);
76
+ params.signature = signature;
77
+
78
+ const formData = new URLSearchParams();
79
+ for (const [key, value] of Object.entries(params)) {
80
+ formData.append(key, String(value));
81
+ }
82
+
83
+ const response = await axios.put<{ data: Merchandise }>(
84
+ `https://www.donationalerts.com/api/v1/merchandise/${merchandise_id}`,
85
+ formData,
86
+ {
87
+ headers: {
88
+ Authorization: `Bearer ${access_token}`,
89
+ "Content-Type": "application/x-www-form-urlencoded"
90
+ }
91
+ }
92
+ );
93
+
94
+ return response.data.data;
95
+ } catch (error: any) {
96
+ throw new Error(formatAxiosError(error));
97
+ }
98
+ }
@@ -0,0 +1,54 @@
1
+ import axios from "axios";
2
+ import { OAuthToken, OAuthScope } from "@type";
3
+ import { formatAxiosError } from "@utils";
4
+
5
+ /**
6
+ * Refresh Access token using Refresh token.
7
+ *
8
+ * @param {string} client_id - Your client (application) ID
9
+ * @param {string} client_secret - Your client (application) secret
10
+ * @param {string} refresh_token - User refresh token
11
+ * @param {OAuthScope[]} scopes - Array of access scopes
12
+ *
13
+ * @returns {Promise<OAuthToken>} A promise that resolves to the new token data from the API.
14
+ * @see {@link https://www.donationalerts.com/apidoc#authorization__authorization_code__getting_access_token}
15
+ */
16
+
17
+ export default async function updateOauthToken(
18
+ client_id: string,
19
+ client_secret: string,
20
+ refresh_token: string,
21
+ scopes: OAuthScope[]
22
+ ): Promise<OAuthToken> {
23
+ if (!client_id || typeof client_id !== "string") {
24
+ throw new Error("client_id must be a non-empty string");
25
+ }
26
+ if (!client_secret || typeof client_secret !== "string") {
27
+ throw new Error("client_secret must be a non-empty string");
28
+ }
29
+ if (!refresh_token || typeof refresh_token !== "string") {
30
+ throw new Error("refresh_token must be a non-empty string");
31
+ }
32
+ if (!Array.isArray(scopes) || scopes.length === 0) {
33
+ throw new Error("scopes must be a non-empty array");
34
+ }
35
+
36
+ try {
37
+ const formData = new URLSearchParams();
38
+ formData.append("grant_type", "refresh_token");
39
+ formData.append("refresh_token", refresh_token);
40
+ formData.append("client_id", client_id);
41
+ formData.append("client_secret", client_secret);
42
+ formData.append("scope", Array.from(new Set(scopes)).join(" "));
43
+
44
+ const response = await axios.post<OAuthToken>("https://www.donationalerts.com/oauth/token", formData, {
45
+ headers: {
46
+ "Content-Type": "application/x-www-form-urlencoded"
47
+ }
48
+ });
49
+
50
+ return response.data;
51
+ } catch (error: any) {
52
+ throw new Error(formatAxiosError(error));
53
+ }
54
+ }
@@ -0,0 +1,103 @@
1
+ import axios from "axios";
2
+ import { Merchandise } from "@type";
3
+ import generateSignature from "@function/generateSignature.js";
4
+ import { formatAxiosError } from "@utils";
5
+
6
+ /**
7
+ * Update or create a merchandise by merchant and merchandise identifiers.
8
+ *
9
+ * @param {string} access_token - User access token
10
+ * @param {string} client_secret - API client secret key for signature
11
+ * @param {string} merchant_identifier - Merchant's ID on DonationAlerts
12
+ * @param {string} merchandise_identifier - Up to 16 characters long unique merchandise ID
13
+ * @param {Record<string, string>} title - Object with merchandise titles in different locales (en_US required)
14
+ * @param {string} currency - Currency code (EUR, USD, RUB, BRL, TRY)
15
+ * @param {number} price_user - Revenue added to streamer for each sale
16
+ * @param {number} price_service - Revenue added to DonationAlerts for each sale
17
+ * @param {number} [is_active=0] - 0 or 1, whether merchandise is available for purchase
18
+ * @param {number} [is_percentage=0] - 0 or 1, whether prices are percentages or absolute amounts
19
+ * @param {string} [url] - URL to merchandise's web page
20
+ * @param {string} [img_url] - URL to merchandise's image
21
+ * @param {number} [end_at_ts] - Unix timestamp when merchandise becomes inactive
22
+ *
23
+ * @returns {Promise<Merchandise>} Updated or created merchandise data
24
+ * @see {@link https://www.donationalerts.com/apidoc#api_v1__merchandises__update_or_create_merchandise}
25
+ */
26
+
27
+ export default async function updateOrCreateMerchandise(
28
+ access_token: string,
29
+ client_secret: string,
30
+ merchant_identifier: string,
31
+ merchandise_identifier: string,
32
+ title: Record<string, string>,
33
+ currency: string,
34
+ price_user: number,
35
+ price_service: number,
36
+ is_active: number = 0,
37
+ is_percentage: number = 0,
38
+ url?: string,
39
+ img_url?: string,
40
+ end_at_ts?: number
41
+ ): Promise<Merchandise> {
42
+ if (!access_token || typeof access_token !== "string") {
43
+ throw new Error("access_token must be a non-empty string");
44
+ }
45
+ if (!client_secret || typeof client_secret !== "string") {
46
+ throw new Error("client_secret must be a non-empty string");
47
+ }
48
+ if (!merchant_identifier || typeof merchant_identifier !== "string") {
49
+ throw new Error("merchant_identifier must be a non-empty string");
50
+ }
51
+ if (!merchandise_identifier || typeof merchandise_identifier !== "string") {
52
+ throw new Error("merchandise_identifier must be a non-empty string");
53
+ }
54
+ if (!title || Object.keys(title).length === 0) {
55
+ throw new Error("title must be a non-empty object");
56
+ }
57
+ if (!currency || typeof currency !== "string") {
58
+ throw new Error("currency must be a non-empty string");
59
+ }
60
+ if (typeof price_user !== "number" || typeof price_service !== "number") {
61
+ throw new Error("price_user and price_service must be numbers");
62
+ }
63
+
64
+ try {
65
+ const params: Record<string, string | number> = {
66
+ currency,
67
+ price_user,
68
+ price_service,
69
+ is_active,
70
+ is_percentage
71
+ };
72
+
73
+ for (const [locale, value] of Object.entries(title)) {
74
+ params[`title[${locale}]`] = value;
75
+ }
76
+ if (url) params.url = url;
77
+ if (img_url) params.img_url = img_url;
78
+ if (end_at_ts !== undefined) params.end_at_ts = end_at_ts;
79
+
80
+ const signature = generateSignature(params, client_secret);
81
+ params.signature = signature;
82
+
83
+ const formData = new URLSearchParams();
84
+ for (const [key, value] of Object.entries(params)) {
85
+ formData.append(key, String(value));
86
+ }
87
+
88
+ const response = await axios.put<{ data: Merchandise }>(
89
+ `https://www.donationalerts.com/api/v1/merchandise/${merchant_identifier}/${merchandise_identifier}`,
90
+ formData,
91
+ {
92
+ headers: {
93
+ Authorization: `Bearer ${access_token}`,
94
+ "Content-Type": "application/x-www-form-urlencoded"
95
+ }
96
+ }
97
+ );
98
+
99
+ return response.data.data;
100
+ } catch (error: any) {
101
+ throw new Error(formatAxiosError(error));
102
+ }
103
+ }
@@ -0,0 +1,13 @@
1
+ interface CustomAlerts {
2
+ id: number;
3
+ external_id: string | null;
4
+ header: string | null;
5
+ message: string | null;
6
+ image_url: string | null;
7
+ sound_url: string | null;
8
+ is_shown: 0 | 1;
9
+ created_at: string;
10
+ shown_at: string | null;
11
+ }
12
+
13
+ export default CustomAlerts;
@@ -0,0 +1,32 @@
1
+ interface DonationsAlerts {
2
+ data: {
3
+ id: number;
4
+ name: string;
5
+ username: string;
6
+ message: string;
7
+ message_type: string;
8
+ amount: number;
9
+ currency: string;
10
+ is_shown: number;
11
+ created_at: string;
12
+ shown_at: string | null;
13
+ amount_in_user_currency?: number;
14
+ }[];
15
+ links: {
16
+ first: string;
17
+ last: string;
18
+ prev: string | null;
19
+ next: string | null;
20
+ };
21
+ meta: {
22
+ current_page: number;
23
+ from: number;
24
+ last_page: number;
25
+ path: string;
26
+ per_page: number;
27
+ to: number;
28
+ total: number;
29
+ }
30
+ }
31
+
32
+ export default DonationsAlerts;
@@ -0,0 +1,19 @@
1
+ interface Merchandise {
2
+ id: number;
3
+ merchant: {
4
+ identifier: string;
5
+ name: string;
6
+ };
7
+ identifier: string;
8
+ title: Record<string, string>;
9
+ is_active: 0 | 1;
10
+ is_percentage: 0 | 1;
11
+ currency: string;
12
+ price_user: number;
13
+ price_service: number;
14
+ url: string | null;
15
+ img_url: string | null;
16
+ end_at: string | null;
17
+ }
18
+
19
+ export default Merchandise;
@@ -0,0 +1,15 @@
1
+ interface MerchandiseSale {
2
+ id: number;
3
+ name: string;
4
+ external_id: string;
5
+ username: string | null;
6
+ message: string | null;
7
+ amount: number;
8
+ currency: string;
9
+ bought_amount: number;
10
+ is_shown: number;
11
+ created_at: string;
12
+ shown_at: string | null;
13
+ }
14
+
15
+ export default MerchandiseSale;
@@ -0,0 +1,10 @@
1
+ enum OAuthScope {
2
+ UserShow = "oauth-user-show",
3
+ DonationSubscribe = "oauth-donation-subscribe",
4
+ DonationIndex = "oauth-donation-index",
5
+ CustomAlertStore = "oauth-custom_alert-store",
6
+ GoalSubscribe = "oauth-goal-subscribe",
7
+ PollSubscribe = "oauth-poll-subscribe"
8
+ }
9
+
10
+ export default OAuthScope;
@@ -0,0 +1,8 @@
1
+ interface OAuthToken {
2
+ token_type: string;
3
+ expires_in: number;
4
+ access_token: string;
5
+ refresh_token?: string;
6
+ }
7
+
8
+ export default OAuthToken;
@@ -0,0 +1,10 @@
1
+ interface User {
2
+ id: number;
3
+ code: string;
4
+ name: string;
5
+ avatar: string;
6
+ email: string;
7
+ socket_connection_token: string;
8
+ }
9
+
10
+ export default User;
@@ -0,0 +1,17 @@
1
+ import CustomAlerts from "./CustomAlerts.js";
2
+ import DonationsAlerts from "./DonationsAlerts.js";
3
+ import Merchandise from "./Merchandise.js";
4
+ import MerchandiseSale from "./MerchandiseSale.js";
5
+ import OAuthScope from "./OAuthScope.js";
6
+ import OAuthToken from "./OAuthToken.js";
7
+ import User from "./User.js";
8
+
9
+ export {
10
+ CustomAlerts,
11
+ DonationsAlerts,
12
+ Merchandise,
13
+ MerchandiseSale,
14
+ OAuthScope,
15
+ OAuthToken,
16
+ User
17
+ };
@@ -0,0 +1,271 @@
1
+ import getPrivateToken from "@function/getPrivateToken.js";
2
+ import getUser from "@function/getUser.js";
3
+ import getUserChannel from "@function/getUserChannel.js";
4
+ import { User } from "@type";
5
+ import { formatAxiosError } from "@utils";
6
+ import { WebSocket, RawData } from "ws";
7
+ import { EventEmitter } from "events";
8
+ import TypedEmitter from "typed-emitter";
9
+
10
+ interface WSClientOptions {
11
+ access_token: string;
12
+ channels?: string[];
13
+ autoReconnect?: boolean;
14
+ }
15
+
16
+ type MessageEvents = {
17
+ open: () => void;
18
+ message: (data: any) => void;
19
+ close: (code: number, reason: Buffer) => void;
20
+ error: (error: Error) => void;
21
+ reconnect: () => void;
22
+ }
23
+
24
+ /**
25
+ * Class for interacting with Centrifuge donationalerts
26
+ *
27
+ * @param {WSClientOptions} options - Connection options
28
+ * @param {string} options.access_token - User access token
29
+ * @param {string[]} [options.channels] - Custom channels to subscribe to (defaults to donation channel)
30
+ * @param {boolean} [options.autoReconnect=false] - Automatically reconnect on connection close
31
+ */
32
+
33
+ export default class CentrifugeClient extends (EventEmitter as new () => TypedEmitter<MessageEvents>) {
34
+ private WebSocket: WebSocket | null;
35
+ private user: User | null;
36
+ private userPromise: Promise<User> | null;
37
+ private access_token: string;
38
+ private channels: string[];
39
+ private autoReconnect: boolean;
40
+ private isAuthorized: boolean;
41
+ private reconnectTimer: NodeJS.Timeout | null;
42
+ private isReconnecting: boolean;
43
+ private authMessageHandler: ((rawMessage: RawData) => void) | null;
44
+
45
+ constructor(options: WSClientOptions) {
46
+ super();
47
+ this.access_token = options.access_token;
48
+ this.channels = options.channels ?? [];
49
+ this.autoReconnect = options.autoReconnect ?? false;
50
+ this.isAuthorized = false;
51
+ this.reconnectTimer = null;
52
+ this.isReconnecting = false;
53
+ this.authMessageHandler = null;
54
+
55
+ this.WebSocket = null;
56
+ this.user = null;
57
+ this.userPromise = this.getUser(this.access_token).catch((error) => {
58
+ this.emit("error", error);
59
+ throw error;
60
+ });
61
+
62
+ this.createNewConnection();
63
+ this.setupEvent();
64
+ }
65
+
66
+ /**
67
+ * Obtain user data
68
+ *
69
+ * @param {string} access_token - User access token
70
+ * @returns {Promise<User>} User data
71
+ */
72
+
73
+ private async getUser(
74
+ access_token: string
75
+ ): Promise<User> {
76
+ try {
77
+ this.user = await getUser(access_token);
78
+ return this.user;
79
+ } catch (error: any) {
80
+ throw new Error(formatAxiosError(error));
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Get channels to subscribe to
86
+ *
87
+ * @returns {Promise<string[]>} Array of channel identifiers
88
+ */
89
+
90
+ private async getChannels(): Promise<string[]> {
91
+ if (!this.user?.id) this.user = await (this.userPromise ?? this.getUser(this.access_token));
92
+ if (!this.user?.id) throw new Error("Failed to get channels due to missing user ID");
93
+
94
+ if (this.channels.length > 0) return this.channels;
95
+
96
+ return [getUserChannel(this.user.id)];
97
+ }
98
+
99
+ /**
100
+ * Create a new WebSocket connection
101
+ */
102
+
103
+ public createNewConnection() {
104
+ try {
105
+ if (this.WebSocket) return;
106
+ this.WebSocket = new WebSocket("wss://centrifugo.donationalerts.com/connection/websocket");
107
+ } catch (error: any) {
108
+ throw new Error(error?.message || error);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Forward events through a TypedEmitter
114
+ */
115
+
116
+ private setupEvent() {
117
+ if (!this.WebSocket) {
118
+ this.createNewConnection();
119
+ if (!this.WebSocket) throw new Error("WebSocket connection failed");
120
+ }
121
+
122
+ const ws = this.WebSocket;
123
+
124
+ ws.on('open', () => {
125
+ this.emit("open");
126
+ });
127
+
128
+ ws.on('close', (code: number, reason: Buffer) => {
129
+ this.isAuthorized = false;
130
+ this.emit("close", code, reason);
131
+
132
+ if (this.autoReconnect) {
133
+ this.WebSocket = null;
134
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
135
+ this.isReconnecting = true;
136
+ this.reconnectTimer = setTimeout(() => {
137
+ this.isReconnecting = false;
138
+ this.createNewConnection();
139
+ this.setupEvent();
140
+ this.emit("reconnect");
141
+ }, 5000);
142
+ }
143
+ });
144
+
145
+ ws.on('message', (rawMessage: RawData) => {
146
+ try {
147
+ const message = JSON.parse(rawMessage.toString());
148
+ this.emit("message", message);
149
+ } catch (error: any) {
150
+ this.emit("error", new Error(`Failed to parse WebSocket message: ${error?.message || error}`));
151
+ }
152
+ });
153
+
154
+ ws.on('error', (error: Error) => {
155
+ this.emit("error", error);
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Authorize WebSocket and subscribe to selected channels
161
+ */
162
+
163
+ public async authorization() {
164
+ if (this.isReconnecting) {
165
+ throw new Error("Reconnection is in progress, please wait for the 'reconnect' event");
166
+ }
167
+
168
+ if (!this.WebSocket) this.createNewConnection();
169
+ if (!this.WebSocket) throw new Error("WebSocket connection failed");
170
+
171
+ if (this.WebSocket.readyState !== WebSocket.OPEN) {
172
+ throw new Error("WebSocket connection is not open. Call authorization() after the 'open' event");
173
+ }
174
+
175
+ if (!this.user) this.user = await (this.userPromise ?? this.getUser(this.access_token));
176
+ if (!this.user?.socket_connection_token) throw new Error("Failed to log in due to receiving socket connection token");
177
+
178
+ if (this.authMessageHandler) {
179
+ this.WebSocket?.removeListener('message', this.authMessageHandler);
180
+ }
181
+
182
+ const handleAuthResponse = async (rawMessage: RawData) => {
183
+ try {
184
+ const json = JSON.parse(rawMessage.toString());
185
+
186
+ if (json.id === 1) {
187
+ this.authMessageHandler = null;
188
+ this.WebSocket?.removeListener('message', handleAuthResponse);
189
+
190
+ if (!json.result?.client) {
191
+ this.emit("error", new Error("Authorization failed: missing client ID in response"));
192
+ return;
193
+ }
194
+
195
+ const channels = await this.getChannels();
196
+
197
+ for (let i = 0; i < channels.length; i++) {
198
+ const channel = channels[i];
199
+ const privateToken = await getPrivateToken(
200
+ channel,
201
+ json.result.client,
202
+ this.access_token
203
+ );
204
+
205
+ this.sendMessage(JSON.stringify({
206
+ id: 2 + i,
207
+ method: 1,
208
+ params: {
209
+ channel,
210
+ token: privateToken
211
+ }
212
+ }));
213
+ }
214
+
215
+ this.isAuthorized = true;
216
+ }
217
+ } catch (error: any) {
218
+ this.authMessageHandler = null;
219
+ this.WebSocket?.removeListener('message', handleAuthResponse);
220
+ this.emit("error", new Error(`Authorization failed: ${error?.message || error}`));
221
+ }
222
+ };
223
+
224
+ this.authMessageHandler = handleAuthResponse;
225
+ this.WebSocket.on('message', handleAuthResponse);
226
+
227
+ this.sendMessage(JSON.stringify({
228
+ params: {
229
+ token: this.user.socket_connection_token
230
+ },
231
+ id: 1
232
+ }));
233
+ }
234
+
235
+ /**
236
+ * Send a message through the WebSocket connection
237
+ *
238
+ * @param {string} message - JSON-encoded message to send
239
+ */
240
+
241
+ public sendMessage(message: string): void {
242
+ try {
243
+ if (!this.WebSocket) return;
244
+ if (this.WebSocket.readyState !== WebSocket.OPEN) return;
245
+ this.WebSocket.send(message);
246
+ } catch (error: any) {
247
+ throw new Error(error?.message || error);
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Close the WebSocket connection and stop auto-reconnect
253
+ */
254
+
255
+ public close(): void {
256
+ this.autoReconnect = false;
257
+ if (this.reconnectTimer) {
258
+ clearTimeout(this.reconnectTimer);
259
+ this.reconnectTimer = null;
260
+ }
261
+ if (this.WebSocket) {
262
+ if (this.authMessageHandler) {
263
+ this.WebSocket.removeListener('message', this.authMessageHandler);
264
+ this.authMessageHandler = null;
265
+ }
266
+ this.WebSocket.close();
267
+ this.WebSocket = null;
268
+ }
269
+ this.isAuthorized = false;
270
+ }
271
+ }