mirror of
https://github.com/hanzoskill/botlink.git
synced 2026-10-30 21:01:49 +00:00
276 lines
7.2 KiB
JavaScript
276 lines
7.2 KiB
JavaScript
/**
|
|
* ClawdLink Delivery Preferences
|
|
*
|
|
* Controls how and when messages are delivered to the user.
|
|
* Respects quiet hours, batching, urgency, and personal style.
|
|
*/
|
|
|
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
import { homedir } from 'os';
|
|
import { join } from 'path';
|
|
|
|
const DATA_DIR = join(homedir(), '.bot', 'clawdlink');
|
|
const PREFS_FILE = join(DATA_DIR, 'preferences.json');
|
|
const HELD_FILE = join(DATA_DIR, 'held_messages.json');
|
|
|
|
const DEFAULT_PREFERENCES = {
|
|
// Schedule
|
|
schedule: {
|
|
quietHours: {
|
|
enabled: false,
|
|
start: "22:00",
|
|
end: "08:00"
|
|
},
|
|
batchDelivery: {
|
|
enabled: false,
|
|
times: ["09:00", "18:00"] // Deliver batched messages at these times
|
|
},
|
|
timezone: "America/Los_Angeles"
|
|
},
|
|
|
|
// Delivery rules
|
|
delivery: {
|
|
allowUrgentDuringQuiet: true,
|
|
summarizeFirst: true, // Show summary before full message
|
|
includeContext: true, // Include sender's context/reason
|
|
maxPerDelivery: 10 // Don't overwhelm with too many at once
|
|
},
|
|
|
|
// Communication style
|
|
style: {
|
|
tone: "natural", // natural | formal | casual | brief
|
|
voice: null, // Custom voice description
|
|
rewriteMessages: false, // Adapt messages to my style
|
|
greetingStyle: "friendly" // How Hanzo Bot introduces messages
|
|
},
|
|
|
|
// Per-friend overrides
|
|
friends: {
|
|
// "Friend Name": { priority: "high", alwaysDeliver: true, customTone: "casual" }
|
|
},
|
|
|
|
// Context preferences
|
|
context: {
|
|
workHours: { start: "09:00", end: "18:00" },
|
|
preferredContexts: ["personal", "work", "social"], // In priority order
|
|
muteContexts: [] // Contexts to always batch
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Load preferences (with defaults)
|
|
*/
|
|
export function loadPreferences() {
|
|
if (!existsSync(PREFS_FILE)) {
|
|
return { ...DEFAULT_PREFERENCES };
|
|
}
|
|
|
|
const saved = JSON.parse(readFileSync(PREFS_FILE, 'utf8'));
|
|
// Merge with defaults to ensure all fields exist
|
|
return deepMerge(DEFAULT_PREFERENCES, saved);
|
|
}
|
|
|
|
/**
|
|
* Save preferences
|
|
*/
|
|
export function savePreferences(prefs) {
|
|
writeFileSync(PREFS_FILE, JSON.stringify(prefs, null, 2));
|
|
}
|
|
|
|
/**
|
|
* Update specific preference
|
|
*/
|
|
export function updatePreference(path, value) {
|
|
const prefs = loadPreferences();
|
|
setNestedValue(prefs, path, value);
|
|
savePreferences(prefs);
|
|
return prefs;
|
|
}
|
|
|
|
/**
|
|
* Load held messages (messages waiting for delivery)
|
|
*/
|
|
export function loadHeldMessages() {
|
|
if (!existsSync(HELD_FILE)) {
|
|
return [];
|
|
}
|
|
return JSON.parse(readFileSync(HELD_FILE, 'utf8'));
|
|
}
|
|
|
|
/**
|
|
* Save held messages
|
|
*/
|
|
export function saveHeldMessages(messages) {
|
|
writeFileSync(HELD_FILE, JSON.stringify(messages, null, 2));
|
|
}
|
|
|
|
/**
|
|
* Hold a message for later delivery
|
|
*/
|
|
export function holdMessage(message, reason) {
|
|
const held = loadHeldMessages();
|
|
held.push({
|
|
...message,
|
|
heldAt: new Date().toISOString(),
|
|
heldReason: reason
|
|
});
|
|
saveHeldMessages(held);
|
|
}
|
|
|
|
/**
|
|
* Check if current time is in quiet hours
|
|
*/
|
|
export function isQuietHours(prefs) {
|
|
if (!prefs.schedule.quietHours.enabled) return false;
|
|
|
|
const now = new Date();
|
|
const currentTime = now.toLocaleTimeString('en-US', {
|
|
hour12: false,
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
timeZone: prefs.schedule.timezone
|
|
});
|
|
|
|
const start = prefs.schedule.quietHours.start;
|
|
const end = prefs.schedule.quietHours.end;
|
|
|
|
// Handle overnight quiet hours (e.g., 22:00 - 08:00)
|
|
if (start > end) {
|
|
return currentTime >= start || currentTime < end;
|
|
}
|
|
return currentTime >= start && currentTime < end;
|
|
}
|
|
|
|
/**
|
|
* Check if current time is a batch delivery time
|
|
*/
|
|
export function isBatchDeliveryTime(prefs) {
|
|
if (!prefs.schedule.batchDelivery.enabled) return false;
|
|
|
|
const now = new Date();
|
|
const currentTime = now.toLocaleTimeString('en-US', {
|
|
hour12: false,
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
timeZone: prefs.schedule.timezone
|
|
});
|
|
|
|
// Check if within 5 minutes of a batch time
|
|
for (const batchTime of prefs.schedule.batchDelivery.times) {
|
|
const [batchHour, batchMin] = batchTime.split(':').map(Number);
|
|
const [currentHour, currentMin] = currentTime.split(':').map(Number);
|
|
|
|
const batchMinutes = batchHour * 60 + batchMin;
|
|
const currentMinutes = currentHour * 60 + currentMin;
|
|
|
|
if (Math.abs(currentMinutes - batchMinutes) <= 5) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determine if a message should be delivered now
|
|
*/
|
|
export function shouldDeliverNow(message, prefs) {
|
|
const friendName = message.from;
|
|
const friendPrefs = prefs.friends[friendName] || {};
|
|
const urgency = message.content?.urgency || 'normal';
|
|
const context = message.content?.context || 'personal';
|
|
|
|
// Friend-specific override: always deliver
|
|
if (friendPrefs.alwaysDeliver) {
|
|
return { deliver: true, reason: 'Friend marked as always deliver' };
|
|
}
|
|
|
|
// Quiet hours check
|
|
if (isQuietHours(prefs)) {
|
|
if (urgency === 'urgent' && prefs.delivery.allowUrgentDuringQuiet) {
|
|
return { deliver: true, reason: 'Urgent message during quiet hours' };
|
|
}
|
|
return { deliver: false, reason: 'Quiet hours', holdUntil: 'quiet_end' };
|
|
}
|
|
|
|
// Batch delivery check
|
|
if (prefs.schedule.batchDelivery.enabled) {
|
|
// Check if friend is excluded from batching
|
|
if (friendPrefs.priority === 'high') {
|
|
return { deliver: true, reason: 'High priority friend' };
|
|
}
|
|
|
|
// Check if it's batch delivery time
|
|
if (isBatchDeliveryTime(prefs)) {
|
|
return { deliver: true, reason: 'Batch delivery time' };
|
|
}
|
|
|
|
// Check if context should be batched
|
|
if (prefs.context.muteContexts.includes(context)) {
|
|
return { deliver: false, reason: 'Context set to batch', holdUntil: 'batch_time' };
|
|
}
|
|
|
|
// If batching is on but not urgent, hold
|
|
if (urgency !== 'urgent') {
|
|
return { deliver: false, reason: 'Batch delivery enabled', holdUntil: 'batch_time' };
|
|
}
|
|
}
|
|
|
|
// Default: deliver
|
|
return { deliver: true, reason: 'Default delivery' };
|
|
}
|
|
|
|
/**
|
|
* Get friend-specific preferences
|
|
*/
|
|
export function getFriendPrefs(friendName) {
|
|
const prefs = loadPreferences();
|
|
return prefs.friends[friendName] || {};
|
|
}
|
|
|
|
/**
|
|
* Set friend-specific preferences
|
|
*/
|
|
export function setFriendPrefs(friendName, friendPrefs) {
|
|
const prefs = loadPreferences();
|
|
prefs.friends[friendName] = { ...prefs.friends[friendName], ...friendPrefs };
|
|
savePreferences(prefs);
|
|
}
|
|
|
|
// Utility functions
|
|
function deepMerge(target, source) {
|
|
const result = { ...target };
|
|
for (const key of Object.keys(source)) {
|
|
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
} else {
|
|
result[key] = source[key];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function setNestedValue(obj, path, value) {
|
|
const keys = path.split('.');
|
|
let current = obj;
|
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
if (!current[keys[i]]) current[keys[i]] = {};
|
|
current = current[keys[i]];
|
|
}
|
|
current[keys[keys.length - 1]] = value;
|
|
}
|
|
|
|
export default {
|
|
loadPreferences,
|
|
savePreferences,
|
|
updatePreference,
|
|
loadHeldMessages,
|
|
saveHeldMessages,
|
|
holdMessage,
|
|
isQuietHours,
|
|
isBatchDeliveryTime,
|
|
shouldDeliverNow,
|
|
getFriendPrefs,
|
|
setFriendPrefs,
|
|
DEFAULT_PREFERENCES
|
|
};
|