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/smlReport.js ADDED
@@ -0,0 +1,143 @@
1
+ import chalk from "chalk"
2
+ import cliProgress from 'cli-progress'
3
+ import fetch from 'node-fetch'
4
+ import { promises } from "fs"
5
+ import { averageFilter } from "./connectToChurch/averageFilter.js"
6
+ import Bottleneck from "bottleneck"
7
+
8
+ function formatTime(minutes) {
9
+ const days = Math.floor(minutes / 1440); // 1 day = 1440 minutes
10
+ const hours = Math.floor((minutes % 1440) / 60); // Remaining hours
11
+ const mins = Math.floor(minutes % 60); // Remaining minutes
12
+
13
+ let result = [];
14
+ if (days > 0) result.push(`${days} day${days > 1 ? 's' : ''}`);
15
+ if (hours > 0) result.push(`${hours} hr${hours > 1 ? 's' : ''}`);
16
+ if (mins > 0 || result.length === 0) result.push(`${mins} min${mins > 1 ? 's' : ''}`);
17
+
18
+ return result.join(" ");
19
+ }
20
+
21
+ async function processContactTime(timeline) {
22
+ const reversedTimeline = [...timeline].reverse();
23
+
24
+ let referralSent = null;
25
+ let lastContact = null;
26
+
27
+ for (const item of reversedTimeline) {
28
+ switch (item.timelineItemType) {
29
+ case "NEW_REFERRAL":
30
+ referralSent = new Date(item.itemDate);
31
+ lastContact = null; // Reset when a new referral is found
32
+ break;
33
+ case "CONTACT":
34
+ case "TEACHING":
35
+ if (!lastContact) {
36
+ lastContact = new Date(item.itemDate);
37
+ }
38
+ break;
39
+ default:
40
+ continue;
41
+ }
42
+ }
43
+
44
+ if (referralSent && lastContact) {
45
+ const duration = (lastContact - referralSent) / (1000 * 60); // Convert milliseconds to minutes
46
+ return Math.floor(duration);
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+
53
+ const getPersonTimeline = async (person, bearer, cookie, bar, unprocessed) => {
54
+ const personTimeline = await fetch(`https://referralmanager.churchofjesuschrist.org/services/progress/timeline/${person.guid}`, {
55
+ method: 'GET',
56
+ 'headers': {
57
+ "Authorization": `Bearer ${bearer}`,
58
+ "Cookie": cookie,
59
+ "User-Agent": "Mozilla/5.0"
60
+ }
61
+ })
62
+ .then(response => response.json())
63
+
64
+ bar.increment()
65
+
66
+ const time = await processContactTime(personTimeline)
67
+
68
+ if (!unprocessed[person.areaName]) {
69
+ unprocessed[person.areaName] = [];
70
+ }
71
+ unprocessed[person.areaName].push(time);
72
+ }
73
+
74
+ export const smlReport = async () => {
75
+
76
+ const bar = new cliProgress.SingleBar({
77
+ format: 'But the fat Hobbit, he knows. Eyes always watching. |' + chalk.magenta('{bar}') + '| {value}/{total} People || ETA: {eta_formatted}',
78
+ barCompleteChar: '\u2588',
79
+ barIncompleteChar: '\u2591',
80
+ hideCursor: true
81
+ });
82
+
83
+
84
+ let rawList
85
+ try {
86
+ rawList = await JSON.parse( await promises.readFile('resources/rawList.json'))
87
+ // eslint-disable-next-line no-unused-vars
88
+ } catch(e) {
89
+ console.log(chalk.red("You shall not pass! Run Charles at least once before accessing SML features."))
90
+ return
91
+ }
92
+
93
+ // Filter list down
94
+ const filtered = await averageFilter(rawList.persons)
95
+ const bearer = (await promises.readFile('resources/bearer.txt')).toString().trim()
96
+ const cookieDough = await JSON.parse(await promises.readFile('resources/cookies.json'))
97
+ const cookieString = cookieDough
98
+ .map(cookie => `${cookie.name}=${cookie.value}`)
99
+ .join("; ");
100
+
101
+ bar.start(filtered.length, 0)
102
+ let contactsWithoutAveraging = {}
103
+
104
+ const limiter = new Bottleneck({
105
+ maxConcurrent: 10,
106
+ })
107
+
108
+
109
+ // If you don't await these promises the church servers will block you so you don't DDoS them.
110
+ let tasks = filtered
111
+ .filter(person => person.guid) // Ignore people without a GUID
112
+ .map(person =>
113
+ limiter.schedule(() =>
114
+ getPersonTimeline(person, bearer, cookieString, bar, contactsWithoutAveraging)
115
+ )
116
+ );
117
+ await Promise.all(tasks)
118
+
119
+ bar.stop();
120
+
121
+ // DELETE anything empty that remains
122
+
123
+ delete contactsWithoutAveraging[null];
124
+ delete contactsWithoutAveraging[undefined];
125
+ delete contactsWithoutAveraging[""];
126
+
127
+ let areaAverages = Object.entries(contactsWithoutAveraging)
128
+ .map(([area, times]) => ({ area: area.trim(), avgTime: times.reduce((sum, t) => sum + (t || 0), 0) / times.length })) // Compute numeric average
129
+ .sort((a, b) => a.avgTime - b.avgTime); // Sort by shortest average contact time
130
+
131
+ let message = "-->Contact Times<--\n\n"
132
+ for (const { area, avgTime } of areaAverages) {
133
+ let prefix = "⌛"
134
+ if (avgTime < 360) {
135
+ prefix = '🔥'
136
+ } else if (avgTime > 720) {
137
+ prefix = '😱'
138
+ }
139
+ message += `${prefix} ${area}: ${formatTime(avgTime)}\n`
140
+ }
141
+
142
+ return message
143
+ }