Skip to content
This repository was archived by the owner on Aug 25, 2023. It is now read-only.

Commit eb044f3

Browse files
committed
Reddit Live bot
1 parent c107eed commit eb044f3

File tree

9 files changed

+335
-10
lines changed

9 files changed

+335
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ build/Release
7676
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
7777
node_modules
7878
config.json
79+
profiles.json
7980

8081
# Test script
8182
test.js

.npmignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,6 @@ tests/
8585

8686
# Documentation
8787
docs/
88+
89+
# Examples
90+
examples/

examples/powercalcbot/bot.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
/* A Lightning Fast Multi-Purpose Calculator Bot.
22
3-
Dev: Mohammed Sohail
4-
Version: 1.1.0
5-
Demo: @powercalcbot
3+
Dev: Mohammed Sohail
4+
Version: 1.1.0
5+
Demo: @powercalcbot
66
7-
*/
7+
*/
88

99
const nodeogram = require('nodeogram'),
10-
bot = new nodeogram.Bot('token');
10+
config = require('./config.json'),
11+
bot = new nodeogram.Bot(config.token);
1112
const math = require('mathjs');
1213
const Keyboard = nodeogram.Keyboard;
1314

1415
var math2 = math.create({
15-
matrix: 'Array'
16+
matrix: 'Array'
1617
});
1718

1819
bot.init();
@@ -57,7 +58,7 @@ bot.command('tan', 'Returns the tangent of an angle. Usage /tan [num]', false, (
5758
});
5859

5960
bot.command('sin', 'Returns the sine of an angle. Usage /sin [num]', false, (args, message) => {
60-
message.reply(Math.sin(args[0]/180*Math.PI));
61+
message.reply(Math.sin(args[0]/180*Math.PI));
6162
});
6263

6364
bot.command('cos', 'Returns the cosine of an angle. Usage /cos [num]', false, (args, message) => {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"token" : "SOMETHING"
3+
}

examples/redditlivebot/app.js

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
'use strict';
2+
const nodeogram = require('nodeogram'),
3+
WebSocketClient = require('websocket').client,
4+
config = require('./config.json'),
5+
bot = new nodeogram.Bot(config.token, {profiles_path: __dirname + '/profiles.json', enableHelp: false}),
6+
request = require('superagent-promise')(require('superagent'), Promise);
7+
8+
var threads = {}, // thread_id: [user_id, ...]
9+
clients = {}, // thread_id: websocket_client
10+
messages = {}, // update_id: [message_id, ...]
11+
cache = {}; // thread_id: settings
12+
13+
bot.command('start', '', true, (args, message) => {
14+
if (args[0] != '') {
15+
updateUser(message.chat, args[0])
16+
} else {
17+
message.reply(`Hello there!\n\nI'm @redditlive_bot. I can fetch Reddit live threads and forward to you incoming messages, in order to keep you always updated.\n\nPlease use /follow to start following a thread. If any error occurs or you have a suggestion, you can query my owner @ALCC01 (https://albertocoscia.me).`)
18+
}
19+
});
20+
21+
bot.command('follow', 'Start following a thread', false, (args, message) => {
22+
if (args[0] != '') {
23+
updateUser(message.chat, args[0]);
24+
} else {
25+
fetchThreads().then((threads) => {
26+
var text = '❓ Please select one of the current hot threads from /r/live\n\n';
27+
var keyboard = new nodeogram.Keyboard();
28+
var i = 1;
29+
threads.forEach((thread) => {
30+
text += `${i}\u20E3 ${thread.title}\n`;
31+
var row = i > 8 ? 1 : 0;
32+
keyboard.addButton(row, row == 0 ? i - 1 : i - 9, {text: '' + (i), callback_data: JSON.stringify({id: thread.secure_media.event_id, action: 'follow'})});
33+
i++;
34+
});
35+
text += '\nYou can also use /follow [id] if you already have an id for a live thread';
36+
keyboard.toInline();
37+
message.reply(text, {reply_markup: keyboard});
38+
}).catch(() => {
39+
message.reply('An error occurred while executing this command!')
40+
});
41+
}
42+
});
43+
44+
bot.command('unfollow', 'Stop following a thread', false, (args, message) => {
45+
if (args[0] === '') {
46+
var keyboard = new nodeogram.Keyboard();
47+
var text = '❓ Which of these threads do you want to stop following?\n\n';
48+
var i = 1;
49+
for (var thread in threads) {
50+
if (threads.hasOwnProperty(thread) && threads[thread].includes(message.chat.id) && cache[thread]) {
51+
text += `${i}\u20E3 ${cache[thread].title}\n`;
52+
var row = i > 8 ? 1 : 0;
53+
keyboard.addButton(row, row == 0 ? i - 1 : i - 9, {text: '' + (i), callback_data: JSON.stringify({id: thread, action: 'unfollow'})});
54+
i++;
55+
}
56+
}
57+
text += '\nYou can also use /unfollow [id] if you already have an id for a live thread';
58+
keyboard.toInline();
59+
message.reply(text, {reply_markup: keyboard});
60+
} else {
61+
unfollow(message.chat, args[0])
62+
}
63+
});
64+
65+
bot.on('callback_query', (query) => {
66+
var data = JSON.parse(query.data);
67+
console.log(query);
68+
if (data.action === 'follow') {
69+
if (query.message) {
70+
updateUser(query.message.chat, data.id)
71+
query.answer('👍 Got it!')
72+
}
73+
}
74+
if (data.action === 'unfollow') {
75+
if (query.message) {
76+
unfollow(query.message.chat, data.id)
77+
query.answer('👍 Got it!')
78+
}
79+
}
80+
});
81+
82+
bot.init();
83+
bot.on('message', (message) => {
84+
85+
});
86+
87+
function fetchThreads() {
88+
return get('https://www.reddit.com/r/live/hot.json?limit=10', {}).then((res) => {
89+
var threads = [];
90+
res.body.data.children.forEach((thread) => {
91+
if (thread.data.secure_media && thread.data.secure_media.event_id) {
92+
threads.push(thread.data);
93+
}
94+
});
95+
return threads;
96+
}).catch(err => console.log(err))
97+
}
98+
99+
function updateUser(user, id) {
100+
if (!threads[id]) {
101+
threads[id] = [];
102+
} else {
103+
if (threads[id].indexOf(user.id) > -1) {
104+
user.sendMessage('⛔️ Looks like you\'re already following this thread!')
105+
}
106+
}
107+
108+
threads[id].push(user.id);
109+
get(`https://api.reddit.com/live/${id}/about.json`, {})
110+
.then(res => {
111+
var thread = parseThread(res),
112+
message = `*${thread.title}*\n_${thread.description}_\n\n${thread.online ? '📡 Online' : '🔇 Offline'} ${thread.nsfw ? '🔞 NSFW' : '✅ SFW'} ${thread.viewer_count ? '👥 ' + thread.viewer_count + ' viewers' : ''}\n📅 ${new Date(thread.created_utc * 1000).toUTCString()}\n\n🌐 https://reddit.com/live/${thread.id} 🆔 \`${thread.id}\``;
113+
user.sendMessage(message, {
114+
parse_mode: 'Markdown',
115+
disable_web_page_preview: true
116+
}).catch(err => console.log(err));
117+
user.sendMessage('*Resources* \n' + thread.resources, {
118+
parse_mode: 'Markdown',
119+
disable_web_page_preview: true
120+
}).catch(err => user.sendMessage(`I was unable to parse the formatting of the resources from this thread, I\'m sending you an unparsed version!\n\n${thread.resources}`, {disable_web_page_preview: true}));
121+
if (thread.online) {
122+
user.sendMessage('🔓 This thread is currently live, I\'ll start keeping you updated. Please use /unfollow if you don\'t want to receive any update from this thread');
123+
if (!clients[id]) {
124+
createClient(id, thread);
125+
}
126+
} else {
127+
user.sendMessage('🔒 Since this thread is offline at the moment I won\'t send you any update! :(');
128+
clearId(id)
129+
}
130+
}, err => {
131+
user.sendMessage('⛔️ An error occurred while fetching this thread, please try again later!')
132+
})
133+
}
134+
135+
function parseThread(res) {
136+
var obj = res.body.data;
137+
cache[obj.id] = obj;
138+
obj.online = obj.state === 'live';
139+
return obj;
140+
}
141+
142+
function createClient(id, thread) {
143+
var client = new WebSocketClient();
144+
client.on('connectionFailed', err => {
145+
console.log(err);
146+
});
147+
148+
client.on('connect', connection => {
149+
console.log(`Connection established for thread ${id}`);
150+
151+
connection.on('error', err => {
152+
// We don't know for sure if the socket is still in a clean state, so we restart it
153+
console.log(err);
154+
console.log(`Restarting websocket for thread ${id} due to an error`);
155+
createClient(id, thread)
156+
});
157+
158+
connection.on('close', (code) => {
159+
if (code === 1000) {
160+
console.log(`Websocket for thread ${id} was closed with code 1000`);
161+
clearId(id);
162+
return;
163+
}
164+
// This may be due to the address expiration
165+
console.log(`Fetching a new websocket URL for thread ${id}`);
166+
get(`https://api.reddit.com/live/${id}/about.json`, {})
167+
.then(res => {
168+
thread = parseThread(res);
169+
if (thread.online) {
170+
console.log(`Restarting websocket for thread ${id} due to a connection closure`);
171+
createClient(id, thread)
172+
} else {
173+
console.log(`Websocket for thread ${id} went offline along with its thread`);
174+
clearId(id);
175+
}
176+
})
177+
.catch(err => {
178+
console.log(`Could not fetch a new URL for thread ${id}`);
179+
console.log(err)
180+
})
181+
});
182+
183+
connection.on('message', message => {
184+
console.log(message);
185+
if (message.type === 'utf8') {
186+
message = JSON.parse(message.utf8Data);
187+
switch (message.type) {
188+
case 'activity':
189+
case 'embeds_ready':
190+
// We're ignoring them atm
191+
break;
192+
case 'settings':
193+
if (cache[id]) {
194+
for (var key in message.payload) {
195+
if (message.payload.hasOwnProperty(key)) {
196+
cache[id][key] = message.payload[key];
197+
}
198+
}
199+
}
200+
break;
201+
case 'update':
202+
if (message.payload.kind == 'LiveUpdate') {
203+
streamToUsers(id, `Update from 🗣 /u/${message.payload.data.author} 📢 ${thread.title}\n\n${message.payload.data.body}`, message.payload.data.name, {parse_mode: 'Markdown'});
204+
}
205+
break;
206+
case 'delete':
207+
remove(message.payload.data);
208+
break;
209+
case 'strike':
210+
strikeToUsers(message.payload.data);
211+
break;
212+
case 'complete':
213+
streamToUsers(id, `🔒 This thread has been marked as complete. It is now offline and you will no longer receive any update about it.`);
214+
clearId(id);
215+
connection.close(1000);
216+
break;
217+
}
218+
}
219+
})
220+
});
221+
222+
client.connect(thread.websocket_url/*'ws://localhost:8080/'*/);
223+
clients[id] = client;
224+
}
225+
226+
function clearId(id) {
227+
delete threads[id];
228+
delete clients[id];
229+
delete cache[id];
230+
}
231+
232+
function streamToUsers(id, message, update_id, options) {
233+
if (threads[id]) {
234+
threads[id].forEach(user => {
235+
options = options ? options : {};
236+
if (update_id) messages[update_id] = [];
237+
bot.sendMessage(user, message, options)
238+
.then(message => {
239+
if (update_id) {
240+
messages[update_id].push(message);
241+
}
242+
})
243+
.catch(() => {
244+
options.parse_mode = undefined;
245+
bot.sendMessage(user, message, options).then(message => {
246+
if (update_id) {
247+
messages[update_id].push(message);
248+
}
249+
});
250+
})
251+
})
252+
}
253+
}
254+
255+
function strikeToUsers(update_id) {
256+
if (messages[update_id]) {
257+
messages[update_id].forEach(message => {
258+
message.editText('☢ This update has been marked as incorrect ☢\n' + message.text, false, {parse_mode: 'Markdown'})
259+
.catch(() => {
260+
message.editText('☢ This update has been marked as incorrect ☢\n' + message.text, false, {parse_mode: undefined})
261+
})
262+
});
263+
messages[update_id] = [];
264+
}
265+
}
266+
267+
function remove(update_id) {
268+
if (messages[update_id]) {
269+
messages[update_id].forEach(message => {
270+
message.editText('📛 *This update has been removed!* 📛', false, {parse_mode: 'Markdown'})
271+
.catch(() => {
272+
message.editText('📛 *This update has been removed!* 📛' + message.text, false, {parse_mode: undefined})
273+
})
274+
});
275+
messages[update_id] = [];
276+
}
277+
}
278+
279+
function unfollow(user, id) {
280+
if (threads[id] && threads[id].indexOf(user.id) > -1) {
281+
threads[id].splice(threads[id].indexOf(user.id), 1);
282+
bot.sendMessage(user.id, '💔 You successfully stopped following this thread.')
283+
} else {
284+
bot.sendMessage(user.id, '🦄 Looks like you\'re not following this thread or this thread does not exist.')
285+
}
286+
}
287+
288+
function get(path, query) {
289+
return request
290+
.get(path)
291+
.set('User-Agent', 'nodejs:redditlivebot:1.0.0 (by /u/alcc01)')
292+
.query(query)
293+
.end()
294+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"token": "SOMETHING"
3+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "redditlivebot",
3+
"version": "1.0.0",
4+
"description": "A Telegram bot that fetches and forwards updates from Reddit live threads",
5+
"main": "app.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"start": "node app.js"
9+
},
10+
"author": "Alberto Coscia",
11+
"license": "GPL-3.0",
12+
"dependencies": {
13+
"nodeogram": "0.0.3",
14+
"superagent": "^2.1.0",
15+
"superagent-promise": "^1.1.0",
16+
"websocket": "^1.0.23"
17+
}
18+
}

examples/thingsbot/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
"description": "",
55
"main": "thingsbot.js",
66
"scripts": {
7-
"test": "echo \"Error: no test specified\" && exit 1"
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"start": "node thingsbot.js"
89
},
910
"author": "Alberto Coscia",
1011
"license": "GPL-3.0",
1112
"dependencies": {
13+
"nodeogram": "0.0.3",
1214
"superagent": "^1.8.3"
1315
}
1416
}

0 commit comments

Comments
 (0)