diff --git a/chatmap-im-connector/main.go b/chatmap-im-connector/main.go index 2a31de6c..90f7ee24 100644 --- a/chatmap-im-connector/main.go +++ b/chatmap-im-connector/main.go @@ -959,8 +959,10 @@ func main() { redis_port = DefaultRedisPort } log.Printf("Connecting to Redis %s:%s \n", redis_host, redis_port) + redis_password := os.Getenv("REDIS_PASSWORD") redisClient = redis.NewClient(&redis.Options{ - Addr: redis_host + ":" + redis_port, + Addr: redis_host + ":" + redis_port, + Password: redis_password, }) // WhatsApp rejects connections using an outdated hardcoded client diff --git a/chatmap-ui/public/shoelace/assets/icons/crosshair.svg b/chatmap-ui/public/shoelace/assets/icons/crosshair.svg new file mode 100644 index 00000000..13bed746 --- /dev/null +++ b/chatmap-ui/public/shoelace/assets/icons/crosshair.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/chatmap-ui/public/shoelace/assets/icons/crosshair2.svg b/chatmap-ui/public/shoelace/assets/icons/crosshair2.svg new file mode 100644 index 00000000..3c285860 --- /dev/null +++ b/chatmap-ui/public/shoelace/assets/icons/crosshair2.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/chatmap-ui/src/components/ChatMap/parsers/ignore.js b/chatmap-ui/src/components/ChatMap/parsers/ignore.js index d7af2874..7d2b1e72 100644 --- a/chatmap-ui/src/components/ChatMap/parsers/ignore.js +++ b/chatmap-ui/src/components/ChatMap/parsers/ignore.js @@ -14,5 +14,14 @@ const ignore = [ "", ] -export default ignore; +// Check if message is in list of ignored strings +export const isInTheIgnoreList = msg => { + for (let i = 0; i < ignore.length; i++) { + if (msg.indexOf(ignore[i]) > -1) { + return true; + } + }; + return false; +} + diff --git a/chatmap-ui/src/components/ChatMap/parsers/whatsapp-datetime.js b/chatmap-ui/src/components/ChatMap/parsers/whatsapp-datetime.js new file mode 100644 index 00000000..c4608cd5 --- /dev/null +++ b/chatmap-ui/src/components/ChatMap/parsers/whatsapp-datetime.js @@ -0,0 +1,309 @@ +import { MSG_PATTERN } from "./whatsapp"; + +const HOUR_PATTERN = /(\d{1,2})[:.](\d{1,2})(:\d{1,2})?(\s+[pP])?/; + +// Parse time strings +export const parseTimeString = (dateStr) => { + let dateTimeStr = dateStr.replace("a. m.", "AM").replace("p. m.", "PM") + dateTimeStr = dateTimeStr.replace("a.m.", "AM").replace("p.m.", "PM") + let dateTime = dateTimeStr.split(" "); + const now = new Date(); + let fmtDate = [[now.getFullYear(), now.getMonth() + 1, now.getDate()].join("/"), dateTime[1]].join(" ").replaceAll(".", ":") + + return new Date(fmtDate); +} + +/** + * Returns how many items differ between two 3-item arrays and what their + * indices are + */ +export function howManyChanged(parts1, parts2) { + const indices = []; + + for (let i = 0; i<3; i++) { + if (parts1[i] !== parts2[i]) { + indices.push(i); + } + } + + return indices; +} + +/** + * @returns {int[]} A three-element array with the parts of a date as numbers + */ +function getDateParts(match) { + return match + .split(' ')[0].replace(',', '') + .split('/').map(Number); +} + +/** + * @returns {string[]} + */ +function getTimeParts(match) { + const matches = match.match(HOUR_PATTERN); + let hour = Number(matches[1]); + const minute = Number(matches[2]); + let second = 0; + + if ((matches[3] && matches[3].startsWith(' ')) || matches[4]) { + hour += 12; + } else if (matches[3] && matches[3].match(/\d+/)) { + second = Number(matches[3].slice(1)); + } + + return [ + hour.toString().padStart(2, "0"), + minute.toString().padStart(2, "0"), + second.toString().padStart(2, "0"), + ]; +} + +function normalizeParts(dateParts, yearIndex, monthIndex, dayIndex, match) { + try { + const year = (dateParts[yearIndex] < 100 ? dateParts[yearIndex] + 2000 : dateParts[yearIndex]).toString(); + const [month, day] = [dateParts[monthIndex].toString().padStart(2, "0"), dateParts[dayIndex].toString().padStart(2, "0")]; + const [hour, minute, second] = getTimeParts(match[1]); + return [year, month, day, hour, minute, second]; + } catch (e) { + // This prevents errors with date parsing, but we should review date parsing for preventing errors + const now = new Date(); + return [now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds()] + } + +} + +/** + * Returns a function that correctly parses a message's date given that no + * transition was found in the exported file. + */ +function makeCase0Function(dateParts, system) { + let yearIndex, monthIndex, dayIndex; + + if (dateParts[0] <= 12) { + monthIndex = 0; + dayIndex = 1; + yearIndex = 2; + } else if (dateParts[1] <= 12) { + monthIndex = 1; + dayIndex = 0; + yearIndex = 2; + } else { + monthIndex = 2; + yearIndex = 0; + dayIndex = 1; + } + + return (msg) => { + const match = msg.match(MSG_PATTERN[system]); + const dateParts = getDateParts(match[1]); + const [year, month, day, hour, minute, second] = normalizeParts( + dateParts, yearIndex, monthIndex, dayIndex, match + ); + + return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`); + }; +} + +/** + * returns a function that correctly parses a message's date given that a + * transition of a single day (no month) was found. + * + * @param {int[]} oldDate the older date + * @param {int[]} newDate the newer date + * @param {int[]} indices an array with a + */ +function makeCase1Function(oldDate, dayIndex, system) { + const otherIndex1 = (dayIndex + 1) % 3; + const otherIndex2 = (dayIndex + 2) % 3; + let yearIndex, monthIndex; + + if (oldDate[otherIndex1] < oldDate[otherIndex2]) { + monthIndex = otherIndex1; + yearIndex = otherIndex2; + } else { + monthIndex = otherIndex2; + yearIndex = otherIndex1; + } + + return (msg) => { + const match = msg.match(MSG_PATTERN[system]); + const dateParts = getDateParts(match[1]); + const [year, month, day, hour, minute, second] = normalizeParts( + dateParts, yearIndex, monthIndex, dayIndex, match + ); + + return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`); + }; +} + +/** + * returns a function that correctly parses a message's date given that a + * transition of month happened. + */ +function makeCase2Function(changes, maxObserved, system) { + // year is the index that didn't change + const yearIndex = changes.map((c, i) => [c, i]).filter(([c, i]) => c === 0)[0][1]; + const otherIndex1 = (yearIndex + 1) % 3; + const otherIndex2 = (yearIndex + 2) % 3; + let monthIndex, dayIndex; + + // Day is the index that changed most, month is the other one. + if (changes[otherIndex1] > changes[otherIndex2]) { + dayIndex = otherIndex1; + monthIndex = otherIndex2; + } else if (changes[otherIndex1] == changes[otherIndex2]) { + if (maxObserved[otherIndex1] > maxObserved[otherIndex2]) { + dayIndex = otherIndex1; + monthIndex = otherIndex2; + } else { + dayIndex = otherIndex2; + monthIndex = otherIndex1; + } + } else { + dayIndex = otherIndex2; + monthIndex = otherIndex1; + } + // day is the index that became 1 + + return (msg) => { + const match = msg.match(MSG_PATTERN[system]); + const dateParts = getDateParts(match[1]); + const [year, month, day, hour, minute, second] = normalizeParts( + dateParts, yearIndex, monthIndex, dayIndex, match + ); + + return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`); + }; +} + +function makeCase3Function(changes, maxObserved, hasDecreased, fourDigits, system) { + // If there is a four digit number take it straight as the year + const maybeYearIndex = fourDigits.map((n, i) => [n, i]).filter(([n, i]) => n); + let yearIndex; + + if (maybeYearIndex.length > 0) { + yearIndex = maybeYearIndex[0][1]; + } else { + const decreased = hasDecreased.map((h, i) => [h, i]).filter(([h, i]) => !h); + // year index is the one number that didn't decrease. give priority to the + // last position, since the year tends to be last item + // in the date + yearIndex = decreased[decreased.length-1][1]; + } + + const otherIndex1 = (yearIndex + 1) % 3; + const otherIndex2 = (yearIndex + 2) % 3; + let dayIndex, monthIndex; + + const changesWithIndex = changes.map((c, i) => [c, i]); + + // sort by number of changes in increasing order + changesWithIndex.sort((a, b) => a[0] - b[0]); + + // if there's only one index bellow 12 that's the month, otherwise take the + // index with the most changes is the day. + if (maxObserved[otherIndex1] <= 12 && maxObserved[otherIndex2] <= 12) { + if (changes[otherIndex1] > changes[otherIndex2]) { + dayIndex = otherIndex1; + monthIndex = otherIndex2; + } else { + dayIndex = otherIndex2; + monthIndex = otherIndex1; + } + } else { + if (maxObserved[otherIndex1] <= 12) { + monthIndex = otherIndex1; + dayIndex = otherIndex2; + } else { + monthIndex = otherIndex2; + dayIndex = otherIndex1; + } + } + + return (msg) => { + const match = msg.match(MSG_PATTERN[system]); + const dateParts = getDateParts(match[1]); + const [year, month, day, hour, minute, second] = normalizeParts( + dateParts, yearIndex, monthIndex, dayIndex, match + ); + + return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`); + }; +} + +/** + * This function analyzes all the dates in `lines` and returns a function that + * correctly identifies its parts (year, month, day). It works under the + * assumption that the times in every line are sorted chronologically. The logic + * is as follows: + * + * It analyzes all messages in order and stores how many times the date changed. + * The core assumption is that the component that changes the most is the day, + * followed by the month and finally the year. Ordering the amount of changes + * observed we can tell what is what. + * + * @param {string[]} lines of the chat. + * @param {string} the identified system for this chat. + * @returns {function} A function that takes a message and returns correctly its + * date object. + */ +export function getDateFormat(lines, system) { + let oldDateParts = [null, null, null]; + const changes = [0, 0, 0]; // stores how many times each part has changed + let maxObserved = [0, 0, 0]; + const hasDecreased = [false, false, false] + + for (let line of lines) { + const match = line.match(MSG_PATTERN[system]); + + if (match) { + // if this line includes a date, get its parts as numbers. E.g. "3/4/25" + // becomes [3, 4, 25] + const newDateParts = getDateParts(match[1]); + + if (oldDateParts[0] === null) { + // this is the first date seen, set it for comparisson + oldDateParts = newDateParts; + maxObserved = newDateParts; + } else { + // see which parts have changed since the last date observed + const indices = howManyChanged(oldDateParts, newDateParts); + for (let i of indices) { + changes[i] += 1; + } + for (let i = 0; i < 3; i++) { + if (newDateParts[i] > maxObserved[i]) { + maxObserved[i] = newDateParts[i]; + } + if (newDateParts[i] < oldDateParts[i]) { + hasDecreased[i] = true; + } + } + oldDateParts = newDateParts; + } + } + } + + // How many date components changed? + const numChanges = changes.map((c) => c !== 0).reduce((a, b) => a + b, 0); + const fourDigits = maxObserved.map((n) => n > 999); + + if (numChanges === 0) { + // nothing changed (only one date was observed in the export) + return makeCase0Function(oldDateParts, system); + } else if (numChanges === 1) { + // only the day ever changed. + const dayIndex = changes + .map((c, i) => [c, i]) + .filter(([c, i]) => c > 0)[0][1]; + return makeCase1Function(oldDateParts, dayIndex, system); + } else if (numChanges === 2) { + // date and month changed + return makeCase2Function(changes, maxObserved, system); + } else { + // everything changed + return makeCase3Function(changes, maxObserved, hasDecreased, fourDigits, system); + } +} \ No newline at end of file diff --git a/chatmap-ui/src/components/ChatMap/parsers/whatsapp.js b/chatmap-ui/src/components/ChatMap/parsers/whatsapp.js index 1fb4efe3..074315f5 100644 --- a/chatmap-ui/src/components/ChatMap/parsers/whatsapp.js +++ b/chatmap-ui/src/components/ChatMap/parsers/whatsapp.js @@ -7,19 +7,18 @@ * of media files */ -import ignore from "./ignore"; +import { isInTheIgnoreList } from "./ignore"; +import { getDateFormat } from "./whatsapp-datetime"; // Regex to search for coordinates in the format , (ex: -31.006037,-64.262794) const LOCATION_PATTERN = /[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?).*$/; // Regex to search for messages in the format [,