charlesv2 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.

Potentially problematic release.


This version of charlesv2 might be problematic. Click here for more details.

@@ -0,0 +1,215 @@
1
+ import { existsSync, promises } from "fs"
2
+ import puppeteer from "puppeteer-extra"
3
+ import StealthPlugin from 'puppeteer-extra-plugin-stealth'
4
+ import { getZone } from "../getZone.js"
5
+ import { prettyString } from "./prettyString.js"
6
+ import { editPayload } from "./editPayload.js"
7
+ import ora from "ora"
8
+
9
+ puppeteer.use(StealthPlugin())
10
+
11
+
12
+ const wiggy = async (page) => {
13
+ const viewport = await page.viewport()|| { width: 1280, height: 800 }
14
+ const maxX = await viewport.width
15
+ const maxY = await viewport.height
16
+ const x = Math.floor(Math.random() * maxX);
17
+ const y = Math.floor(Math.random() * maxY);
18
+
19
+ // Move the mouse with a smooth transition
20
+ await page.mouse.move(x, y, { steps: 10 });
21
+ }
22
+
23
+
24
+
25
+ const waitForE2ee = async (e2ee, page) => {
26
+ let pinBox = null
27
+ try {
28
+ pinBox = await page.waitForSelector('input[id="mw-numeric-code-input-prevent-composer-focus-steal"]', {
29
+ timeout: 5000
30
+ })
31
+ // eslint-disable-next-line no-unused-vars
32
+ } catch (err) {
33
+ await sleep(500)
34
+ }
35
+ if (pinBox) {
36
+ await pinBox.type(e2ee, { delay: 250 })
37
+ }
38
+
39
+ }
40
+
41
+ const goToChat = async (allTheChats, page, id) => {
42
+ if (page.url().includes(id)) {
43
+ return
44
+ }
45
+ let chatFound = false
46
+
47
+ for (const chat of allTheChats) {
48
+ if (chat.id === id.toString()) {
49
+ chatFound = true
50
+ await page.evaluate( async (href) => {
51
+ const element = document.querySelector(`a[href="${href}"]`);
52
+ if (element) {
53
+ element.scrollIntoView({ behavior: 'smooth', block: 'center' });
54
+ await element.click()
55
+ }
56
+
57
+ }, chat.href);
58
+ }
59
+ }
60
+ if (!chatFound) {
61
+ await page.goto(`https://www.messenger.com/t/${id}`)
62
+ await page.waitForNetworkIdle()
63
+ }
64
+ }
65
+
66
+
67
+ const login = async (page, mail, pass) => {
68
+ // logs into facebook messenger
69
+ await page.goto('https://messenger.com/', { waitUntil: 'networkidle2'})
70
+
71
+ // Get elements
72
+ const mailBox = await page.waitForSelector('input[id="email"]')
73
+ const passBox = await page.waitForSelector('input[id="pass"]')
74
+ const button = await page.waitForSelector('button[id="loginbutton"]')
75
+
76
+ // Send in data
77
+ await mailBox.type(mail, { delay: 250 })
78
+ await passBox.type(pass, { delay: 375 })
79
+ await button.click()
80
+ await page.waitForNavigation()
81
+ }
82
+
83
+ const saveCookies = async (browser) => {
84
+ // Goes and smuggles cookies
85
+ const cookies = await browser.cookies()
86
+ await promises.writeFile('resources/messenger.json', JSON.stringify(cookies, null, 4))
87
+ }
88
+
89
+ const loadCookies = async (browser, page) => {
90
+ await page.goto('https://messenger.com/')
91
+ let cookies
92
+ try {
93
+ cookies = await JSON.parse( await promises.readFile('resources/messenger.json'))
94
+ // eslint-disable-next-line no-unused-vars
95
+ } catch(e) {
96
+ return
97
+ }
98
+
99
+ for (const cookie of cookies) {
100
+ browser.setCookie(cookie)
101
+ }
102
+
103
+ await page.reload()
104
+ }
105
+
106
+ const sleep = (ms) => {
107
+ return new Promise(resolve => setTimeout(resolve, ms));
108
+ }
109
+
110
+ export const sneakyFacebook = async (testId=null, headless=true, e2ee) => {
111
+ let spool = ora('Booting up Charles').start()
112
+ // Set up environment (is that how it's spelt?)
113
+ const browser = await puppeteer.launch({
114
+ headless: headless, // toggle if you want to see the browser
115
+ args: [
116
+ '--disable-infobars',
117
+ '--start-maximized',
118
+ '--disable-extensions',
119
+ '--window-size=1920,1080',
120
+ '--disable-gpu',
121
+ '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'
122
+ ],
123
+ })
124
+ const page = await browser.newPage()
125
+ // FB user and password
126
+ spool.text = 'Sequestering your identification'
127
+ spool.color = 'blue'
128
+ const userObj = await JSON.parse( await promises.readFile('resources/config.json'))
129
+ const username = userObj.botname
130
+ const password = userObj.botpassword
131
+
132
+
133
+ // Try to snag cookies and skip login
134
+ await loadCookies(browser, page)
135
+ const mailBox = await page.waitForSelector('input[id="email"]', { timeout: 5000 }).catch(() => null);
136
+ if (mailBox) {
137
+ await login(page, username, password)
138
+ await saveCookies(browser)
139
+ }
140
+ spool.text = "I'm totally a human, Facebook, TRUST ME!"
141
+ spool.color = 'red'
142
+
143
+
144
+
145
+
146
+ await waitForE2ee(e2ee, page)
147
+
148
+ let zones
149
+
150
+ // Go through each chat and zone and do cool stuff
151
+ if (existsSync('resources/charlesConfig.json')) {
152
+ zones = await promises.readFile('resources/charlesConfig.json')
153
+ .then(JSON.parse)
154
+ } else {
155
+ zones = await getZone()
156
+ }
157
+
158
+ let reformattedPayload = await editPayload()
159
+
160
+ let intervalId = setInterval(() => wiggy(page), 5000);
161
+ spool.succeed(' Everything is spick and span')
162
+ // Go through list of zones and send a message to each
163
+ delete zones?.Dothan
164
+
165
+ for (const zone in zones) {
166
+ let waitingSpool = ora(`Storming the castle`).start()
167
+
168
+ const allTheChats = await page.$$eval(
169
+ 'a[class="x1i10hfl x1qjc9v5 xjbqb8w xjqpnuy xa49m3k xqeqjp1 x2hbi6w x13fuv20 xu3j5b3 x1q0q8m5 x26u7qi x972fbf xcfux6l x1qhh985 xm0m39n x9f619 x1ypdohk xdl72j9 x2lah0s xe8uvvx x2lwn1j xeuugli xexx8yu x4uap5 x18d9i69 xkhd6sd x1n2onr6 x16tdsg8 x1hl2dhg xggy1nq x1ja2u2z x1t137rt x1q0g3np x87ps6o x1lku1pv x1a2a7pz x1lq5wgf xgqcy7u x30kzoy x9jhf4c xdj266r x11i5rnm xat24cr x1mh8g0r x78zum5"]',
170
+ (elements) => {
171
+ return elements.map((item) => {
172
+ const href = item.getAttribute('href');
173
+ return {
174
+ id: href.match(/(\d+)\/?$/)?.[1],
175
+ href: href
176
+ };
177
+ });
178
+ }
179
+ );
180
+
181
+ waitingSpool.text = `Sending a message to ${zone}`
182
+ waitingSpool.color = 'magenta'
183
+
184
+ const message = await prettyString(zone, reformattedPayload[zone], reformattedPayload["avg"])
185
+ // bar.start(message.length, 0)
186
+
187
+ // So originally the problem was not
188
+ // appearing to be human, that's why we have
189
+ // humantype and other things like that.
190
+ // We've discovered that you can just
191
+ // copy and paste and that's human
192
+ // enough. If you try to change this, remember
193
+ // that you have to be human! :)
194
+
195
+ await goToChat(allTheChats, page, testId ? testId : zones[zone])
196
+
197
+ await sleep(5000)
198
+ await page.evaluate((message) => {
199
+ navigator.clipboard.writeText(message);
200
+ }, message)
201
+ await page.focus('div[role="textbox"]')
202
+ // await humanType(page, boxy, message, bar)
203
+ await sleep(Math.floor(Math.random() * 500) + 1)
204
+ await page.keyboard.down('Control');
205
+ await page.keyboard.press('KeyV');
206
+ await page.keyboard.up('Control');
207
+ await sleep(Math.floor(Math.random() * 500) + 1)
208
+ const button = await page.waitForSelector('div[aria-label="Press enter to send"]')
209
+ await button.click()
210
+ await sleep(5000)
211
+ waitingSpool.succeed(`Message sent to ${zone}`)
212
+ }
213
+ clearInterval(intervalId)
214
+ await browser.close()
215
+ }
@@ -0,0 +1,46 @@
1
+ import chalk from "chalk";
2
+ import { promises } from "fs";
3
+ import prompts from "prompts";
4
+
5
+
6
+ export async function createConfig(configPath) {
7
+
8
+ const questions = [
9
+ {
10
+ type: "text",
11
+ name: "username",
12
+ message: "What is your churchofjesuschrist.org username?",
13
+ },
14
+ {
15
+ type: "password",
16
+ name: "password",
17
+ message: "What is your password?",
18
+ },
19
+ {
20
+ type: "text",
21
+ name: "botname",
22
+ message: "What email is your bot registered under?",
23
+ },
24
+ {
25
+ type: "password",
26
+ name: "botpassword",
27
+ message: "What is the password for that Facebook account?",
28
+ },
29
+ {
30
+ type: "toggle",
31
+ name: "save",
32
+ message: "Would you like to save this information for future logins (recommended)?",
33
+ initial: true,
34
+ active: "yes",
35
+ inactive: "no",
36
+ },
37
+ ];
38
+ console.log(chalk.dim("Setting up Charles"));
39
+ const response = await prompts(questions);
40
+ if (response.save) {
41
+ await promises.writeFile(configPath, JSON.stringify(response));
42
+ return response;
43
+ } else {
44
+ return response;
45
+ }
46
+ }
@@ -0,0 +1,54 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2
+ import { configCharles } from './configCharles.js';
3
+
4
+ function isMoreThan12HoursOld(timestamp) {
5
+ const TWELVE_HOURS_MS = 12 * 60 * 60 * 1000;
6
+ return (Date.now() - timestamp) > TWELVE_HOURS_MS;
7
+ }
8
+
9
+ export async function createPayload(list, avgMessage) {
10
+ const FILE_NAME = 'payload.json';
11
+
12
+ if (existsSync(FILE_NAME)) {
13
+ try {
14
+ const oldpay = JSON.parse(readFileSync(FILE_NAME, 'utf-8'));
15
+ if (!isMoreThan12HoursOld(oldpay.stamp)) {
16
+ return oldpay;
17
+ }
18
+ } catch (error) {
19
+ console.error(`Error reading ${FILE_NAME}:`, error);
20
+ }
21
+ }
22
+
23
+ let zoneList;
24
+ try {
25
+ zoneList = await configCharles('./resources/charlesConfig.json')
26
+ } catch (error) {
27
+ console.error("Error fetching zone data:", error);
28
+ return null; // Return `null` or throw an error if needed
29
+ }
30
+
31
+ delete zoneList?.Dorthan
32
+ let payload = { stamp: Date.now(), average: avgMessage, payload: {} }
33
+ const zones = Object.keys(zoneList);
34
+
35
+ // If list.persons exists and is an object, use its values;
36
+ // if it's already an array, use it directly.
37
+ let cleaned = list
38
+
39
+
40
+ for (let zone of zones) {
41
+ payload.payload[zone] = cleaned.filter(person =>
42
+ person.zoneName?.trim().toLowerCase() === zone.toLowerCase()
43
+ )
44
+ }
45
+
46
+ try {
47
+ writeFileSync(FILE_NAME, JSON.stringify(payload, null, 2))
48
+ } catch (error) {
49
+ console.error(`Error writing ${FILE_NAME}:`, error)
50
+ }
51
+
52
+ return payload;
53
+ }
54
+
package/getZone.js ADDED
@@ -0,0 +1,103 @@
1
+ import puppeteer from "puppeteer";
2
+ import { cookieHandler, saveCookies } from "./connectToChurch/cookieHandler.js";
3
+ import { getBearer } from "./connectToChurch/getBearer.js";
4
+ import { jwtDecode } from "jwt-decode";
5
+ import { promises as fs } from "node:fs";
6
+ import ora from "ora";
7
+
8
+ async function login(user, pass, page) {
9
+ // Enter username
10
+ await page.goto('https://referralmanager.churchofjesuschrist.org/')
11
+ await page.type("input[name='identifier']", user)
12
+ await page.click("input[type='submit']")
13
+
14
+ // Enter password
15
+ await page.waitForSelector("input[name='credentials.passcode']", {timeout: 10000})
16
+ await page.type("input[name='credentials.passcode']", pass)
17
+ await page.click("input[type='submit']")
18
+ await page.waitForNavigation()
19
+ // get cookies and save
20
+ const cookies = await page.cookies()
21
+ await saveCookies(cookies)
22
+
23
+ }
24
+
25
+ async function getPeopleList(page, bearer, decodedBearer) {
26
+ const list = await page.evaluate(async (decodedBearer, bearer) => {
27
+ const peopleList = await fetch(`https://referralmanager.churchofjesuschrist.org/services/people/mission/${JSON.stringify(decodedBearer.missionId)}?includeDroppedPersons=true`, {
28
+ method: 'GET',
29
+ headers: {
30
+ 'Authorization' : `Bearer ${bearer}`
31
+ }
32
+ })
33
+ const list = await peopleList.text()
34
+ return list
35
+ }, decodedBearer, bearer)
36
+ return list
37
+ }
38
+
39
+ export async function getZone(config=null) {
40
+ const spinner = ora('Getting zone info').start()
41
+ const browser = await puppeteer.launch()
42
+ const page = await browser.newPage()
43
+ if (!config) {
44
+ config = await fs.readFile('./resources/config.json', 'utf8').then(JSON.parse);
45
+ }
46
+ const user = config.username
47
+ const pass = config.password
48
+
49
+ const needToGoOnline = async () => {
50
+ try {
51
+ let rawList = fs.readFile('./resources/rawList.json', 'utf-8').then(JSON.parse)
52
+ return rawList
53
+ // eslint-disable-next-line no-unused-vars
54
+ } catch (e) {
55
+ return false
56
+ }
57
+ }
58
+
59
+ let useWitchery = await needToGoOnline()
60
+ let stuff = undefined
61
+
62
+ if (!useWitchery) {
63
+ const okayToSkipLogin = await cookieHandler(page)
64
+ if (okayToSkipLogin) {
65
+ await page.goto('https://referralmanager.churchofjesuschrist.org/')
66
+ const isLoggedOut = await page.$("input[name='identifier']")
67
+ if (isLoggedOut) {
68
+ spinner.color = 'red'
69
+ spinner.text = 'Session expired, logging in again'
70
+ await login(user, pass, page)
71
+ }
72
+ } else {
73
+ await login(user, pass, page)
74
+ }
75
+
76
+ const bearer = await getBearer(page)
77
+ const decodedBearer = jwtDecode(bearer)
78
+
79
+ spinner.color = 'yellow'
80
+ spinner.text = 'Making it look pretty'
81
+
82
+ stuff = await JSON.parse(await getPeopleList(page, bearer, decodedBearer))
83
+ } else {
84
+ spinner.text = 'Skipping login'
85
+ spinner.color = 'green'
86
+ stuff = useWitchery
87
+ }
88
+
89
+ const list = stuff.persons
90
+
91
+ let zoneList = [];
92
+ for (let i = 0; i < list.length; i++) {
93
+ if (list[i].zoneName) {
94
+ const trimmedZoneName = list[i].zoneName.trim();
95
+ if (!zoneList.includes(trimmedZoneName)) {
96
+ zoneList.push(trimmedZoneName);
97
+ }
98
+ }
99
+ }
100
+ await browser.close()
101
+ spinner.succeed(' Zone information updated!')
102
+ return zoneList
103
+ }
package/index.js ADDED
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ import chalk from "chalk";
4
+ import prompts from "prompts";
5
+ import { configCharles } from "./configCharles.js";
6
+ import { sneakyChurch } from "./connectToChurch/sneakyChurch.js";
7
+ import { createPayload } from "./createPayload.js";
8
+ import { promises as fs, existsSync, mkdirSync } from "fs";
9
+ import { sneakyFacebook } from "./connectToFacebook/sneakyFacebook.js";
10
+ import { lastRun } from "./lastRun.js";
11
+ import { smlReport } from "./smlReport.js";
12
+ import { createConfig } from "./createConfig.js";
13
+
14
+ async function main() {
15
+ if (!existsSync('./resources')) {
16
+ mkdirSync('./resources')
17
+ }
18
+ console.clear()
19
+ console.log(chalk.dim('Welcome to Charles, booting up...'))
20
+ let config
21
+ try {
22
+ config = await fs.readFile('./resources/config.json', 'utf8').then(JSON.parse);
23
+ // eslint-disable-next-line no-unused-vars
24
+ } catch (e) {
25
+ console.log(chalk.redBright("Looks like you haven't used Charles before! Let's set this up...\n"))
26
+ config = await createConfig("resources/config.json")
27
+ }
28
+
29
+ async function menu() {
30
+ const questions = {
31
+ type: 'select',
32
+ name: 'program',
33
+ message: 'What should we do today?',
34
+ choices: [
35
+ { title: 'Send Charles message', value: 'charles'},
36
+ { title: 'Get SML report', value: 'report'},
37
+ {title: 'Change settings', value: 'settings'},
38
+ { title: 'Test run Charles', value: 'test'},
39
+ { title: 'Yeet outta here', value: 'exit'},
40
+ ],
41
+ initial: 0,
42
+ instructions: false
43
+ }
44
+ return await prompts(questions)
45
+ }
46
+
47
+ let select;
48
+ do {
49
+ select = await menu();
50
+ if (!select.program) {
51
+ console.log(chalk.red(chalk.italic("No option selected. Please try again.")));
52
+ continue;
53
+ }
54
+ if (select.program?.includes('charles')) {
55
+ // run charles
56
+ // Only run if it has been less than 24 hours...
57
+ const lessThan24HoursAgo = await lastRun()
58
+ const e2ee = await prompts({
59
+ type: 'text',
60
+ name: 'e2ee',
61
+ message: 'What is your e2ee pin?'
62
+ })
63
+ if (lessThan24HoursAgo) {
64
+ const moveOn = await prompts(
65
+ {
66
+ type: 'confirm',
67
+ name: 'value',
68
+ message: "Woah there, do you still wanna run Charles? It's been less than two days since he last ran.",
69
+ initial: true
70
+ }
71
+ )
72
+ if (moveOn.value) {
73
+ console.clear()
74
+ const [todaysList, beginPackage] = await sneakyChurch(config.username, config.password)
75
+ await createPayload(todaysList, beginPackage)
76
+ await sneakyFacebook(undefined, undefined, e2ee.e2ee)
77
+ }
78
+ } else {
79
+ console.clear()
80
+ const [todaysList, beginPackage] = await sneakyChurch(config.username, config.password)
81
+ await createPayload(todaysList, beginPackage)
82
+ await sneakyFacebook(undefined, undefined, e2ee)
83
+ }
84
+ } else if (select.program?.includes('report')) {
85
+ try {
86
+ const report = await smlReport()
87
+ console.log(report)
88
+ // eslint-disable-next-line no-unused-vars
89
+ } catch (e) {
90
+ console.log(chalk.redBright("Your cookies must've gone stale :( Try test running Charles."))
91
+ }
92
+ } else if (select.program?.includes('exit')) {
93
+ console.log("Exiting...");
94
+ break
95
+ } else if (select.program?.includes('settings')) {
96
+ try {
97
+ await fs.unlink('./resources/charlesConfig.json')
98
+ await configCharles('./resources/charlesConfig.json')
99
+ // eslint-disable-next-line no-unused-vars
100
+ } catch (err) {
101
+ await configCharles('./resources/charlesConfig.json')
102
+ }
103
+ }
104
+ else if (select.program?.includes('test')) {
105
+ console.clear()
106
+ const security = await prompts([{
107
+ type: 'text',
108
+ name: 'testZone',
109
+ message: 'What is the Messenger ID of your test chat?'
110
+ }, {
111
+ type: 'text',
112
+ name: 'e2ee',
113
+ message: 'What is your e2ee pin?'
114
+ }])
115
+ console.clear()
116
+ const [todaysList, beginPackage] = await sneakyChurch(config.username, config.password, "", false)
117
+ await createPayload(todaysList, beginPackage)
118
+ await sneakyFacebook(security.testZone, false, security.e2ee)
119
+ }
120
+ } while (!select.program?.includes('exit'));
121
+ }
122
+
123
+ main();
package/lastRun.js ADDED
@@ -0,0 +1,24 @@
1
+ // Returns true if you should move on, false if it has been less than two days
2
+
3
+ import { promises } from "fs";
4
+
5
+ export async function lastRun() {
6
+ let timeStampFile
7
+ try {
8
+ timeStampFile = (await promises.readFile('resources/lastRun.txt')).toString()
9
+ // eslint-disable-next-line no-unused-vars
10
+ } catch (e) {
11
+ timeStampFile = null
12
+ }
13
+ if (timeStampFile) {
14
+ if ((Date.now() - (2 * 24 * 60 * 60 * 1000)) <= parseInt(timeStampFile)) {
15
+ return true
16
+ } else {
17
+ await promises.writeFile('resources/lastRun.txt', Date.now().toString())
18
+ return false
19
+ }
20
+ } else {
21
+ await promises.writeFile('resources/lastRun.txt', Date.now().toString())
22
+ return false
23
+ }
24
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "charlesv2",
3
+ "version": "1.0.0",
4
+ "description": "a friendly chabot written in js for use in the SCCM",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1",
9
+ "start": "node index.js"
10
+ },
11
+ "author": "Jack Jones",
12
+ "bin": {
13
+ "charles": "./index.js"
14
+ },
15
+ "license": "ISC",
16
+ "dependencies": {
17
+ "bottleneck": "^2.19.5",
18
+ "chalk": "^5.4.1",
19
+ "cli-progress": "^3.12.0",
20
+ "jwt-decode": "^4.0.0",
21
+ "node-fetch": "^3.3.2",
22
+ "ora": "^8.2.0",
23
+ "prompts": "^2.4.2",
24
+ "puppeteer": "^24.2.1",
25
+ "puppeteer-extra": "^3.3.6",
26
+ "puppeteer-extra-plugin-stealth": "^2.11.2"
27
+ }
28
+ }
@@ -0,0 +1,18 @@
1
+ export function prettyStringZones(data, avg) {
2
+ let result = '';
3
+ result += avg
4
+ result += "\n"
5
+ // Loop through each zone in the data
6
+ for (const zone in data.payload) {
7
+ if (data.payload.hasOwnProperty(zone)) {
8
+ result += `${zone}\n`; // Add the zone name
9
+ // For each member under the zone, list their name
10
+ data.payload[zone].forEach(person => {
11
+ result += ` - ${person.name}\n`; // Add the person's name
12
+ });
13
+ result += '\n'; // Add an empty line between zones
14
+ }
15
+ }
16
+
17
+ return result;
18
+ }