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.

package/README.md ADDED
@@ -0,0 +1,35 @@
1
+
2
+ # CHARLESV3
3
+
4
+ If you don't know what this code does, just forget about it.
5
+
6
+ ## Why JS?
7
+
8
+ > JavaScript is the only language that I'm aware of that people feel they don't need to learn before they start using it. --Douglas Crockford
9
+
10
+ JavaScript (or JS) is one of the only truly universal programming languages. It's easy to use and simple to read. For that reason I rewrote the Holly System into JavaScript.
11
+
12
+ ## Why Not TypeScript?
13
+
14
+ ...I have yet to learn TypeScript. I would love to, but I'm a missionary and don't have time for silly things like that.
15
+
16
+ ## How do I...?
17
+
18
+ HA, nice try. If you're supposed to use this, you'll have received a document with instructions and videos for setup.
19
+
20
+ AUTHORS NOTE: if you already know how to do everything and want a smoother running experience, double click the charles.bat file
21
+
22
+ ## TODO
23
+
24
+ - [x] Set up basic framework
25
+ - [x] Create pseudo-human input for websites
26
+ - [x] Create a CLI to interact with Charles (this particular instance of Holly)
27
+ - [ ] Reformat ugly code
28
+ - [x] Refactor fetching
29
+ - [ ] Wrap in electron.js interface?
30
+ - [ ] Make shareable across missions
31
+ - [ ] Increase efficiency
32
+
33
+ # CREDIT
34
+
35
+ Original credit for the Holly system goes to @jkcoxson (github)
package/charles.bat ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ node index.js
3
+ pause
@@ -0,0 +1,43 @@
1
+ import chalk from "chalk"
2
+ import { existsSync, readFileSync, writeFileSync } from "fs"
3
+ import prompts from "prompts"
4
+ import { getZone } from './getZone.js'
5
+ import { createConfig } from "./createConfig.js"
6
+
7
+
8
+ function checkConfig(configPath) {
9
+ try {
10
+ if (!existsSync(configPath)) {
11
+ throw new Error("Config file not found.");
12
+ }
13
+ return JSON.parse(readFileSync(configPath));
14
+ // eslint-disable-next-line no-unused-vars
15
+ } catch (e) {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ export async function configCharles(configPath) {
21
+ if (!existsSync(configPath.replace('/charlesConfig.json', '/config.json'))) {
22
+ await createConfig('./resources/config.json')
23
+ }
24
+ if (checkConfig(configPath)) {
25
+ return await JSON.parse(readFileSync(configPath))
26
+ } else {
27
+ console.clear()
28
+ console.log(chalk.dim("You don't seem to have set up Charles yet, let's do that now!\n"))
29
+ console.log(chalk.cyanBright("You're going to be asked for the Zone Chat ID of your groups.\nYou can find them in the Messenger Website ") + chalk.dim("(EX https://www.messenger.com/t/") + chalk.cyanBright("2276304299063254") + chalk.dim("<-- ID)"))
30
+ const zoneArray = await getZone()
31
+ let questions = []
32
+ for (let i = 0; i < zoneArray.length; i++) {
33
+ questions.push({
34
+ type: 'text',
35
+ name: zoneArray[i],
36
+ message: `What is the Zone Chat ID for ${zoneArray[i]}?`
37
+ });
38
+ }
39
+ const charlesConfig = await prompts(questions)
40
+ writeFileSync('resources/charlesConfig.json', JSON.stringify(charlesConfig))
41
+ return charlesConfig
42
+ }
43
+ }
@@ -0,0 +1,22 @@
1
+ function isRecent(person) {
2
+ const now = Date.now(); // Get the current timestamp in milliseconds
3
+ const twoWeeksAgo = now - (14 * 24 * 60 * 60 * 1000); // Subtract 2 weeks in milliseconds
4
+ const twelveHoursAgo = now - (12 * 60 * 60 * 1000); // Subtract 12 hours in milliseconds
5
+
6
+ // Check if referral date is between 12 hours ago and 2 weeks ago
7
+ return person.referralAssignedDate > twoWeeksAgo && person.referralAssignedDate <= twelveHoursAgo;
8
+ }
9
+
10
+
11
+ export function averageFilter(thing) {
12
+ return thing.reduce((acc, person) => {
13
+ if (isRecent(person)) {
14
+ acc.push({
15
+ guid: person.personGuid,
16
+ zoneName: person.zoneName,
17
+ areaName: person.areaName
18
+ });
19
+ }
20
+ return acc;
21
+ }, []);
22
+ }
@@ -0,0 +1,43 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+
4
+ // Helper to get the current directory from the file URL
5
+ // Helper to get the current directory from the file URL
6
+ const getCurrentDir = () => {
7
+ const fileUrl = new URL(import.meta.url);
8
+ let pathname = fileUrl.pathname;
9
+
10
+ // If pathname starts with a leading slash, remove it for Windows compatibility
11
+ if (pathname.startsWith('/')) {
12
+ pathname = pathname.substring(1);
13
+ }
14
+
15
+ return path.dirname(pathname);
16
+ };
17
+
18
+ // Returns true if cookies exist and are applied, false if login is needed
19
+ export async function cookieHandler(page) {
20
+ try {
21
+ const currentDir = getCurrentDir();
22
+ const filePath = path.resolve(currentDir, '..', 'resources', 'cookies.json'); // Properly resolve relative path
23
+ const cookies = await fs.readFile(filePath, 'utf8').then(JSON.parse); // Using fs.readFile instead of fetch
24
+
25
+ await page.setCookie(...cookies);
26
+ return true;
27
+ } catch (error) {
28
+ console.error('Failed to load cookies:', error.message);
29
+ return false;
30
+ }
31
+ }
32
+
33
+ // Saves cookies to file
34
+ export async function saveCookies(cookie) {
35
+ try {
36
+ const currentDir = getCurrentDir();
37
+
38
+ const filePath = path.resolve(currentDir, '..', 'resources', 'cookies.json'); // Properly resolve relative path
39
+ await fs.writeFile(filePath, JSON.stringify(cookie, null, 2));
40
+ } catch (error) {
41
+ console.error('Writing cookies failed:', error.message);
42
+ }
43
+ }
@@ -0,0 +1,121 @@
1
+ import chalk from "chalk";
2
+ import cliProgress from 'cli-progress'
3
+ import { averageFilter } from "./averageFilter.js";
4
+ import Bottleneck from "bottleneck";
5
+
6
+
7
+ function formatTime(minutes) {
8
+ const days = Math.floor(minutes / 1440); // 1 day = 1440 minutes
9
+ const hours = Math.floor((minutes % 1440) / 60); // Remaining hours
10
+ const mins = Math.floor(minutes % 60); // Remaining minutes
11
+
12
+ let result = [];
13
+ if (days > 0) result.push(`${days} day${days > 1 ? 's' : ''}`);
14
+ if (hours > 0) result.push(`${hours} hr${hours > 1 ? 's' : ''}`);
15
+ if (mins > 0 || result.length === 0) result.push(`${mins} min${mins > 1 ? 's' : ''}`);
16
+
17
+ return result.join(" ");
18
+ }
19
+
20
+
21
+
22
+
23
+
24
+ async function processContactTime(timeline) {
25
+ const reversedTimeline = [...timeline].reverse();
26
+
27
+ let referralSent = null;
28
+ let lastContact = null;
29
+
30
+ for (const item of reversedTimeline) {
31
+ switch (item.timelineItemType) {
32
+ case "NEW_REFERRAL":
33
+ referralSent = new Date(item.itemDate);
34
+ lastContact = null; // Reset when a new referral is found
35
+ break;
36
+ case "CONTACT":
37
+ case "TEACHING":
38
+ if (!lastContact) {
39
+ lastContact = new Date(item.itemDate);
40
+ }
41
+ break;
42
+ default:
43
+ continue;
44
+ }
45
+ }
46
+
47
+ if (referralSent && lastContact) {
48
+ const duration = (lastContact - referralSent) / (1000 * 60); // Convert milliseconds to minutes
49
+ return Math.floor(duration);
50
+ }
51
+
52
+ return null;
53
+ }
54
+
55
+ async function contactTimeUnhinged(guid, page, bar, unprocessedContacts, person) {
56
+ // await delay(500); // Wait .5 second between requests
57
+ const response = await page.evaluate(async (guid) => {
58
+ const url = `https://referralmanager.churchofjesuschrist.org/services/progress/timeline/${guid}`;
59
+ const response = await fetch(url, {method: 'GET'} )
60
+ return await response.json()
61
+ }, guid)
62
+
63
+ bar.increment()
64
+
65
+ const time = await processContactTime(response)
66
+ if (!unprocessedContacts[person.zoneName]) {
67
+ unprocessedContacts[person.zoneName] = [];
68
+ }
69
+ unprocessedContacts[person.zoneName].push(time);
70
+
71
+ }
72
+
73
+ const limiter = new Bottleneck({
74
+ maxConcurrent: 10,
75
+ })
76
+
77
+
78
+
79
+ export async function getAverage(wholeShebang, page) {
80
+ const bar = new cliProgress.SingleBar({
81
+ format: 'DigiStalking People |' + chalk.cyan('{bar}') + '| {percentage}% || {value}/{total} People || ETA: {eta_formatted}',
82
+ barCompleteChar: '\u2588',
83
+ barIncompleteChar: '\u2591',
84
+ hideCursor: true
85
+ });
86
+
87
+ let parsed = await averageFilter(wholeShebang);
88
+ let unprocessedContacts = {};
89
+
90
+ bar.start(parsed.length, 0);
91
+
92
+ let tasks = parsed
93
+ .filter(person => person.guid) // Ignore people without a GUID
94
+ .map(person =>
95
+ limiter.schedule(() =>
96
+ contactTimeUnhinged(person.guid, page, bar, unprocessedContacts, person)
97
+ )
98
+ );
99
+ await Promise.all(tasks)
100
+
101
+ bar.stop();
102
+
103
+ // Remove empty zones
104
+ delete unprocessedContacts[null];
105
+ delete unprocessedContacts[undefined];
106
+ delete unprocessedContacts[""];
107
+ delete unprocessedContacts['Dothan']
108
+
109
+ // Compute and sort averages
110
+ let zoneAverages = Object.entries(unprocessedContacts)
111
+ .map(([zone, times]) => ({ zone: zone.trim(), avgTime: times.reduce((sum, t) => sum + (t || 0), 0) / times.length })) // Compute numeric average
112
+ .sort((a, b) => a.avgTime - b.avgTime); // Sort by shortest average contact time
113
+
114
+ // Construct message
115
+ let message = "-->Contact Time<--\n";
116
+ for (const { zone, avgTime } of zoneAverages) {
117
+ message += `↳ ${zone}: ${formatTime(avgTime)}\n`;
118
+ }
119
+
120
+ return message;
121
+ }
@@ -0,0 +1,55 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+
4
+ // Helper function to resolve the absolute path to bearer.txt
5
+ // Function to get the bearer token
6
+
7
+ // Helper to get the current directory from the file URL
8
+ const getCurrentDir = () => {
9
+ const fileUrl = new URL(import.meta.url);
10
+ let pathname = fileUrl.pathname;
11
+
12
+ // If pathname starts with a leading slash, remove it for Windows compatibility
13
+ if (pathname.startsWith('/')) {
14
+ pathname = pathname.substring(1);
15
+ }
16
+
17
+ return path.dirname(pathname);
18
+ };
19
+
20
+ export async function getBearer(page) {
21
+
22
+ let bearer
23
+ const currentDir = getCurrentDir();
24
+ const filePath = path.resolve(currentDir, '..', 'resources', 'bearer.txt'); // Resolves to the correct path
25
+ try {
26
+ bearer = await fs.readFile(filePath, 'utf8'); // Using fs.readFile instead of fetch
27
+ // eslint-disable-next-line no-unused-vars
28
+ } catch (e) {
29
+ bearer = null
30
+ }
31
+
32
+ if (!bearer) {
33
+ try {
34
+ // Fetch authentication data from the server
35
+ const missionaryObj = await page.evaluate(async () => {
36
+ const response = await fetch("https://referralmanager.churchofjesuschrist.org/services/auth");
37
+ return await response.json(); // Ensure it's parsed as JSON
38
+ });
39
+
40
+ if (!missionaryObj.token) {
41
+ throw new Error("No token found in response");
42
+ }
43
+
44
+ // Save the token to bearer.txt
45
+ await fs.writeFile(filePath, missionaryObj.token, "utf-8");
46
+
47
+ bearer = missionaryObj.token;
48
+ } catch (error) {
49
+ console.error("Error fetching bearer token:", error.message);
50
+ return null; // Return null to indicate failure
51
+ }
52
+ }
53
+
54
+ return bearer;
55
+ }
@@ -0,0 +1,49 @@
1
+ function isWithinTimeRange(dateString) {
2
+ const givenDate = new Date(dateString);
3
+ const now = new Date();
4
+
5
+ // Calculate the time boundaries
6
+ const fortyEightHoursAgo = new Date(now);
7
+ fortyEightHoursAgo.setHours(now.getHours() - 2);
8
+
9
+ const fourteenDaysAgo = new Date(now);
10
+ fourteenDaysAgo.setDate(now.getDate() - 14);
11
+
12
+ // Check if the given date is within the range
13
+ return givenDate >= fourteenDaysAgo && givenDate <= fortyEightHoursAgo;
14
+ }
15
+
16
+ function isGreenOrYellow(obj) {
17
+ if (obj.personStatus === 1 || obj.personStatus === 2 || obj.personStatus === 3 || obj.personStatus === 4) {
18
+ return true
19
+ } else {
20
+ return false
21
+ }
22
+ }
23
+
24
+ function unattempted(obj) {
25
+ return (obj.referralStatus === 10)
26
+ }
27
+
28
+ function unsuccessful(obj) {
29
+ return (obj.referralStatus === 20)
30
+ }
31
+
32
+ export async function listToday(list) {
33
+ let listFinal = []
34
+ if (!Array.isArray(list) && typeof list === 'object') {
35
+ for (const key in list) {
36
+ if (isWithinTimeRange(list[key].assignedDate) && isGreenOrYellow(list[key]) && (unattempted(list[key]) || unsuccessful(list[key]))) {
37
+ listFinal.push(list[key])
38
+ }
39
+ }
40
+ } else {
41
+ let todaysList = list.filter(obj => isWithinTimeRange(obj.assignedDate))
42
+ const todaysListWithoutGrey = todaysList.filter(obj => isGreenOrYellow(obj))
43
+ listFinal = todaysListWithoutGrey.filter(obj =>{
44
+ return (unattempted(obj) || unsuccessful(obj))
45
+ })
46
+ }
47
+
48
+ return listFinal
49
+ }
@@ -0,0 +1,132 @@
1
+ import puppeteer from "puppeteer"
2
+ import path from 'node:path'
3
+ import { cookieHandler, saveCookies } from "./cookieHandler.js"
4
+ import { readFileSync, writeFileSync } from "fs"
5
+ import { jwtDecode } from "jwt-decode"
6
+ import { getAverage } from "./getAverage.js"
7
+ import { superParse } from "./superParse.js"
8
+ import { listToday } from "./listToday.js"
9
+ import ora from "ora"
10
+ import { getBearer } from "./getBearer.js"
11
+
12
+ function isMoreThanADayOld(timestamp) {
13
+ const oneDay = 24 * 60 * 60 * 1000
14
+ const now = Date.now()
15
+ return now - timestamp > oneDay
16
+ }
17
+
18
+ export async function login(user, pass, page) {
19
+ // Enter username
20
+ await page.goto('https://referralmanager.churchofjesuschrist.org/')
21
+ await page.waitForSelector("input[name='identifier']", { timeout: 10000 });
22
+ const username = await page.$("input[name='identifier']")
23
+ await username.type(user)
24
+ await page.click("input[type='submit']")
25
+
26
+ // Enter password
27
+ await page.waitForSelector("input[name='credentials.passcode']", {timeout: 10000})
28
+ await page.type("input[name='credentials.passcode']", pass)
29
+ await page.click("input[type='submit']")
30
+ await page.waitForNavigation()
31
+ // get cookies and save
32
+ const cookies = await page.cookies()
33
+ await saveCookies(cookies)
34
+
35
+ }
36
+
37
+ // False == should not pull
38
+ async function toPullOrNotToPullThatIsTheQuestion(pathToHome) {
39
+ try {
40
+ let thiccList = readFileSync(path.join(pathToHome, 'resources', 'people.json'), 'utf-8')
41
+ let thiccJSON = await JSON.parse(thiccList)
42
+ let rawList = readFileSync(path.join(pathToHome, 'resources', 'rawList.json'), 'utf-8')
43
+ let rawJSON = await JSON.parse(rawList)
44
+ if (!isMoreThanADayOld(thiccJSON.processedTime) && !isMoreThanADayOld(rawJSON.processedTime)) {
45
+ return false
46
+ }
47
+ else {
48
+ return true
49
+ }
50
+
51
+ } catch (e) {
52
+ console.log(e)
53
+ return true
54
+ }
55
+ }
56
+
57
+ export async function getPeopleList(page, bearer, decodedBearer) {
58
+ const list = await page.evaluate(async (decodedBearer, bearer) => {
59
+ const peopleList = await fetch(`https://referralmanager.churchofjesuschrist.org/services/people/mission/${JSON.stringify(decodedBearer.missionId)}?includeDroppedPersons=true`, {
60
+ method: 'GET',
61
+ headers: {
62
+ 'Authorization' : `Bearer ${bearer}`
63
+ }
64
+ })
65
+ const list = await peopleList.json()
66
+ return list
67
+ }, decodedBearer, bearer)
68
+ return list
69
+ }
70
+
71
+ export async function sneakyChurch(user, pass, pathToHome="", headless=true) {
72
+ console.clear()
73
+ let spool = ora('Opening browser').start()
74
+ // Launch browser and use cookies from previous session if possible.
75
+ const browser = await puppeteer.launch({ headless:headless })
76
+ const page = await browser.newPage()
77
+ spool.color = 'magenta'
78
+ spool.text = "Doin' some black magic"
79
+ const okayToSkipLogin = await cookieHandler(page, pathToHome)
80
+ if (okayToSkipLogin) {
81
+ await page.goto('https://referralmanager.churchofjesuschrist.org/')
82
+ const isLoggedOut = await page.$("input[name='identifier']")
83
+ if (isLoggedOut) {
84
+ await login(user, pass, page)
85
+ }
86
+ } else {
87
+ await login(user, pass, page)
88
+ }
89
+ spool.color = 'green'
90
+ spool.text = 'Stealing your identity'
91
+ // Snag the bearer token *enters hacker mode*
92
+ const bearer = await getBearer(page)
93
+ const decodedBearer = jwtDecode(bearer)
94
+ let lossyList
95
+ let todaysList
96
+ let beginPackage
97
+
98
+ // Get new list if we don't have one cached
99
+ if (await toPullOrNotToPullThatIsTheQuestion(pathToHome)) {
100
+ spool.color = 'cyan'
101
+ spool.text = 'Fetching referrals'
102
+ const fullListObj = await getPeopleList(page, bearer, decodedBearer)
103
+
104
+ writeFileSync(path.join(pathToHome, 'resources', 'rawList.json'), JSON.stringify(fullListObj))
105
+ spool.info('Getting Average contact times!')
106
+
107
+ beginPackage = await getAverage(fullListObj.persons, page)
108
+
109
+ lossyList = await superParse(fullListObj)
110
+ todaysList = await listToday(lossyList)
111
+ writeFileSync(path.join(pathToHome, 'resources', 'people.json'), JSON.stringify(
112
+ { 'processedTime' : Date.now(),
113
+ 'persons' : todaysList
114
+ }
115
+ ))
116
+ spool.succeed('Referrals retrieved')
117
+ await browser.close()
118
+ return [todaysList, beginPackage]
119
+ } else {
120
+ lossyList = await JSON.parse(readFileSync(path.join(pathToHome, 'resources', 'people.json')))
121
+ let rawList = await JSON.parse(readFileSync(path.join(pathToHome, 'resources', 'rawList.json')))
122
+ spool.info('Snooping out Average contact times!')
123
+
124
+ beginPackage = await getAverage(rawList.persons, page)
125
+ todaysList = await listToday(lossyList.persons)
126
+ spool.succeed('Referrals retrieved')
127
+ await browser.close()
128
+ return [todaysList, beginPackage]
129
+ }
130
+
131
+ }
132
+
@@ -0,0 +1,27 @@
1
+ export function superParse(data) {
2
+ if (!data || !Array.isArray(data.persons)) {
3
+ return [];
4
+ }
5
+
6
+ return data.persons
7
+ .map(person => {
8
+ try {
9
+ return {
10
+ guid: person.personGuid,
11
+ name: person.lastName ? `${person.firstName} ${person.lastName}` : person.firstName,
12
+ referralStatus: person.referralStatusId,
13
+ personStatus: person.personStatusId,
14
+ missionId: person.missionId,
15
+ zoneId: person.zoneId ?? null,
16
+ zoneName: person.zoneName ?? null,
17
+ districtId: person.districtId ?? null,
18
+ areaName: person.areaName ?? null,
19
+ assignedDate: person.referralAssignedDate,
20
+ };
21
+ // eslint-disable-next-line no-unused-vars
22
+ } catch (error) {
23
+ return null;
24
+ }
25
+ })
26
+ .filter(person => person !== null);
27
+ };
@@ -0,0 +1,10 @@
1
+ [
2
+ "BOOYAH!!! Y'all don't have any uncontacted referrals\nᕕ(⌐■_■)ᕗ ♪♬",
3
+ "Nice work on contacting all of your referrals! Keep the work up! \nᕕ( ᐛ )ᕗ",
4
+ "How did y'all get so cool? Wait, probably by contacting ALL OF YOUR REFERRALS?\n ¯\\_(ツ)_/¯",
5
+ "I'm sure your referrals will thank you for contacting them, and just in case they don't I'll say it myself.\nThank you.\n\n\\(^-^)/",
6
+ "Referrals contacted? ✅\nThe work hastens? ✅\nCharles approves? ✅\nLET'S GO!! 🚀",
7
+ "🔥 Y'ALL DID IT! No uncontacted referrals left! 🔥 Absolute legends! 🚀",
8
+ "🎉 100% CONTACT RATE ACHIEVED! 🎯 Y'all are on fire! Keep that energy up! 💪",
9
+ "📢 Attention world: These missionaries just reached 100% contacted referrals! Y’all are unstoppable! 🔥"
10
+ ]
@@ -0,0 +1,52 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { getZone } from "../getZone.js";
3
+
4
+ export async function editPayload() {
5
+ let payload;
6
+
7
+ try {
8
+ payload = JSON.parse(readFileSync('payload.json', 'utf8'));
9
+ } catch (err) {
10
+ console.error(err);
11
+ return;
12
+ }
13
+
14
+ let zoneObj;
15
+
16
+ if (existsSync('resources/charlesConfig.json')) {
17
+ try {
18
+ zoneObj = await JSON.parse(readFileSync('resources/charlesConfig.json', 'utf8'));
19
+ } catch (err) {
20
+ console.error(err);
21
+ return;
22
+ }
23
+ } else {
24
+ zoneObj = await getZone();
25
+ }
26
+
27
+ const prettyList = {};
28
+ prettyList["avg"] = payload.average
29
+
30
+ for (const zone in zoneObj) {
31
+ const zoneClean = zone.trim().toString()
32
+ const areaMap = {}; // Will hold areaName as key, and people array as value
33
+ if (!payload.payload[zoneClean]) {
34
+ continue
35
+ }
36
+ for (const person of payload.payload[zoneClean]) {
37
+ if (zone.toLowerCase().trim() === person.zoneName.toLowerCase().trim()) {
38
+ const areaName = person.areaName;
39
+ // Initialize an array for the area if it doesn't exist yet
40
+ if (!areaMap[areaName]) {
41
+ areaMap[areaName] = { people: [] };
42
+ }
43
+ // Add the person's name to the appropriate area
44
+ areaMap[areaName].people.push("· " + person.name); // Assuming 'name' is the key for person names
45
+ }
46
+ }
47
+
48
+ prettyList[zoneClean.toString()] = areaMap;
49
+
50
+ }
51
+ return prettyList
52
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "messages": [
3
+ "You know who’s great at answering calls? The Lord. You know who else should be? Your unsuccessfully contacted referrals. But first, they need a message!",
4
+ "Your unsuccessfully contacted referrals are like manna—best gathered fresh! Don’t let them go stale. Go say hi!",
5
+ "Hey y'all, if I had a dime for every unsuccessfully contacted referral, I’d have… well, a handful of dimes. Let’s change that today!",
6
+ "Good news! Your unsuccessfully contacted referrals are still waiting. Bad news? They won’t reach out first. But you got this!",
7
+ "Imagine me waving a giant foam finger—‘You’re #1 at contacting unsuccessfully contacted referrals!’ Now go prove me right!",
8
+ "Your unsuccessfully contacted referrals are like unopened Christmas presents! Time to unwrap some gospel joy!",
9
+ "I believe in you. More importantly, *He* believes in you. Now go turn those ‘uncontacted’ into ‘new friends’!",
10
+ "Fun fact: Missionary joy is 87% higher when you contact unsuccessfully contacted referrals. (Okay, I made that up, but it sounds legit, right?)",
11
+ "Your unsuccessfully contacted referrals are golden investigators, but first, you gotta *dig*—aka send that first message!",
12
+ "The field is white, already to harvest… but you still gotta grab the sickle (aka your phone). Let’s get messaging!",
13
+ "Remember, every great conversion story starts with a simple ‘Hello!’ Time to make history!",
14
+ "A message today could be the start of someone’s journey toward eternal joy. Don’t leave them waiting!",
15
+ "You have the best news in the world—why keep it to yourself? Let’s spread some light!",
16
+ "Your kindness and testimony are exactly what your unsuccessfully contacted referrals need. Be a little boulder and reach out!",
17
+ "Hey zone, don’t let your unsuccessfully contacted referrals become legendary ghost stories. Time to make contact!",
18
+ "You’re planting seeds of faith—every message is a step toward miracles!",
19
+ "Think of yourself as a celestial delivery service—bringing joy, hope, and truth straight to their inbox!",
20
+ "A small effort today could change someone’s eternity. You never know what impact your message will have!",
21
+ "Faith isn’t about seeing—it’s about acting. Let’s take a leap and send those messages!",
22
+ "The Lord has prepared these people for you. All that’s left is for you to reach out!",
23
+ "Heaven is cheering you on, and so am I! Let’s get those unsuccessfully contacted referrals connected!",
24
+ "Your enthusiasm for the gospel is contagious—now let’s make sure your unsuccessfully contacted referrals catch it!",
25
+ "Missionary work is about love, and love starts with a simple message. Let’s get to it!",
26
+ "The Savior knows and loves each of your unsuccessfully contacted referrals—now it’s your turn to help them feel it!",
27
+ "How many lives can y'all change in one hour?",
28
+ "You can't say 'hi' to Kolob, but you can to your unsuccessfully contacted referrals!",
29
+ "Let's have the SuperFaith to contact our unsuccessfully contacted referrals!",
30
+ "How can we show our unsuccessfully contacted referrals we're excited to meet them?",
31
+ "Q: WWJDIHHAP?\nA: He would contact his unsuccessfully contacted referrals! ;)",
32
+ "How long would you want Christ to wait for you?",
33
+ "Good Morning, y'all! I hope everyone has a great day! Let's BELIEVE, and GO contact those referrals! \n ._.)/\\(._."
34
+ ]
35
+ }
36
+
@@ -0,0 +1,25 @@
1
+ import { readFileSync } from "fs";
2
+ import { randomMessage } from "./randomMessage.js";
3
+
4
+ export async function prettyString(name, wholeShebang, avg) {
5
+ let fancifulString = '';
6
+ const kindMessage = await randomMessage();
7
+ fancifulString += `${kindMessage}\n\n`;
8
+ fancifulString += `${avg}\n-->Uncontacted Referrals<--\n\n`;
9
+ fancifulString += `${name}\n`
10
+
11
+ if (Object.keys(wholeShebang).length <= 0) {
12
+ const options = await JSON.parse(readFileSync('connectToFacebook/crazyJob.json'))
13
+ const randomIndex = Math.floor(Math.random() * options.length)
14
+ fancifulString += options[randomIndex]
15
+ }
16
+
17
+ for (const [area, people] of Object.entries(wholeShebang)) {
18
+ fancifulString += `\n⇒ ${area}\n`
19
+ for (const person of people.people) {
20
+ fancifulString += ` ${person}\n`
21
+ }
22
+ }
23
+
24
+ return fancifulString; // Return the constructed string
25
+ }
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from "fs";
2
+
3
+ export async function randomMessage() {
4
+ const options = await JSON.parse(readFileSync('./connectToFacebook/kindMessages.json')).messages
5
+ const randomIndex = Math.floor(Math.random() * options.length)
6
+
7
+ return options[randomIndex]
8
+ }