Unify chat timestamps format (#4806)

* Unify chat timestamps format

* Handle ISO timestamps in stats.js

* Refactor timestamp parsing on server

* Switch to ISO timestamps for character/messages creation dates

* Fix type error

* Early exist in saveGroupChat if group not found

* Remove redundant fields from temp.chat export header

* Auto-fix char creation date format on edit

* Add name to fallback chat file names

* Rename parseTimestamp server side function
This commit is contained in:
Cohee
2025-11-26 15:55:15 +02:00
committed by GitHub
parent b9cea60c02
commit 5993084ee6
12 changed files with 171 additions and 175 deletions
+1 -4
View File
@@ -1028,9 +1028,6 @@ function verifyCharactersSearchSortRule() {
}
}
/** @typedef {object} Character - A character */
/** @typedef {object} Group - A group */
/**
* @typedef {object} Entity - Object representing a display entity
* @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item
@@ -8248,7 +8245,7 @@ export function select_selected_character(chid, { switchMenu = true } = {}) {
$('#talkativeness_slider').val(characters[chid].talkativeness || talkativeness_default);
$('#mes_example_textarea').val(characters[chid].mes_example);
$('#selected_chat_pole').val(characters[chid].chat);
$('#create_date_pole').val(characters[chid].create_date);
$('#create_date_pole').val(timestampToMoment(characters[chid].create_date).toISOString());
$('#avatar_url_pole').val(characters[chid].avatar);
$('#chat_import_avatar_url').val(characters[chid].avatar);
$('#chat_import_character_name').val(characters[chid].name);
+25 -31
View File
@@ -162,43 +162,37 @@ export function shouldSendOnEnter() {
}
}
//RossAscends: Added function to format dates used in files and chat timestamps to a humanized format.
//Mostly I wanted this to be for file names, but couldn't figure out exactly where the filename save code was as everything seemed to be connected.
//Does not break old characters/chats, as the code just uses whatever timestamp exists in the chat.
//New chats made with characters will use this new formatting.
export function humanizedDateTime() {
const now = new Date(Date.now());
/**
* Gets a humanized date time string from a given timestamp.
* @param {number} timestamp Timestamp in milliseconds
* @returns {string} Humanized date time string in the format `YYYY-MM-DD@HHhMMmSSsMSms`
*/
export function humanizedDateTime(timestamp = Date.now()) {
const date = new Date(timestamp);
const dt = {
year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
hour: date.getHours(),
minute: date.getMinutes(),
second: date.getSeconds(),
millisecond: date.getMilliseconds(),
};
for (const key in dt) {
dt[key] = dt[key].toString().padStart(2, '0');
const padLength = key === 'millisecond' ? 3 : 2;
dt[key] = dt[key].toString().padStart(padLength, '0');
}
return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s`;
return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s${dt.millisecond}ms`;
}
//this is a common format version to display a timestamp on each chat message
//returns something like: June 19, 2023 2:20pm
export function getMessageTimeStamp() {
const date = Date.now();
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const d = new Date(date);
const month = months[d.getMonth()];
const day = d.getDate();
const year = d.getFullYear();
let hours = d.getHours();
const minutes = ('0' + d.getMinutes()).slice(-2);
let meridiem = 'am';
if (hours >= 12) {
meridiem = 'pm';
hours -= 12;
}
if (hours === 0) {
hours = 12;
}
const formattedDate = month + ' ' + day + ', ' + year + ' ' + hours + ':' + minutes + meridiem;
return formattedDate;
/**
* Gets a timestamp for messages in ISO 8601 format.
* @param {number} timestamp - optional timestamp in milliseconds
* @returns {string} ISO 8601 formatted timestamp
*/
export function getMessageTimeStamp(timestamp = Date.now()) {
const date = new Date(timestamp);
return date.toISOString();
}
+2 -7
View File
@@ -10,7 +10,6 @@ import {
event_types,
getCurrentChatId,
getRequestHeaders,
name1,
name2,
reloadCurrentChat,
saveSettingsDebounced,
@@ -2140,13 +2139,9 @@ export function initChatUtilities() {
await viewMessageFile(messageId, fileIndex);
});
$(document).on('click', '.assistant_note_export', async function () {
$(document).on('click', '.assistant_note_export', async function (_e) {
const chatToSave = [
{
user_name: name1,
character_name: name2,
chat_metadata: chat_metadata,
},
{ chat_metadata: chat_metadata },
...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
];
@@ -2865,7 +2865,7 @@ function getCharacterAvatarUrl() {
if (context.groupId) {
const groupMembers = context.groups.find(x => x.id === context.groupId)?.members;
const lastMessageAvatar = context.chat?.filter(x => !x.is_system && !x.is_user)?.slice(-1)[0]?.original_avatar;
const randomMemberAvatar = Array.isArray(groupMembers) ? groupMembers[Math.floor(Math.random() * groupMembers.length)]?.avatar : null;
const randomMemberAvatar = Array.isArray(groupMembers) ? groupMembers[Math.floor(Math.random() * groupMembers.length)] : null;
const avatarToUse = lastMessageAvatar || randomMemberAvatar;
return formatCharacterAvatar(avatarToUse);
} else {
+7 -3
View File
@@ -324,7 +324,7 @@ export async function getGroupChat(groupId, reload = false) {
* Retrieves the members of a group
*
* @param {string} [groupId=selected_group] - The ID of the group to retrieve members from. Defaults to the currently selected group.
* @returns {import('../script.js').Character[]} An array of character objects representing the members of the group. If the group is not found, an empty array is returned.
* @returns {Character[]} An array of character objects representing the members of the group. If the group is not found, an empty array is returned.
*/
export function getGroupMembers(groupId = selected_group) {
const group = groups.find((x) => x.id === groupId);
@@ -617,7 +617,11 @@ function resetSelectedGroup() {
*/
async function saveGroupChat(groupId, shouldSaveGroup, force = false) {
const group = groups.find(x => x.id == groupId);
const chat_id = group.chat_id;
if (!group) {
console.warn('Group not found', groupId);
return;
}
const chatId = group.chat_id;
group['date_last_chat'] = Date.now();
/** @type {ChatHeader} */
const chatHeader = {
@@ -626,7 +630,7 @@ async function saveGroupChat(groupId, shouldSaveGroup, force = false) {
const response = await fetch('/api/chats/group/save', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ id: chat_id, chat: [chatHeader, ...chat], force: force }),
body: JSON.stringify({ id: chatId, chat: [chatHeader, ...chat], force: force }),
});
if (!response.ok) {
+2
View File
@@ -1105,6 +1105,8 @@ function parseTimestamp(timestamp) {
ms = typeof ms !== 'undefined' ? `.${ms.padStart(3, '0')}` : '';
return `${year.padStart(4, '0')}-${month.padStart(2, '0')}-${day.padStart(2, '0')}T${hour.padStart(2, '0')}:${min.padStart(2, '0')}:${sec.padStart(2, '0')}${ms}Z`;
};
// 2024-07-12@01h31m37s123ms
dtFmt.push({ callback: convertFromHumanized, pattern: /(\d{4})-(\d{1,2})-(\d{1,2})@(\d{1,2})h(\d{1,2})m(\d{1,2})s(\d{1,3})ms/ });
// 2024-7-12@01h31m37s
dtFmt.push({ callback: convertFromHumanized, pattern: /(\d{4})-(\d{1,2})-(\d{1,2})@(\d{1,2})h(\d{1,2})m(\d{1,2})s/ });
// 2024-6-5 @14h 56m 50s 682ms
+3 -6
View File
@@ -2,7 +2,7 @@ import { promises as fsPromises } from 'node:fs';
import path from 'node:path';
import urlJoin from 'url-join';
import { DEFAULT_AVATAR_PATH } from './constants.js';
import { extractFileFromZipBuffer, humanizedISO8601DateTime } from './util.js';
import { extractFileFromZipBuffer } from './util.js';
/**
* A parser for BYAF (Backyard Archive Format) files.
@@ -267,7 +267,7 @@ export class ByafParser {
extensions: { ...(character?.displayName && { 'display_name': character?.displayName }) }, // Preserve display name unmodified using extensions. "display_name" is not used by SillyTavern currently.
},
// @ts-ignore Non-standard spec extension
create_date: humanizedISO8601DateTime(),
create_date: new Date().toISOString(),
};
}
/**
@@ -330,13 +330,10 @@ export class ByafParser {
* @returns {string} Chat data
*/
static getChatFromScenario(scenario, userName, characterName, chatBackgrounds) {
const chatStartDate = scenario?.messages?.length == 0 ? humanizedISO8601DateTime() : scenario?.messages?.filter(m => 'createdAt' in m)[0].createdAt;
const chatStartDate = scenario?.messages?.length == 0 ? new Date().toISOString() : scenario?.messages?.filter(m => 'createdAt' in m)[0].createdAt;
const chatBackground = chatBackgrounds.find(bg => bg.paths.includes(scenario?.backgroundImage || ''))?.name || '';
/** @type {object[]} */
const chat = [{
user_name: userName,
character_name: characterName,
create_date: chatStartDate,
chat_metadata: {
scenario: scenario?.narrative ?? '',
mes_example: ByafParser.formatExampleMessages(scenario?.exampleMessages),
+18 -21
View File
@@ -14,7 +14,7 @@ import storage from 'node-persist';
import { AVATAR_WIDTH, AVATAR_HEIGHT, DEFAULT_AVATAR_PATH } from '../constants.js';
import { default as validateAvatarUrlMiddleware, getFileNameValidationFunction } from '../middleware/validateFileName.js';
import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue, mutateJsonString, clientRelativePath, getUniqueName, sanitizeSafeCharacterReplacements } from '../util.js';
import { deepMerge, humanizedDateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue, mutateJsonString, clientRelativePath, getUniqueName, sanitizeSafeCharacterReplacements } from '../util.js';
import { TavernCardValidator } from '../validator/TavernCardValidator.js';
import { parse, read, write } from '../character-card-parser.js';
import { readWorldInfoFile } from './worldinfo.js';
@@ -414,7 +414,7 @@ const processCharacter = async (item, directories, { shallow }) => {
character['json_data'] = imgData;
const charStat = fs.statSync(path.join(directories.characters, item));
character['date_added'] = charStat.ctimeMs;
character['create_date'] = jsonObject['create_date'] || humanizedISO8601DateTime(charStat.ctimeMs);
character['create_date'] = jsonObject['create_date'] || new Date(Math.round(charStat.ctimeMs)).toISOString();
const chatsDirectory = path.join(directories.chats, item.replace('.png', ''));
const { chatSize, dateLastChat } = calculateChatSize(chatsDirectory);
@@ -452,7 +452,7 @@ function getCharaCardV2(jsonObject, directories, hoistDate = true) {
jsonObject = convertToV2(jsonObject, directories);
if (hoistDate && !jsonObject.create_date) {
jsonObject.create_date = humanizedISO8601DateTime();
jsonObject.create_date = new Date().toISOString();
}
} else {
jsonObject = readFromV2(jsonObject);
@@ -486,7 +486,7 @@ function convertToV2(char, directories) {
depth_prompt_role: char.depth_prompt_role,
}, directories);
result.chat = char.chat ?? humanizedISO8601DateTime();
result.chat = char.chat ?? `${char.name} - ${humanizedDateTime()}`;
result.create_date = char.create_date;
return result;
@@ -551,7 +551,7 @@ function readFromV2(char) {
char[charField] = v2Value;
});
char['chat'] = char['chat'] ?? humanizedISO8601DateTime();
char['chat'] = char['chat'] ?? `${char.name} - ${humanizedDateTime()}`;
return char;
}
@@ -587,7 +587,7 @@ function charaFormatData(data, directories) {
// Old ST extension fields (for backward compatibility, will be deprecated)
_.set(char, 'creatorcomment', data.creator_notes || '');
_.set(char, 'avatar', 'none');
_.set(char, 'chat', data.ch_name + ' - ' + humanizedISO8601DateTime());
_.set(char, 'chat', data.ch_name + ' - ' + humanizedDateTime());
_.set(char, 'talkativeness', data.talkativeness || 0.5);
_.set(char, 'fav', data.fav == 'true');
_.set(char, 'tags', typeof data.tags == 'string' ? (data.tags.split(',').map(x => x.trim()).filter(x => x)) : data.tags || []);
@@ -624,9 +624,6 @@ function charaFormatData(data, directories) {
_.set(char, 'data.extensions.depth_prompt.prompt', data.depth_prompt_prompt ?? '');
_.set(char, 'data.extensions.depth_prompt.depth', depth_value);
_.set(char, 'data.extensions.depth_prompt.role', role_value);
//_.set(char, 'data.extensions.create_date', humanizedISO8601DateTime());
//_.set(char, 'data.extensions.avatar', 'none');
//_.set(char, 'data.extensions.chat', data.ch_name + ' - ' + humanizedISO8601DateTime());
// V3 fields
_.set(char, 'data.group_only_greetings', data.group_only_greetings ?? []);
@@ -746,8 +743,8 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
'name': yamlData.name,
'description': yamlData.context ?? '',
'first_mes': yamlData.greeting ?? '',
'create_date': humanizedISO8601DateTime(),
'chat': `${yamlData.name} - ${humanizedISO8601DateTime()}`,
'create_date': new Date().toISOString(),
'chat': `${yamlData.name} - ${humanizedDateTime()}`,
'personality': '',
'creatorcomment': '',
'avatar': 'none',
@@ -800,7 +797,7 @@ async function importFromCharX(uploadPath, { request }, preservedFileName) {
}
unsetPrivateFields(card);
card['create_date'] = humanizedISO8601DateTime();
card['create_date'] = new Date().toISOString();
card.name = sanitize(card.name);
const fileName = preservedFileName || getPngName(card.name, request.user.directories);
const result = await writeCharacterData(avatar, JSON.stringify(card), fileName, request);
@@ -822,7 +819,7 @@ async function importFromByaf(uploadPath, { request }, preservedFileName) {
* @param {Partial<ByafScenario>} scenario
*/
const createChatAsCurrentPersona = (scenario) => {
const chatName = sanitize(`${scenario.title || card.name} - ${humanizedISO8601DateTime()} imported.jsonl`, { replacement: sanitizeSafeCharacterReplacements });
const chatName = sanitize(`${scenario.title || card.name} - ${humanizedDateTime()} imported.jsonl`, { replacement: sanitizeSafeCharacterReplacements });
const filePath = path.join(request.user.directories.chats, path.basename(fileName), chatName);
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -898,7 +895,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
importRisuSprites(request.user.directories, jsonData);
unsetPrivateFields(jsonData);
jsonData = readFromV2(jsonData);
jsonData['create_date'] = humanizedISO8601DateTime();
jsonData['create_date'] = new Date().toISOString();
const pngName = preservedFileName || getPngName(jsonData.data?.name || jsonData.name, request.user.directories);
const char = JSON.stringify(jsonData);
const result = await writeCharacterData(DEFAULT_AVATAR_PATH, char, pngName, request);
@@ -917,10 +914,10 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
'personality': jsonData.personality ?? '',
'first_mes': jsonData.first_mes ?? '',
'avatar': 'none',
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
'chat': jsonData.name + ' - ' + humanizedDateTime(),
'mes_example': jsonData.mes_example ?? '',
'scenario': jsonData.scenario ?? '',
'create_date': humanizedISO8601DateTime(),
'create_date': new Date().toISOString(),
'talkativeness': jsonData.talkativeness ?? 0.5,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
@@ -943,10 +940,10 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
'personality': '',
'first_mes': jsonData.char_greeting ?? '',
'avatar': 'none',
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
'chat': jsonData.name + ' - ' + humanizedDateTime(),
'mes_example': jsonData.example_dialogue ?? '',
'scenario': jsonData.world_scenario ?? '',
'create_date': humanizedISO8601DateTime(),
'create_date': new Date().toISOString(),
'talkativeness': jsonData.talkativeness ?? 0.5,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
@@ -981,7 +978,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
importRisuSprites(request.user.directories, jsonData);
unsetPrivateFields(jsonData);
jsonData = readFromV2(jsonData);
jsonData['create_date'] = humanizedISO8601DateTime();
jsonData['create_date'] = new Date().toISOString();
const char = JSON.stringify(jsonData);
const result = await writeCharacterData(uploadPath, char, pngName, request);
fs.unlinkSync(uploadPath);
@@ -1000,10 +997,10 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
'personality': jsonData.personality ?? '',
'first_mes': jsonData.first_mes ?? '',
'avatar': 'none',
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
'chat': jsonData.name + ' - ' + humanizedDateTime(),
'mes_example': jsonData.mes_example ?? '',
'scenario': jsonData.scenario ?? '',
'create_date': humanizedISO8601DateTime(),
'create_date': new Date().toISOString(),
'talkativeness': jsonData.talkativeness ?? 0.5,
'creator': jsonData.creator ?? '',
'tags': jsonData.tags ?? '',
+22 -24
View File
@@ -11,7 +11,7 @@ import _ from 'lodash';
import validateAvatarUrlMiddleware from '../middleware/validateFileName.js';
import {
getConfigValue,
humanizedISO8601DateTime,
humanizedDateTime,
tryParse,
generateTimestamp,
removeOldBackups,
@@ -106,9 +106,7 @@ process.on('exit', () => {
function importOobaChat(userName, characterName, jsonData) {
/** @type {object[]} */
const chat = [{
user_name: userName,
character_name: characterName,
create_date: humanizedISO8601DateTime(),
chat_metadata: {},
}];
for (const arr of jsonData.data_visible) {
@@ -116,8 +114,9 @@ function importOobaChat(userName, characterName, jsonData) {
const userMessage = {
name: userName,
is_user: true,
send_date: humanizedISO8601DateTime(),
send_date: new Date().toISOString(),
mes: arr[0],
extra: {},
};
chat.push(userMessage);
}
@@ -125,8 +124,9 @@ function importOobaChat(userName, characterName, jsonData) {
const charMessage = {
name: characterName,
is_user: false,
send_date: humanizedISO8601DateTime(),
send_date: new Date().toISOString(),
mes: arr[1],
extra: {},
};
chat.push(charMessage);
}
@@ -145,9 +145,7 @@ function importOobaChat(userName, characterName, jsonData) {
function importAgnaiChat(userName, characterName, jsonData) {
/** @type {object[]} */
const chat = [{
user_name: userName,
character_name: characterName,
create_date: humanizedISO8601DateTime(),
chat_metadata: {},
}];
for (const message of jsonData.messages) {
@@ -155,8 +153,9 @@ function importAgnaiChat(userName, characterName, jsonData) {
chat.push({
name: isUser ? userName : characterName,
is_user: isUser,
send_date: humanizedISO8601DateTime(),
send_date: new Date().toISOString(),
mes: message.msg,
extra: {},
});
}
@@ -178,16 +177,15 @@ function importCAIChat(userName, characterName, jsonData) {
*/
function convert(history) {
const starter = {
user_name: userName,
character_name: characterName,
create_date: humanizedISO8601DateTime(),
chat_metadata: {},
};
const historyData = history.msgs.map((msg) => ({
name: msg.src.is_human ? userName : characterName,
is_user: msg.src.is_human,
send_date: humanizedISO8601DateTime(),
send_date: new Date().toISOString(),
mes: msg.text,
extra: {},
}));
return [starter, ...historyData];
@@ -215,7 +213,8 @@ function importKoboldLiteChat(_userName, _characterName, data) {
name: isUser ? header.user_name : header.character_name,
is_user: isUser,
mes: msg.replaceAll(inputToken, '').replaceAll(outputToken, '').trim(),
send_date: Date.now(),
send_date: new Date().toISOString(),
extra: {},
};
}
@@ -276,9 +275,7 @@ function flattenChubChat(userName, characterName, lines) {
function importRisuChat(userName, characterName, jsonData) {
/** @type {object[]} */
const chat = [{
user_name: userName,
character_name: characterName,
create_date: humanizedISO8601DateTime(),
chat_metadata: {},
}];
for (const message of jsonData.data.message) {
@@ -286,8 +283,9 @@ function importRisuChat(userName, characterName, jsonData) {
chat.push({
name: message.name ?? (isUser ? userName : characterName),
is_user: isUser,
send_date: Number(message.time ?? Date.now()),
send_date: new Date(Number(message.time ?? Date.now())).toISOString(),
mes: message.data ?? '',
extra: {},
});
}
@@ -417,7 +415,7 @@ export async function getChatInfo(pathToFile, additionalData = {}, withMetadata
if (jsonData && (jsonData.name || jsonData.character_name || jsonData.chat_metadata)) {
chatData.chat_items = (itemCounter - 1);
chatData.mes = jsonData['mes'] || '[The message is empty]';
chatData.last_mes = jsonData['send_date'] || stats.mtimeMs;
chatData.last_mes = jsonData['send_date'] || new Date(Math.round(stats.mtimeMs)).toISOString();
res(chatData);
} else {
@@ -623,7 +621,7 @@ router.post('/group/import', function (request, response) {
return response.sendStatus(400);
}
const chatname = humanizedISO8601DateTime();
const chatname = humanizedDateTime();
const pathToUpload = path.join(filedata.destination, filedata.filename);
const pathToNewFile = path.join(request.user.directories.groupChats, `${chatname}.jsonl`);
fs.copyFileSync(pathToUpload, pathToNewFile);
@@ -675,7 +673,7 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response)
}
const handleChat = (chat) => {
const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
const fileName = `${characterName} - ${humanizedDateTime()} imported.jsonl`;
const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
fileNames.push(fileName);
writeFileAtomicSync(filePath, chat, 'utf8');
@@ -714,7 +712,7 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response)
console.warn('Failed to flatten Chub Chat data: ', error);
}
const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
const fileName = `${characterName} - ${humanizedDateTime()} imported.jsonl`;
const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
fileNames.push(fileName);
if (flattenedChat !== data) {
@@ -879,7 +877,7 @@ router.post('/search', validateAvatarUrlMiddleware, function (request, response)
}
const lastMessage = messages[messages.length - 1];
const lastMesDate = lastMessage?.send_date || Math.round(fs.statSync(chatFile.path).mtimeMs);
const lastMesDate = lastMessage?.send_date || new Date(fs.statSync(chatFile.path).mtimeMs).toISOString();
// If no search query, just return metadata
if (!query) {
+2 -2
View File
@@ -6,7 +6,7 @@ import express from 'express';
import sanitize from 'sanitize-filename';
import { sync as writeFileAtomicSync, default as writeFileAtomic } from 'write-file-atomic';
import { color, humanizedISO8601DateTime, tryParse } from '../util.js';
import { color, tryParse } from '../util.js';
import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
export const router = express.Router();
@@ -127,7 +127,7 @@ router.post('/all', (request, response) => {
const group = JSON.parse(fileContents);
const groupStat = fs.statSync(filePath);
group['date_added'] = groupStat.birthtimeMs;
group['create_date'] = humanizedISO8601DateTime(groupStat.birthtimeMs);
group['create_date'] = new Date(groupStat.birthtimeMs).toISOString();
let chat_size = 0;
let date_last_chat = 0;
+67 -65
View File
@@ -12,6 +12,21 @@ import { getAllUserHandles, getUserDirectories } from '../users.js';
const STATS_FILE = 'stats.json';
const monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
/**
* @type {Map<string, Object>} The stats object for each user.
*/
@@ -23,92 +38,79 @@ const TIMESTAMPS = new Map();
/**
* Convert a timestamp to an integer timestamp.
* (sorry, it's momentless for now, didn't want to add a package just for this)
* This function can handle several different timestamp formats:
* 1. Unix timestamps (the number of seconds since the Unix Epoch)
* 2. ST "humanized" timestamps, formatted like "YYYY-MM-DD @HHh MMm SSs ms"
* 3. Date strings in the format "Month DD, YYYY H:MMam/pm"
* 1. Date.now timestamps (the number of milliseconds since the Unix Epoch)
* 2. ST "humanized" timestamps, formatted like `YYYY-MM-DD@HHhMMmSSsMSms`
* 3. Date strings in the format `Month DD, YYYY H:MMam/pm`
* 4. ISO 8601 formatted strings
* 5. Date objects
*
* The function returns the timestamp as the number of milliseconds since
* the Unix Epoch, which can be converted to a JavaScript Date object with new Date().
*
* @param {string|number} timestamp - The timestamp to convert.
* @param {string|number|Date} timestamp - The timestamp to convert.
* @returns {number} The timestamp in milliseconds since the Unix Epoch, or 0 if the input cannot be parsed.
*
* @example
* // Unix timestamp
* timestampToMoment(1609459200);
* parseTimestamp(1609459200);
* // ST humanized timestamp
* timestampToMoment("2021-01-01 \@00h 00m 00s 000ms");
* parseTimestamp("2021-01-01 \@00h 00m 00s 000ms");
* // Date string
* timestampToMoment("January 1, 2021 12:00am");
* parseTimestamp("January 1, 2021 12:00am");
*/
function timestampToMoment(timestamp) {
function parseTimestamp(timestamp) {
if (!timestamp) {
return 0;
}
if (typeof timestamp === 'number') {
return timestamp;
// Date object
if (timestamp instanceof Date) {
return timestamp.getTime();
}
const pattern1 =
/(\d{4})-(\d{1,2})-(\d{1,2}) @(\d{1,2})h (\d{1,2})m (\d{1,2})s (\d{1,3})ms/;
const replacement1 = (
match,
year,
month,
day,
hour,
minute,
second,
millisecond,
) => {
return `${year}-${month.padStart(2, '0')}-${day.padStart(
2,
'0',
)}T${hour.padStart(2, '0')}:${minute.padStart(
2,
'0',
)}:${second.padStart(2, '0')}.${millisecond.padStart(3, '0')}Z`;
};
const isoTimestamp1 = timestamp.replace(pattern1, replacement1);
if (!isNaN(Number(new Date(isoTimestamp1)))) {
return new Date(isoTimestamp1).getTime();
// Unix time
if (typeof timestamp === 'number' || /^\d+$/.test(timestamp)) {
const unixTime = Number(timestamp);
const isValid = Number.isFinite(unixTime) && !Number.isNaN(unixTime) && unixTime >= 0;
if (!isValid) return 0;
return new Date(unixTime).getTime();
}
const pattern2 = /(\w+)\s(\d{1,2}),\s(\d{4})\s(\d{1,2}):(\d{1,2})(am|pm)/i;
const replacement2 = (match, month, day, year, hour, minute, meridiem) => {
const monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
// ISO 8601 format
const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
if (isoPattern.test(timestamp)) {
return new Date(timestamp).getTime();
}
let dateFormats = [];
// meridiem-based format
const convertFromMeridiemBased = (_, month, day, year, hour, minute, meridiem) => {
const monthNum = monthNames.indexOf(month) + 1;
const hour24 =
meridiem.toLowerCase() === 'pm'
? (parseInt(hour, 10) % 12) + 12
: parseInt(hour, 10) % 12;
return `${year}-${monthNum.toString().padStart(2, '0')}-${day.padStart(
2,
'0',
)}T${hour24.toString().padStart(2, '0')}:${minute.padStart(
2,
'0',
)}:00Z`;
const hour24 = meridiem.toLowerCase() === 'pm' ? (parseInt(hour, 10) % 12) + 12 : parseInt(hour, 10) % 12;
return `${year}-${monthNum}-${day.padStart(2, '0')}T${hour24.toString().padStart(2, '0')}:${minute.padStart(2, '0')}:00`;
};
const isoTimestamp2 = timestamp.replace(pattern2, replacement2);
if (!isNaN(Number(new Date(isoTimestamp2)))) {
return new Date(isoTimestamp2).getTime();
// June 19, 2023 2:20pm
dateFormats.push({ callback: convertFromMeridiemBased, pattern: /(\w+)\s(\d{1,2}),\s(\d{4})\s(\d{1,2}):(\d{1,2})(am|pm)/i });
// ST "humanized" format patterns
const convertFromHumanized = (_, year, month, day, hour, min, sec, ms) => {
ms = typeof ms !== 'undefined' ? `.${ms.padStart(3, '0')}` : '';
return `${year.padStart(4, '0')}-${month.padStart(2, '0')}-${day.padStart(2, '0')}T${hour.padStart(2, '0')}:${min.padStart(2, '0')}:${sec.padStart(2, '0')}${ms}Z`;
};
// 2024-07-12@01h31m37s123ms
dateFormats.push({ callback: convertFromHumanized, pattern: /(\d{4})-(\d{1,2})-(\d{1,2})@(\d{1,2})h(\d{1,2})m(\d{1,2})s(\d{1,3})ms/ });
// 2024-7-12@01h31m37s
dateFormats.push({ callback: convertFromHumanized, pattern: /(\d{4})-(\d{1,2})-(\d{1,2})@(\d{1,2})h(\d{1,2})m(\d{1,2})s/ });
// 2024-6-5 @14h 56m 50s 682ms
dateFormats.push({ callback: convertFromHumanized, pattern: /(\d{4})-(\d{1,2})-(\d{1,2}) @(\d{1,2})h (\d{1,2})m (\d{1,2})s (\d{1,3})ms/ });
for (const x of dateFormats) {
const rgxMatch = timestamp.match(x.pattern);
if (!rgxMatch) continue;
const isoTimestamp = x.callback(...rgxMatch);
return new Date(isoTimestamp).getTime();
}
return 0;
@@ -416,7 +418,7 @@ function calculateTotalGenTimeAndWordCount(
// If this is the first user message, set the first chat time
if (json.is_user) {
//get min between firstChatTime and timestampToMoment(json.send_date)
firstChatTime = Math.min(timestampToMoment(json.send_date), firstChatTime);
firstChatTime = Math.min(parseTimestamp(json.send_date), firstChatTime);
}
} catch (error) {
console.error(`Error parsing line ${line}: ${error}`);
+21 -11
View File
@@ -389,17 +389,27 @@ export function uuidv4() {
});
}
export function humanizedISO8601DateTime(date) {
let baseDate = typeof date === 'number' ? new Date(date) : new Date();
let humanYear = baseDate.getFullYear();
let humanMonth = (baseDate.getMonth() + 1);
let humanDate = baseDate.getDate();
let humanHour = (baseDate.getHours() < 10 ? '0' : '') + baseDate.getHours();
let humanMinute = (baseDate.getMinutes() < 10 ? '0' : '') + baseDate.getMinutes();
let humanSecond = (baseDate.getSeconds() < 10 ? '0' : '') + baseDate.getSeconds();
let humanMillisecond = (baseDate.getMilliseconds() < 10 ? '0' : '') + baseDate.getMilliseconds();
let HumanizedDateTime = (humanYear + '-' + humanMonth + '-' + humanDate + ' @' + humanHour + 'h ' + humanMinute + 'm ' + humanSecond + 's ' + humanMillisecond + 'ms');
return HumanizedDateTime;
/**
* Gets a humanized date time string from a given timestamp.
* @param {number} timestamp Timestamp in milliseconds
* @returns {string} Humanized date time string in the format `YYYY-MM-DD@HHhMMmSSsMSms`
*/
export function humanizedDateTime(timestamp = Date.now()) {
const date = new Date(timestamp);
const dt = {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
hour: date.getHours(),
minute: date.getMinutes(),
second: date.getSeconds(),
millisecond: date.getMilliseconds(),
};
for (const key in dt) {
const padLength = key === 'millisecond' ? 3 : 2;
dt[key] = dt[key].toString().padStart(padLength, '0');
}
return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s${dt.millisecond}ms`;
}
export function tryParse(str) {