ragebuttonapi 1.0.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/cli.js ADDED
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * RAGEBUTTONAPI™ CLI — rage from ur terminal
4
+ * install: npm install -g ragebuttonapi
5
+ * usage:
6
+ * rage → register a rage (prompts for reason)
7
+ * rage setup → configure server url + api key
8
+ * rage "this bug is insane" → rage with inline reason
9
+ * rage --reason "msg" → rage with reason flag
10
+ * rage stats → global stats
11
+ * rage feed → live feed
12
+ * rage --help → help
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const readline = require('readline');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const os = require('os');
21
+ const rageApi = require('./index.js');
22
+
23
+ const CONFIG_PATH = path.join(os.homedir(), '.ragebuttonapirc');
24
+ const VERSION = '1.0.0';
25
+
26
+ // ─── Config file helpers ──────────────────────────────────────────────────────
27
+ function loadConfig() {
28
+ try {
29
+ if (fs.existsSync(CONFIG_PATH)) {
30
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
31
+ }
32
+ } catch { /* corrupt config, treat as missing */ }
33
+ return {};
34
+ }
35
+
36
+ function saveConfig(cfg) {
37
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), { mode: 0o600 });
38
+ }
39
+
40
+ // ─── Colors (no deps, raw ANSI) ───────────────────────────────────────────────
41
+ const c = {
42
+ reset : '\x1b[0m',
43
+ bold : '\x1b[1m',
44
+ dim : '\x1b[2m',
45
+ red : '\x1b[31m',
46
+ green : '\x1b[32m',
47
+ yellow : '\x1b[33m',
48
+ blue : '\x1b[34m',
49
+ magenta: '\x1b[35m',
50
+ cyan : '\x1b[36m',
51
+ white : '\x1b[37m',
52
+ };
53
+
54
+ const acc = s => `${c.magenta}${c.bold}${s}${c.reset}`;
55
+ const dim = s => `${c.dim}${s}${c.reset}`;
56
+ const ok = s => `${c.green}${s}${c.reset}`;
57
+ const err = s => `${c.red}${s}${c.reset}`;
58
+ const hl = s => `${c.cyan}${s}${c.reset}`;
59
+
60
+ function logo() {
61
+ console.log(`
62
+ ${acc('💢 RAGEBUTTONAPI™')} ${dim(`v${VERSION}`)}
63
+ ${dim('─────────────────────────────────────────')}
64
+ `);
65
+ }
66
+
67
+ // ─── Prompts (pure readline, no deps) ────────────────────────────────────────
68
+ function prompt(question) {
69
+ return new Promise(resolve => {
70
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
71
+ rl.question(question, ans => { rl.close(); resolve(ans.trim()); });
72
+ });
73
+ }
74
+
75
+ function promptPassword(question) {
76
+ return new Promise(resolve => {
77
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
78
+ // hide input
79
+ process.stdout.write(question);
80
+ process.stdin.setRawMode?.(true);
81
+ let pass = '';
82
+ process.stdin.resume();
83
+ process.stdin.setEncoding('utf8');
84
+
85
+ const handler = ch => {
86
+ if (ch === '\r' || ch === '\n') {
87
+ process.stdin.setRawMode?.(false);
88
+ process.stdin.removeListener('data', handler);
89
+ process.stdin.pause();
90
+ console.log('');
91
+ rl.close();
92
+ resolve(pass);
93
+ } else if (ch === '\u0003') { // ctrl+c
94
+ process.exit(0);
95
+ } else if (ch === '\u007f' || ch === '\b') {
96
+ pass = pass.slice(0, -1);
97
+ } else {
98
+ pass += ch;
99
+ process.stdout.write('*');
100
+ }
101
+ };
102
+
103
+ process.stdin.on('data', handler);
104
+ });
105
+ }
106
+
107
+ // ─── Setup command ────────────────────────────────────────────────────────────
108
+ async function cmdSetup(existing = {}) {
109
+ logo();
110
+ console.log(`${acc('Setup')} — lets get u configured fr\n`);
111
+
112
+ const defaultUrl = existing.url || '';
113
+ let serverUrl = await prompt(
114
+ `Server URL ${defaultUrl ? dim(`[${defaultUrl}]`) : ''}: `
115
+ );
116
+ if (!serverUrl && defaultUrl) serverUrl = defaultUrl;
117
+ if (!serverUrl) { console.log(err('\nurls cant be empty. try again.')); process.exit(1); }
118
+ serverUrl = serverUrl.replace(/\/$/, '');
119
+
120
+ const defaultKey = existing.apiKey ? '***' + existing.apiKey.slice(-4) : '';
121
+ console.log(`\nAPI Key (from ur dashboard)${defaultKey ? dim(` [${defaultKey}]`) : ''}:`);
122
+ let apiKey;
123
+ try {
124
+ apiKey = await promptPassword('> ');
125
+ } catch {
126
+ // fallback if raw mode unavailable (piped stdin etc)
127
+ apiKey = await prompt('API Key: ');
128
+ }
129
+ if (!apiKey && existing.apiKey) apiKey = existing.apiKey;
130
+ if (!apiKey) { console.log(err('\napi key cant be empty bestie.')); process.exit(1); }
131
+
132
+ const cfg = { url: serverUrl, apiKey };
133
+ saveConfig(cfg);
134
+
135
+ console.log(`\n${ok('✓')} saved to ${dim(CONFIG_PATH)}`);
136
+ console.log(`\n url: ${hl(serverUrl)}`);
137
+ console.log(` key: ${dim('***' + apiKey.slice(-4))}\n`);
138
+ console.log(ok(`all done! run ${hl('rage')} anytime to register ur suffering 💢\n`));
139
+ }
140
+
141
+ // ─── Rage command ─────────────────────────────────────────────────────────────
142
+ async function cmdRage(reason, cfg) {
143
+ if (!cfg.url || !cfg.apiKey) {
144
+ console.log(`\n${err('not configured yet fr.')} run ${hl('rage setup')} first.\n`);
145
+ process.exit(1);
146
+ }
147
+
148
+ // ask for reason if not provided inline
149
+ if (reason === null || reason === undefined) {
150
+ reason = await prompt(`${dim('what broke? (enter to skip):')} `);
151
+ }
152
+
153
+ process.stdout.write(`\n${acc('💢')} registering ur rage...`);
154
+
155
+ try {
156
+ rageApi.init({ url: cfg.url, apiKey: cfg.apiKey });
157
+ const res = await rageApi.rage(reason, { source: 'cli' });
158
+
159
+ process.stdout.write('\r' + ' '.repeat(40) + '\r'); // clear the "registering..." line
160
+
161
+ console.log(`\n ${acc('💢 RAGE REGISTERED')}\n`);
162
+ console.log(` ${ok('✓')} message: ${dim(res.message)}`);
163
+ console.log(` ${ok('✓')} ur total: ${hl(res.your_total)} rages`);
164
+ console.log(` ${ok('✓')} streak: ${hl(res.your_streak + 'd')} 🔥`);
165
+ console.log(` ${ok('✓')} rank: ${hl(res.rank_emoji + ' ' + res.rank)}`);
166
+ console.log(` ${ok('✓')} remaining: ${hl(res.daily_remaining + '/24')} today`);
167
+ console.log(` ${dim('cooldown:')} ${res.cooldown_sec}s`);
168
+
169
+ if (res.new_achievements?.length) {
170
+ console.log('');
171
+ res.new_achievements.forEach(a => {
172
+ console.log(` 🏅 ${c.yellow}${c.bold}ACHIEVEMENT UNLOCKED${c.reset}: ${acc(a.name)}`);
173
+ console.log(` ${dim(a.desc)}`);
174
+ });
175
+ }
176
+
177
+ if (reason) {
178
+ console.log(`\n ${dim('reason:')} ${c.yellow}"${reason}"${c.reset}`);
179
+ }
180
+
181
+ console.log('');
182
+
183
+ } catch (e) {
184
+ process.stdout.write('\r' + ' '.repeat(40) + '\r');
185
+ if (e.code === 'cooldown') {
186
+ console.log(`\n ${err('⏱️ cooldown active')} — ${e.message}\n`);
187
+ } else if (e.code === 'daily_limit') {
188
+ console.log(`\n ${err('😤 daily limit hit')} — 24/24 rages today. iconic. come back tomorrow.\n`);
189
+ } else if (e.code === 'ECONNREFUSED' || e.code === 'ENOTFOUND') {
190
+ console.log(`\n ${err('❌ couldnt connect')} — is ur server up? check the url in ur config (${dim('rage setup')} to edit)\n`);
191
+ } else {
192
+ console.log(`\n ${err('💀 error:')} ${e.message}\n`);
193
+ }
194
+ process.exit(1);
195
+ }
196
+ }
197
+
198
+ // ─── Stats command ────────────────────────────────────────────────────────────
199
+ async function cmdStats(cfg) {
200
+ if (!cfg.url) {
201
+ console.log(`\n${err('no url configured.')} run ${hl('rage setup')} first.\n`);
202
+ process.exit(1);
203
+ }
204
+ try {
205
+ const res = await rageApi.stats({ url: cfg.url });
206
+ logo();
207
+ console.log(` ${acc('📊 global stats')}\n`);
208
+ console.log(` total rages: ${hl(Number(res.total_rages).toLocaleString())}`);
209
+ console.log(` rages today: ${hl(Number(res.today_rages).toLocaleString())}`);
210
+ console.log(` total users: ${hl(Number(res.user_count).toLocaleString())}`);
211
+ console.log('');
212
+ } catch (e) {
213
+ console.log(`\n ${err('error:')} ${e.message}\n`);
214
+ process.exit(1);
215
+ }
216
+ }
217
+
218
+ // ─── Feed command ─────────────────────────────────────────────────────────────
219
+ async function cmdFeed(limit, cfg) {
220
+ if (!cfg.url) {
221
+ console.log(`\n${err('no url configured.')} run ${hl('rage setup')} first.\n`);
222
+ process.exit(1);
223
+ }
224
+ try {
225
+ const res = await rageApi.feed(limit, { url: cfg.url });
226
+ const feed = res.feed || [];
227
+ logo();
228
+ console.log(` ${acc('🔴 live rage feed')} ${dim(`(last ${feed.length})`)} \n`);
229
+
230
+ if (!feed.length) {
231
+ console.log(` ${dim('no rages yet... wild.')}`);
232
+ } else {
233
+ feed.forEach(item => {
234
+ const user = item.user === 'Anonymous Gremlin' ? dim('anonymous gremlin') : acc(item.user);
235
+ const time = dim(` [${item.time_ago}]`);
236
+ const reason = item.reason ? ` ${c.yellow}"${item.reason}"${c.reset}` : '';
237
+ console.log(` ${user}${time}`);
238
+ if (reason) console.log(` ${reason}`);
239
+ console.log('');
240
+ });
241
+ }
242
+ } catch (e) {
243
+ console.log(`\n ${err('error:')} ${e.message}\n`);
244
+ process.exit(1);
245
+ }
246
+ }
247
+
248
+ // ─── Help ─────────────────────────────────────────────────────────────────────
249
+ function showHelp() {
250
+ logo();
251
+ console.log(` ${acc('Commands')}\n`);
252
+ console.log(` ${hl('rage')} → register rage (asks reason interactively)`);
253
+ console.log(` ${hl('rage setup')} → configure server url + api key`);
254
+ console.log(` ${hl('rage stats')} → global stats`);
255
+ console.log(` ${hl('rage feed')} → live rage feed`);
256
+ console.log(` ${hl('rage "reason here"')} → rage with inline reason`);
257
+ console.log(` ${hl('rage --reason "msg"')} → rage with reason flag`);
258
+ console.log(` ${hl('rage --no-prompt')} → rage silently (no reason prompt)`);
259
+ console.log(` ${hl('rage --version')} → show version`);
260
+ console.log(` ${hl('rage --help')} → this menu`);
261
+ console.log('');
262
+ console.log(` ${dim('config stored at:')} ${CONFIG_PATH}`);
263
+ console.log('');
264
+ }
265
+
266
+ // ─── Main ─────────────────────────────────────────────────────────────────────
267
+ async function main() {
268
+ const args = process.argv.slice(2);
269
+ const cfg = loadConfig();
270
+
271
+ // flags
272
+ const reasonFlag = args.find((a, i) => a === '--reason' && args[i + 1]);
273
+ const reasonVal = reasonFlag ? args[args.indexOf('--reason') + 1] : undefined;
274
+ const noPrompt = args.includes('--no-prompt') || args.includes('--silent') || args.includes('-s');
275
+ const helpFlag = args.includes('--help') || args.includes('-h');
276
+ const versionFlag = args.includes('--version') || args.includes('-v');
277
+
278
+ if (versionFlag) { console.log(`RAGEBUTTONAPI™ CLI v${VERSION}`); process.exit(0); }
279
+ if (helpFlag) { showHelp(); process.exit(0); }
280
+
281
+ const subcommand = args.find(a => !a.startsWith('-'));
282
+
283
+ if (subcommand === 'setup') { await cmdSetup(cfg); return; }
284
+ if (subcommand === 'stats') { await cmdStats(cfg); return; }
285
+ if (subcommand === 'feed') {
286
+ const limit = parseInt(args[args.indexOf('feed') + 1]) || 10;
287
+ await cmdFeed(limit, cfg);
288
+ return;
289
+ }
290
+
291
+ // default: rage
292
+ // inline reason: first non-flag, non-subcommand arg
293
+ const inlineReason = args.find(a => !a.startsWith('-') && a !== 'rage');
294
+
295
+ let reason;
296
+ if (reasonVal !== undefined) { reason = reasonVal; } // --reason "..."
297
+ else if (inlineReason !== undefined) { reason = inlineReason; } // rage "inline"
298
+ else if (noPrompt) { reason = ''; } // --no-prompt
299
+ else { reason = null; } // ask interactively
300
+
301
+ await cmdRage(reason, cfg);
302
+ }
303
+
304
+ main().catch(e => {
305
+ console.error(`\n${err('unexpected error:')} ${e.message}\n`);
306
+ process.exit(1);
307
+ });
package/index.js ADDED
@@ -0,0 +1,145 @@
1
+ /**
2
+ * RAGEBUTTONAPI™ — Official Node.js / npm Library
3
+ *
4
+ * Usage:
5
+ * const rageApi = require('ragebuttonapi');
6
+ *
7
+ * // one-time init
8
+ * rageApi.init({ url: 'https://ragebutton.yoursite.com', apiKey: 'rbapi_xxx' });
9
+ *
10
+ * // register rage
11
+ * await rageApi.rage('this stupid bug omg');
12
+ *
13
+ * // or inline config
14
+ * await rageApi.rage('why', { url: '...', apiKey: '...' });
15
+ *
16
+ * // get global stats
17
+ * const stats = await rageApi.stats();
18
+ *
19
+ * // get live feed
20
+ * const feed = await rageApi.feed(5);
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ const https = require('https');
26
+ const http = require('http');
27
+ const url = require('url');
28
+
29
+ let _globalConfig = { url: null, apiKey: null };
30
+
31
+ // ─── low-level request helper ────────────────────────────────────────────────
32
+ function request(endpoint, { method = 'GET', body = null } = {}) {
33
+ return new Promise((resolve, reject) => {
34
+ const parsed = url.parse(endpoint);
35
+ const isHttps = parsed.protocol === 'https:';
36
+ const lib = isHttps ? https : http;
37
+
38
+ const payload = body ? Object.entries(body)
39
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v ?? '')}`)
40
+ .join('&') : null;
41
+
42
+ const opts = {
43
+ hostname: parsed.hostname,
44
+ port : parsed.port || (isHttps ? 443 : 80),
45
+ path : parsed.path,
46
+ method,
47
+ headers : {
48
+ 'User-Agent': 'RAGEBUTTONAPI-npm/1.0',
49
+ ...(payload ? {
50
+ 'Content-Type' : 'application/x-www-form-urlencoded',
51
+ 'Content-Length': Buffer.byteLength(payload),
52
+ } : {}),
53
+ },
54
+ };
55
+
56
+ const req = lib.request(opts, res => {
57
+ let data = '';
58
+ res.on('data', c => { data += c; });
59
+ res.on('end', () => {
60
+ try {
61
+ const json = JSON.parse(data);
62
+ if (!json.ok && json.error) {
63
+ const err = new Error(json.error);
64
+ err.data = json;
65
+ err.code = json.reason || 'api_error';
66
+ reject(err);
67
+ } else {
68
+ resolve(json);
69
+ }
70
+ } catch {
71
+ reject(new Error(`bad response from server (status ${res.statusCode}). is ur server up?`));
72
+ }
73
+ });
74
+ });
75
+
76
+ req.on('error', reject);
77
+ if (payload) req.write(payload);
78
+ req.end();
79
+ });
80
+ }
81
+
82
+ // ─── resolve config, merging inline overrides ────────────────────────────────
83
+ function resolveConfig(inline = {}) {
84
+ const cfg = {
85
+ url : inline.url ?? _globalConfig.url,
86
+ apiKey: inline.apiKey ?? _globalConfig.apiKey,
87
+ };
88
+ if (!cfg.url) throw new Error('ragebuttonapi: server url is required. call rageApi.init({ url }) or pass it inline.');
89
+ if (!cfg.apiKey) throw new Error('ragebuttonapi: apiKey is required. get it from ur dashboard.');
90
+ return { ...cfg, url: cfg.url.replace(/\/$/, '') };
91
+ }
92
+
93
+ // ─── Public API ──────────────────────────────────────────────────────────────
94
+
95
+ /**
96
+ * One-time global config. call this at app startup.
97
+ * @param {{ url: string, apiKey: string }} config
98
+ */
99
+ function init(config = {}) {
100
+ if (config.url) _globalConfig.url = config.url.replace(/\/$/, '');
101
+ if (config.apiKey) _globalConfig.apiKey = config.apiKey;
102
+ }
103
+
104
+ /**
105
+ * Register a rage. returns the API response with ur new stats.
106
+ * @param {string} [reason] - optional, 255 char max
107
+ * @param {{ url?, apiKey?, source? }} [config] - inline config override
108
+ * @returns {Promise<{ok, total_rages, your_total, your_streak, rank, new_achievements, ...}>}
109
+ */
110
+ async function rage(reason, config = {}) {
111
+ const { url: serverUrl, apiKey } = resolveConfig(config);
112
+ return request(`${serverUrl}/api/rage.php`, {
113
+ method: 'POST',
114
+ body : {
115
+ api_key: apiKey,
116
+ reason : reason ?? '',
117
+ source : config.source ?? 'npm',
118
+ },
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Get global stats (no auth required).
124
+ * @param {{ url? }} [config]
125
+ * @returns {Promise<{total_rages, today_rages, user_count}>}
126
+ */
127
+ async function stats(config = {}) {
128
+ const serverUrl = config.url ?? _globalConfig.url;
129
+ if (!serverUrl) throw new Error('ragebuttonapi: url is required for stats()');
130
+ return request(`${serverUrl.replace(/\/$/, '')}/api/stats.php`);
131
+ }
132
+
133
+ /**
134
+ * Get the live rage feed (no auth required).
135
+ * @param {number} [limit=10]
136
+ * @param {{ url? }} [config]
137
+ * @returns {Promise<{ feed: Array<{user, reason, time_ago}> }>}
138
+ */
139
+ async function feed(limit = 10, config = {}) {
140
+ const serverUrl = config.url ?? _globalConfig.url;
141
+ if (!serverUrl) throw new Error('ragebuttonapi: url is required for feed()');
142
+ return request(`${serverUrl.replace(/\/$/, '')}/api/feed.php?limit=${limit}`);
143
+ }
144
+
145
+ module.exports = { init, rage, stats, feed };
package/npm-cli.tar.gz ADDED
Binary file
package/npm-cli.zip ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "ragebuttonapi",
3
+ "version": "1.0.0",
4
+ "description": "official npm package + cli for RAGEBUTTONAPI™ — register developer rage from ur terminal or node app. no account needed for anon rages.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "rage": "cli.js",
8
+ "npm-cli": "cli.js",
9
+ "npmc": "cli.js"
10
+ },
11
+ "scripts": {
12
+ "test": "node cli.js --help"
13
+ },
14
+ "keywords": [
15
+ "rage",
16
+ "developer",
17
+ "api",
18
+ "cli",
19
+ "fun",
20
+ "mental-health",
21
+ "programming",
22
+ "anger"
23
+ ],
24
+ "author": "RAGEBUTTONAPI™",
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=16.0.0"
28
+ }
29
+ }