1.3 fixes and changes

This commit is contained in:
Tea 2025-12-10 18:28:48 -05:00
parent 419d025e58
commit d6c83812af
3 changed files with 194 additions and 19 deletions

209
main.ts
View file

@ -10,6 +10,8 @@ interface MultimuseObsidianSettings {
enabled: boolean;
apiKey: string; // API key for authentication (Bearer token)
cachedUserId: string; // Cached user ID from API key (auto-populated)
trackRoleplay: boolean; // Whether to add Roleplay property from folder path
trackIsActive: boolean; // Whether to add Is Active? property (defaulting to true)
}
const DEFAULT_SETTINGS: MultimuseObsidianSettings = {
@ -21,7 +23,9 @@ const DEFAULT_SETTINGS: MultimuseObsidianSettings = {
userIds: '', // Deprecated - auto-synced from API key
enabled: true,
apiKey: '',
cachedUserId: '' // Auto-populated from API key
cachedUserId: '', // Auto-populated from API key
trackRoleplay: true, // Default: extract Roleplay from folder path
trackIsActive: true // Default: add Is Active? property
};
interface MuseInfo {
@ -36,6 +40,7 @@ export default class MultimuseObsidian extends Plugin {
settings: MultimuseObsidianSettings;
pollIntervalId: number | null = null;
museCache: Map<string, MuseInfo[]> = new Map(); // user_id (as string) -> muses
recentlyCreatedFiles: Set<string> = new Set(); // Track recently created files to skip immediate checking
async onload() {
await this.loadSettings();
@ -405,6 +410,12 @@ export default class MultimuseObsidian extends Plugin {
// Process each scene file
for (const file of sceneFiles) {
try {
// Skip checking if this file was recently created by the plugin
if (this.recentlyCreatedFiles.has(file.path)) {
console.log(`[MultimuseObsidian] checkAllThreadsViaBotApi: Skipping recently created file: ${file.path}`);
continue;
}
const cache = this.app.metadataCache.getFileCache(file);
if (!cache || !cache.frontmatter) {
continue;
@ -485,6 +496,12 @@ export default class MultimuseObsidian extends Plugin {
async querySceneState(file: TFile): Promise<boolean> {
/**Query the tracker API for a specific scene's state and update frontmatter.*/
// Skip checking if this file was recently created by the plugin
if (this.recentlyCreatedFiles.has(file.path)) {
console.log(`[MultimuseObsidian] querySceneState: Skipping check for recently created file: ${file.path}`);
return false;
}
const cache = this.app.metadataCache.getFileCache(file);
if (!cache || !cache.frontmatter) {
return false;
@ -538,10 +555,19 @@ export default class MultimuseObsidian extends Plugin {
// If scene is not tracked, don't update anything
if (!data.tracked || !data.state) {
console.log(`[MultimuseObsidian] Scene ${file.basename} is not tracked (tracked: ${data.tracked}, has state: ${!!data.state}) - skipping update`);
return false;
}
const state = data.state;
console.log(`[MultimuseObsidian] Scene ${file.basename} is tracked. State:`, JSON.stringify(state));
// Only update if state.replied is explicitly defined (not undefined/null)
if (state.replied === undefined || state.replied === null) {
console.log(`[MultimuseObsidian] Scene ${file.basename} has undefined/null replied value - skipping update`);
return false;
}
return await this.updateSceneFromState(file, cache, state);
} catch (error) {
console.error(`[MultimuseObsidian] Error querying scene state for ${file.path}:`, error);
@ -553,12 +579,29 @@ export default class MultimuseObsidian extends Plugin {
/**Update scene frontmatter from API state data.*/
let updated = false;
// Only update if state.replied is explicitly defined (boolean or string 'true'/'false')
if (state.replied === undefined || state.replied === null) {
console.log(`[MultimuseObsidian] ${file.basename}: state.replied is undefined/null - skipping update`);
return false;
}
// Check if the state looks suspicious (e.g., bot couldn't access channel)
// If timestamp is null and your_last_post is null, it might indicate the bot couldn't read the channel
// In this case, don't update the Replied? field to avoid incorrect updates
if (state.timestamp === null && state.your_last_post === null && state.posted_since_count === 0) {
console.log(`[MultimuseObsidian] ${file.basename}: State appears invalid (timestamp and your_last_post are null) - likely bot can't access channel. Skipping update to prevent incorrect "Replied?" value.`);
return false;
}
// Update Replied? field - normalize boolean values for comparison
const currentRepliedRaw = cache.frontmatter['Replied?'];
// Handle both boolean and string values
const currentReplied = currentRepliedRaw === true || currentRepliedRaw === 'true' || currentRepliedRaw === 'True';
// Only treat as true if explicitly true or 'true' string
const shouldBeReplied = state.replied === true || state.replied === 'true'; // true = your turn, false = not your turn
console.log(`[MultimuseObsidian] ${file.basename}: Current Replied?=${currentReplied}, API replied=${state.replied}, shouldBeReplied=${shouldBeReplied}`);
if (currentReplied !== shouldBeReplied) {
console.log(`[MultimuseObsidian] ${file.basename}: Updated Replied? to ${shouldBeReplied}`);
await this.updateFrontmatter(file, 'Replied?', shouldBeReplied);
@ -591,6 +634,12 @@ export default class MultimuseObsidian extends Plugin {
return;
}
// Skip checking if this file was recently created by the plugin
if (this.recentlyCreatedFiles.has(file.path)) {
console.log(`[MultimuseObsidian] Skipping check for recently created file: ${file.path}`);
return;
}
// Small delay to avoid checking during file creation
await new Promise(resolve => setTimeout(resolve, 1000));
@ -778,11 +827,14 @@ export default class MultimuseObsidian extends Plugin {
// 2) Select muse
const museOptions = muses.map(m => m.name);
const selectedMuseIndex = await this.showSuggester(museOptions, museOptions);
const selectedMuseIndex = await this.showSuggester(museOptions, museOptions, 'Select a muse');
if (selectedMuseIndex === null || selectedMuseIndex < 0) return;
const selectedMuse = muses[selectedMuseIndex];
// Small delay to ensure previous modal is fully closed
await new Promise(resolve => setTimeout(resolve, 100));
// 3) Get Discord thread/channel link
const threadUrl = await this.showInputPrompt('Enter Discord thread/channel URL');
if (!threadUrl) return;
@ -793,8 +845,13 @@ export default class MultimuseObsidian extends Plugin {
return;
}
// 4) Get location (RP folder)
const location = await this.selectSceneLocation();
// Small delay before opening location modal
await new Promise(resolve => setTimeout(resolve, 100));
// 4) Get location (RP folder) - pass muse name for context
console.log(`[MultimuseObsidian] createNewScene: About to select location for muse "${selectedMuse.name}"`);
const location = await this.selectSceneLocation(`muse "${selectedMuse.name}"`);
console.log(`[MultimuseObsidian] createNewScene: Selected location: ${location || 'null (cancelled)'}`);
if (!location) return;
// 5) Get scene name
@ -807,7 +864,7 @@ export default class MultimuseObsidian extends Plugin {
// 7) Create scene file
const filePath = `${location}/${sceneName}.md`;
const frontmatter = {
const frontmatter: Record<string, any> = {
'Link': threadUrl,
'Characters': [selectedMuse.name],
'Participants': participants,
@ -815,6 +872,19 @@ export default class MultimuseObsidian extends Plugin {
'Created': new Date().toISOString().split('T')[0],
};
// Add Roleplay property if enabled
if (this.settings.trackRoleplay) {
const roleplay = this.extractRoleplayFromPath(location);
if (roleplay) {
frontmatter['Roleplay'] = roleplay;
}
}
// Add Is Active? property if enabled
if (this.settings.trackIsActive) {
frontmatter['Is Active?'] = true;
}
const frontmatterLines = ['---'];
for (const [key, value] of Object.entries(frontmatter)) {
if (Array.isArray(value)) {
@ -831,15 +901,35 @@ export default class MultimuseObsidian extends Plugin {
const content = frontmatterLines.join('\n');
// Ensure folder exists
const folderPath = location;
const folder = this.app.vault.getAbstractFileByPath(folderPath);
if (!folder) {
await this.app.vault.createFolder(folderPath);
// Ensure all folders in the path exist (create recursively)
const segs = location.split("/").filter(Boolean);
let currentPath = segs[0];
// Ensure root folder exists
let abs = this.app.vault.getAbstractFileByPath(currentPath);
if (!abs) {
await this.app.vault.createFolder(currentPath);
}
// Create nested folders
for (let i = 1; i < segs.length; i++) {
currentPath += "/" + segs[i];
const folder = this.app.vault.getAbstractFileByPath(currentPath);
if (!folder) {
await this.app.vault.createFolder(currentPath);
}
}
// Create file
const createdFile = await this.app.vault.create(filePath, content);
// Mark this file as recently created to skip immediate checking
this.recentlyCreatedFiles.add(filePath);
// Remove from the set after 60 seconds (enough time for the scene to be registered and settled with the API)
setTimeout(() => {
this.recentlyCreatedFiles.delete(filePath);
console.log(`[MultimuseObsidian] Removed ${filePath} from recently created files - will now be checked by polling`);
}, 60000);
// 8) Register with bot API and optionally create thread tracking
try {
@ -1031,7 +1121,7 @@ export default class MultimuseObsidian extends Plugin {
const sceneName = thread.thread_name || `${museName} - Thread ${threadId}`;
const filePath = `${location}/${sceneName}.md`;
const frontmatter = {
const frontmatter: Record<string, any> = {
'Link': threadUrl,
'Characters': [museName],
'Participants': participants,
@ -1039,6 +1129,19 @@ export default class MultimuseObsidian extends Plugin {
'Created': new Date().toISOString().split('T')[0],
};
// Add Roleplay property if enabled
if (this.settings.trackRoleplay) {
const roleplay = this.extractRoleplayFromPath(location);
if (roleplay) {
frontmatter['Roleplay'] = roleplay;
}
}
// Add Is Active? property if enabled
if (this.settings.trackIsActive) {
frontmatter['Is Active?'] = true;
}
const frontmatterLines = ['---'];
for (const [key, value] of Object.entries(frontmatter)) {
if (Array.isArray(value)) {
@ -1055,14 +1158,34 @@ export default class MultimuseObsidian extends Plugin {
const content = frontmatterLines.join('\n');
// Ensure folder exists
const folder = this.app.vault.getAbstractFileByPath(location);
if (!folder) {
await this.app.vault.createFolder(location);
// Ensure all folders in the path exist (create recursively)
const segs = location.split("/").filter(Boolean);
let currentPath = segs[0];
// Ensure root folder exists
let abs = this.app.vault.getAbstractFileByPath(currentPath);
if (!abs) {
await this.app.vault.createFolder(currentPath);
}
// Create nested folders
for (let i = 1; i < segs.length; i++) {
currentPath += "/" + segs[i];
const folder = this.app.vault.getAbstractFileByPath(currentPath);
if (!folder) {
await this.app.vault.createFolder(currentPath);
}
}
// Create file
const file = await this.app.vault.create(filePath, content);
// Mark this file as recently created to skip immediate checking
this.recentlyCreatedFiles.add(filePath);
// Remove from the set after 30 seconds (enough time for the scene to be registered with the API)
setTimeout(() => {
this.recentlyCreatedFiles.delete(filePath);
}, 30000);
// Register with bot
// Use primary user ID for scene registration
@ -1219,6 +1342,26 @@ export default class MultimuseObsidian extends Plugin {
return null;
}
/**
* Extract the Roleplay property from a folder path.
* Roleplay is the first folder under the scenes folder.
* @param folderPath Full folder path (e.g., "RP Scenes/For The Greeks/Twin Flames")
* @returns Roleplay name (e.g., "For The Greeks") or null if path is invalid
*/
extractRoleplayFromPath(folderPath: string): string | null {
const RP_ROOT = this.settings.scenesFolder;
if (!folderPath.startsWith(RP_ROOT + "/")) {
return null;
}
// Remove RP_ROOT prefix and split
const relPath = folderPath.slice(RP_ROOT.length + 1);
const parts = relPath.split("/").filter(p => p.length > 0);
// First part is the Roleplay
return parts.length > 0 ? parts[0] : null;
}
async selectSceneLocation(context?: string): Promise<string | null> {
/**Select or create scene location folder.
* @param context Optional context string (e.g., muse name) to display in the prompt
@ -1244,6 +1387,7 @@ export default class MultimuseObsidian extends Plugin {
.filter(Boolean)
.sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));
// Always add the option to create a new folder path
options.push("+ New folder path…");
// Build title with context if provided
@ -1251,8 +1395,12 @@ export default class MultimuseObsidian extends Plugin {
? `Select location for ${context}`
: 'Select scene location';
console.log(`[MultimuseObsidian] selectSceneLocation: Showing ${options.length} options with title: ${suggesterTitle}`);
const choiceIndex = await this.showSuggester(options, options, suggesterTitle);
if (choiceIndex === null) return null;
if (choiceIndex === null) {
console.log(`[MultimuseObsidian] selectSceneLocation: User cancelled or no selection`);
return null;
}
const choice = options[choiceIndex];
if (!choice) return null;
@ -1337,7 +1485,11 @@ export default class MultimuseObsidian extends Plugin {
}
})(this.app, items, values, title);
modal.open();
console.log(`[MultimuseObsidian] showSuggester: Opening modal with ${items.length} items, title: ${title || 'Select an option'}`);
// Use requestAnimationFrame to ensure modal opens after any previous modals are fully closed
requestAnimationFrame(() => {
modal.open();
});
});
}
@ -1590,6 +1742,29 @@ class MultimuseObsidianSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
// Scene Properties Tracking
containerEl.createEl('h3', { text: 'Scene Properties' });
new Setting(containerEl)
.setName('Track Roleplay Property')
.setDesc(`Automatically add "Roleplay" property to scene files based on the selected folder (e.g., "For The Greeks" from "${this.plugin.settings.scenesFolder}/For The Greeks/Twin Flames")`)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.trackRoleplay)
.onChange(async (value) => {
this.plugin.settings.trackRoleplay = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Track Is Active? Property')
.setDesc('Automatically add "Is Active?" property to new scene files (defaults to true)')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.trackIsActive)
.onChange(async (value) => {
this.plugin.settings.trackIsActive = value;
await this.plugin.saveSettings();
}));
// Bot API URL - Hidden from user for security (uses hardcoded default)
// Removed from settings UI to prevent exposing server IP address

View file

@ -1,7 +1,7 @@
{
"id": "multimuse-obsidian",
"name": "Multimuse Obsidian",
"version": "1.0.0",
"version": "1.3.0",
"minAppVersion": "1.0.0",
"description": "Roleplay sync tool for use with the MultiMuse bot in discord",
"author": "BackstagePass Group",

View file

@ -1,6 +1,6 @@
{
"name": "multimuse-obsidian",
"version": "1.0.0",
"version": "1.3.0",
"description": "Obsidian plugin for MultiMuse bot integration - track Discord threads and send messages as muses",
"main": "main.js",
"scripts": {