onenotif 0.1.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/LICENSE +21 -0
- package/Notifier.js +72 -0
- package/README.md +90 -0
- package/cli.js +107 -0
- package/package.json +52 -0
- package/sources/BaseSource.js +32 -0
- package/sources/TelegramSource.js +65 -0
- package/sources/WebhookSource.js +57 -0
- package/sources/index.js +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Notifier.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export class NotifierUI {
|
|
4
|
+
constructor(config = {}) {
|
|
5
|
+
this.notifications = [];
|
|
6
|
+
this.maxNotifications = config.maxNotifications || 20;
|
|
7
|
+
this.separator = '─'.repeat(60);
|
|
8
|
+
this.showWelcome();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
showWelcome() {
|
|
12
|
+
console.clear();
|
|
13
|
+
console.log(chalk.cyan.bold('╔════════════════════════════════════════════════════════════╗'));
|
|
14
|
+
console.log(chalk.cyan.bold('║') + ' ' + chalk.white.bold('OneNotif') + ' - Notification Viewer ' + chalk.cyan.bold('║'));
|
|
15
|
+
console.log(chalk.cyan.bold('╚════════════════════════════════════════════════════════════╝'));
|
|
16
|
+
console.log(chalk.gray('Waiting for notifications...\n'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
formatTime(date) {
|
|
20
|
+
return date.toLocaleTimeString('en-US', { hour12: false });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
display(notification) {
|
|
24
|
+
const isNew = !this.notifications.includes(notification);
|
|
25
|
+
|
|
26
|
+
// Add to history
|
|
27
|
+
this.notifications.push(notification);
|
|
28
|
+
if (this.notifications.length > this.maxNotifications) {
|
|
29
|
+
this.notifications.shift();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log(chalk.gray(this.separator));
|
|
34
|
+
|
|
35
|
+
// Source badge
|
|
36
|
+
const sourceBadge = this.getSourceColor(notification.source);
|
|
37
|
+
const timeStr = this.formatTime(notification.timestamp);
|
|
38
|
+
console.log(sourceBadge(' ' + notification.source + ' ') + chalk.gray(' · ') + chalk.gray(timeStr));
|
|
39
|
+
|
|
40
|
+
// Title
|
|
41
|
+
if (notification.title) {
|
|
42
|
+
console.log(chalk.white.bold(' ' + notification.title));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Content
|
|
46
|
+
if (notification.content) {
|
|
47
|
+
console.log(chalk.gray(' ' + notification.content));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// From info
|
|
51
|
+
if (notification.from) {
|
|
52
|
+
console.log(chalk.dim(' from: @' + notification.from));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(chalk.gray(this.separator));
|
|
56
|
+
|
|
57
|
+
// If it's a status message, show differently
|
|
58
|
+
if (notification.type === 'status') {
|
|
59
|
+
console.log(chalk.cyan(' ' + notification.content || notification.message));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getSourceColor(source) {
|
|
64
|
+
const colors = {
|
|
65
|
+
'Telegram': chalk.bgRed,
|
|
66
|
+
'Webhook': chalk.bgBlue,
|
|
67
|
+
'Email': chalk.bgGreen,
|
|
68
|
+
'System': chalk.bgYellow
|
|
69
|
+
};
|
|
70
|
+
return colors[source] || chalk.bgGray;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# OneNotif
|
|
2
|
+
|
|
3
|
+
Simple terminal notification viewer for Telegram and other sources.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g onenotif
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 18 or higher.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
1. Create a Telegram bot:
|
|
16
|
+
- Open Telegram and search for @BotFather
|
|
17
|
+
- Send `/newbot` and follow the instructions
|
|
18
|
+
- Copy the bot token
|
|
19
|
+
|
|
20
|
+
2. Get your Telegram ID:
|
|
21
|
+
- Search for @userinfobot in Telegram
|
|
22
|
+
- It will show your user ID
|
|
23
|
+
|
|
24
|
+
3. Configure:
|
|
25
|
+
|
|
26
|
+
Create a config file at `~/.onenotif/config.json`:
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"sources": {
|
|
30
|
+
"telegram": {
|
|
31
|
+
"enabled": true,
|
|
32
|
+
"botToken": "YOUR_BOT_TOKEN",
|
|
33
|
+
"allowedUsers": ["your_telegram_id"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or set environment variable:
|
|
40
|
+
```bash
|
|
41
|
+
export ONENOTIF_TELEGRAM_TOKEN="your_bot_token"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
Start the app:
|
|
47
|
+
```bash
|
|
48
|
+
onenotif
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Send messages to your bot on Telegram - they'll appear in your terminal!
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
────────────────────────────────────────────────────────────
|
|
55
|
+
Telegram · 14:32:15
|
|
56
|
+
John
|
|
57
|
+
Hello from Telegram!
|
|
58
|
+
from @username
|
|
59
|
+
────────────────────────────────────────────────────────────
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/yourusername/onenotif.git
|
|
66
|
+
cd onenotif
|
|
67
|
+
npm install
|
|
68
|
+
npm start
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Adding More Sources
|
|
72
|
+
|
|
73
|
+
Create a new source class in `sources/` that extends `BaseSource`:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { BaseSource } from './BaseSource.js';
|
|
77
|
+
|
|
78
|
+
export class MySource extends BaseSource {
|
|
79
|
+
async start() {
|
|
80
|
+
// Your implementation
|
|
81
|
+
// Call this.emit({ title, content, timestamp }) when notification arrives
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then add it to `cli.js`.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
package/cli.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { config as loadEnv } from 'dotenv';
|
|
4
|
+
import { readFileSync, existsSync } from 'fs';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { TelegramSource } from './sources/TelegramSource.js';
|
|
10
|
+
import { WebhookSource } from './sources/WebhookSource.js';
|
|
11
|
+
import { NotifierUI } from './Notifier.js';
|
|
12
|
+
|
|
13
|
+
loadEnv();
|
|
14
|
+
|
|
15
|
+
// Get the directory where this script is located
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
// Load config - try multiple paths in order
|
|
19
|
+
let config = {
|
|
20
|
+
sources: {
|
|
21
|
+
telegram: { enabled: true },
|
|
22
|
+
webhook: { enabled: false }
|
|
23
|
+
},
|
|
24
|
+
display: { maxNotifications: 20 }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const configPaths = [
|
|
28
|
+
process.env.CONFIG_PATH,
|
|
29
|
+
join(homedir(), '.onenotif', 'config.json'),
|
|
30
|
+
join(__dirname, 'config.json'),
|
|
31
|
+
'./config.json'
|
|
32
|
+
].filter(Boolean);
|
|
33
|
+
|
|
34
|
+
for (const path of configPaths) {
|
|
35
|
+
if (existsSync(path)) {
|
|
36
|
+
try {
|
|
37
|
+
config = { ...config, ...JSON.parse(readFileSync(path, 'utf-8')) };
|
|
38
|
+
break;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
// Continue to next path
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Initialize UI
|
|
46
|
+
const ui = new NotifierUI(config.display);
|
|
47
|
+
const sources = [];
|
|
48
|
+
|
|
49
|
+
// Initialize Telegram
|
|
50
|
+
if (config.sources?.telegram?.enabled) {
|
|
51
|
+
const tgConfig = {
|
|
52
|
+
botToken: config.sources.telegram.botToken || process.env.ONENOTIF_TELEGRAM_TOKEN || process.env.TELEGRAM_BOT_TOKEN,
|
|
53
|
+
allowedUsers: config.sources.telegram.allowedUsers || []
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (tgConfig.botToken) {
|
|
57
|
+
const telegram = new TelegramSource(tgConfig);
|
|
58
|
+
telegram.onNotification((notif) => ui.display(notif));
|
|
59
|
+
sources.push(telegram);
|
|
60
|
+
} else {
|
|
61
|
+
ui.display({
|
|
62
|
+
source: 'System',
|
|
63
|
+
type: 'error',
|
|
64
|
+
title: 'Telegram',
|
|
65
|
+
content: 'Set TELEGRAM_BOT_TOKEN env var or add botToken to config.json',
|
|
66
|
+
timestamp: new Date()
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Initialize Webhook
|
|
72
|
+
if (config.sources?.webhook?.enabled) {
|
|
73
|
+
const webhook = new WebhookSource({
|
|
74
|
+
port: config.sources.webhook.port || 3000
|
|
75
|
+
});
|
|
76
|
+
webhook.onNotification((notif) => ui.display(notif));
|
|
77
|
+
sources.push(webhook);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Start all sources
|
|
81
|
+
console.log(chalk.gray('Starting notification sources...\n'));
|
|
82
|
+
|
|
83
|
+
for (const source of sources) {
|
|
84
|
+
try {
|
|
85
|
+
await source.start();
|
|
86
|
+
} catch (err) {
|
|
87
|
+
ui.display({
|
|
88
|
+
source: 'System',
|
|
89
|
+
type: 'error',
|
|
90
|
+
title: source.name,
|
|
91
|
+
content: err.message,
|
|
92
|
+
timestamp: new Date()
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Keep running
|
|
98
|
+
console.log(chalk.green('\n✓ All sources started. Press Ctrl+C to exit.\n'));
|
|
99
|
+
|
|
100
|
+
// Graceful shutdown
|
|
101
|
+
process.on('SIGINT', async () => {
|
|
102
|
+
console.log(chalk.yellow('\n\nShutting down...'));
|
|
103
|
+
for (const source of sources) {
|
|
104
|
+
await source.stop();
|
|
105
|
+
}
|
|
106
|
+
process.exit(0);
|
|
107
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "onenotif",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Simple terminal notification viewer for Telegram and other sources",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"onenotif": "./cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./cli.js",
|
|
12
|
+
"./sources": "./sources/index.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node cli.js",
|
|
16
|
+
"dev": "node --watch cli.js",
|
|
17
|
+
"test": "node cli.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"notification",
|
|
21
|
+
"telegram",
|
|
22
|
+
"terminal",
|
|
23
|
+
"cli",
|
|
24
|
+
"notifier",
|
|
25
|
+
"alerts",
|
|
26
|
+
"webhook"
|
|
27
|
+
],
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"cli.js",
|
|
35
|
+
"Notifier.js",
|
|
36
|
+
"sources/",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/techbruwh/onenotif"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/techbruwh/onenotif",
|
|
45
|
+
"bugs": "https://github.com/techbruwh/onenotif/issues",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"dotenv": "^16.4.5",
|
|
48
|
+
"grammy": "^1.21.1",
|
|
49
|
+
"chalk": "^5.3.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {}
|
|
52
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Base class for notification sources
|
|
2
|
+
export class BaseSource {
|
|
3
|
+
constructor(name, config) {
|
|
4
|
+
this.name = name;
|
|
5
|
+
this.config = config;
|
|
6
|
+
this.callbacks = [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Override this in subclasses
|
|
10
|
+
async start() {
|
|
11
|
+
throw new Error(`${this.name}: start() not implemented`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async stop() {
|
|
15
|
+
// Override if needed
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Register callback for new notifications
|
|
19
|
+
onNotification(callback) {
|
|
20
|
+
this.callbacks.push(callback);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Notify all listeners
|
|
24
|
+
emit(notification) {
|
|
25
|
+
for (const cb of this.callbacks) {
|
|
26
|
+
cb({
|
|
27
|
+
source: this.name,
|
|
28
|
+
...notification
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Bot } from 'grammy';
|
|
2
|
+
import { BaseSource } from './BaseSource.js';
|
|
3
|
+
|
|
4
|
+
export class TelegramSource extends BaseSource {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
super('Telegram', config);
|
|
7
|
+
this.bot = null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async start() {
|
|
11
|
+
if (!this.config.botToken) {
|
|
12
|
+
console.error('Telegram: No botToken provided');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.bot = new Bot(this.config.botToken);
|
|
17
|
+
|
|
18
|
+
this.bot.command('start', (ctx) => this.handleStart(ctx));
|
|
19
|
+
this.bot.command('help', (ctx) => this.handleHelp(ctx));
|
|
20
|
+
|
|
21
|
+
// Listen for any text message
|
|
22
|
+
this.bot.on('message:text', (ctx) => this.handleMessage(ctx));
|
|
23
|
+
|
|
24
|
+
// Start polling
|
|
25
|
+
await this.bot.start();
|
|
26
|
+
this.emit({ type: 'status', message: '✅ Telegram bot started' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
handleStart(ctx) {
|
|
30
|
+
ctx.reply('Welcome to OneNotif! Send me any message and it will appear in your terminal.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
handleHelp(ctx) {
|
|
34
|
+
ctx.reply('Commands: /start, /help. Any message you send will be shown in the terminal.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
handleMessage(ctx) {
|
|
38
|
+
const allowedUsers = this.config.allowedUsers || [];
|
|
39
|
+
|
|
40
|
+
// Check if user is allowed
|
|
41
|
+
if (allowedUsers.length > 0 && !allowedUsers.includes(String(ctx.from.id))) {
|
|
42
|
+
ctx.reply('Sorry, you are not authorized to use this bot.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const notification = {
|
|
47
|
+
type: 'message',
|
|
48
|
+
title: ctx.from.first_name || ctx.from.username || 'User',
|
|
49
|
+
content: ctx.message.text,
|
|
50
|
+
timestamp: new Date(),
|
|
51
|
+
from: ctx.from.username || ctx.from.id
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.emit(notification);
|
|
55
|
+
|
|
56
|
+
// Send read receipt (optional)
|
|
57
|
+
// ctx.reply('📨 Notification received!');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async stop() {
|
|
61
|
+
if (this.bot) {
|
|
62
|
+
await this.bot.stop();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { BaseSource } from './BaseSource.js';
|
|
2
|
+
|
|
3
|
+
export class WebhookSource extends BaseSource {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
super('Webhook', config);
|
|
6
|
+
this.server = null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async start() {
|
|
10
|
+
const http = await import('http');
|
|
11
|
+
|
|
12
|
+
this.server = http.createServer((req, res) => {
|
|
13
|
+
if (req.method === 'POST' && req.url === '/webhook') {
|
|
14
|
+
let body = '';
|
|
15
|
+
|
|
16
|
+
req.on('data', (chunk) => {
|
|
17
|
+
body += chunk.toString();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
req.on('end', () => {
|
|
21
|
+
try {
|
|
22
|
+
const data = JSON.parse(body);
|
|
23
|
+
|
|
24
|
+
this.emit({
|
|
25
|
+
type: 'webhook',
|
|
26
|
+
title: data.title || 'Webhook',
|
|
27
|
+
content: data.message || data.content || JSON.stringify(data),
|
|
28
|
+
timestamp: new Date()
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
32
|
+
res.end(JSON.stringify({ status: 'received' }));
|
|
33
|
+
} catch (err) {
|
|
34
|
+
res.writeHead(400);
|
|
35
|
+
res.end('Invalid JSON');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
res.writeHead(404);
|
|
40
|
+
res.end('Not found');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this.server.listen(this.config.port || 3000, () => {
|
|
45
|
+
this.emit({
|
|
46
|
+
type: 'status',
|
|
47
|
+
message: `✅ Webhook server listening on port ${this.config.port || 3000}`
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async stop() {
|
|
53
|
+
if (this.server) {
|
|
54
|
+
this.server.close();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/sources/index.js
ADDED