feat: spinner, clean output, multi-cycle loop after !stop
This commit is contained in:
parent
537f5a52e5
commit
952606cf7e
28
main.py
28
main.py
@ -108,23 +108,25 @@ def main():
|
||||
|
||||
print("\nPoste '!start Name' im Chat um zu beginnen.\n")
|
||||
|
||||
window = monitor.run()
|
||||
|
||||
resolver = Resolver(cache_path=CACHE_PATH, page=page)
|
||||
print(f"\nLöse {len(window.entries)} E-Mail-Adresse(n) auf...")
|
||||
resolved_entries = []
|
||||
for entry in window.entries:
|
||||
email = resolver.resolve(entry.display_name)
|
||||
print(f" {entry.display_name} → {email}")
|
||||
resolved_entries.append(AuditEntry(display_name=entry.display_name, email=email))
|
||||
|
||||
window.entries = resolved_entries
|
||||
while True:
|
||||
window = monitor.run_once()
|
||||
|
||||
memo_content = generate_memo(window)
|
||||
path = save_memo(memo_content)
|
||||
print(f"\nLöse {len(window.entries)} E-Mail-Adresse(n) auf...")
|
||||
resolved_entries = []
|
||||
for entry in window.entries:
|
||||
email = resolver.resolve(entry.display_name)
|
||||
print(f" {entry.display_name} → {email}")
|
||||
resolved_entries.append(AuditEntry(display_name=entry.display_name, email=email))
|
||||
window.entries = resolved_entries
|
||||
|
||||
print(f"\nMemo gespeichert: {path}")
|
||||
print("\n" + memo_content)
|
||||
memo_content = generate_memo(window)
|
||||
path = save_memo(memo_content)
|
||||
|
||||
print(f"\nMemo gespeichert: {path}\n")
|
||||
print(memo_content)
|
||||
print("\n" + "─" * 60 + "\n")
|
||||
|
||||
context.close()
|
||||
|
||||
|
||||
@ -1,27 +1,24 @@
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from teampulse.models import AuditEntry, AuditWindow, ChatMessage
|
||||
|
||||
# Selectors verified against Teams Web DOM (2026-05-17)
|
||||
# Supports both channel meetings (channel-pane-message) and meeting chat (chat-pane-message)
|
||||
_MSG_SELECTOR = "[data-tid='channel-pane-message'], [data-tid='chat-pane-message']"
|
||||
_PROFILE_EMAIL_SELECTOR = ".lpc_ip_root_class a[href*='mailto:']"
|
||||
|
||||
_START_RE = re.compile(r'^!start(?:\s+(.+))?$', re.IGNORECASE)
|
||||
_STOP_RE = re.compile(r'^!stop$', re.IGNORECASE)
|
||||
_QUOTE_CHARS = '"“”' # straight, left and right double quote
|
||||
_SPINNER = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
||||
|
||||
# JavaScript that extracts (sender, text, id) from both channel and meeting chat DOM.
|
||||
# Use raw string so \s, \d etc. pass through to JavaScript unchanged.
|
||||
_POLL_JS = r"""() => {
|
||||
const msgs = [];
|
||||
|
||||
// Channel meeting: channel-pane-message contains reply-message-header + message-body siblings
|
||||
// Filter to visible elements only using bounding rect (offsetParent fails with fixed layouts)
|
||||
const isVisible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
|
||||
|
||||
// Channel meeting: reply-message-header + message-body are siblings inside channel-pane-message
|
||||
const channelPanes = Array.from(
|
||||
document.querySelectorAll("[data-tid='channel-pane-message']")
|
||||
).filter(isVisible);
|
||||
@ -40,21 +37,18 @@ _POLL_JS = r"""() => {
|
||||
senderIdx++;
|
||||
} else if (tid === 'message-body' && currentSender) {
|
||||
const text = el.innerText.trim();
|
||||
if (text) {
|
||||
msgs.push({
|
||||
sender: currentSender,
|
||||
text: text,
|
||||
id: paneIdx + '_' + senderIdx + '_' + elIdx + '_' + text.substring(0, 20)
|
||||
});
|
||||
}
|
||||
if (text) msgs.push({
|
||||
sender: currentSender,
|
||||
text: text,
|
||||
id: paneIdx + '_' + senderIdx + '_' + elIdx + '_' + text.substring(0, 20)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return msgs;
|
||||
}
|
||||
|
||||
// Meeting chat: message-author-name is a SIBLING of chat-pane-message, not a child.
|
||||
// Walk both element types in document order, tracking current sender.
|
||||
// Private meeting chat: message-author-name is a sibling of chat-pane-message
|
||||
const chatEls = Array.from(document.querySelectorAll(
|
||||
"[data-tid='message-author-name'], [data-tid='chat-pane-message']"
|
||||
));
|
||||
@ -70,9 +64,7 @@ _POLL_JS = r"""() => {
|
||||
text = Array.from(pEls).map(p => p.innerText.trim()).filter(t => t).join('\n');
|
||||
} else {
|
||||
let raw = el.innerText.trim();
|
||||
if (currentSender && raw.startsWith(currentSender)) {
|
||||
raw = raw.slice(currentSender.length);
|
||||
}
|
||||
if (currentSender && raw.startsWith(currentSender)) raw = raw.slice(currentSender.length);
|
||||
raw = raw.replace(/^\s*\d{1,2}:\d{2}(?:\s*(?:AM|PM))?\s*/i, '').trim();
|
||||
text = raw;
|
||||
}
|
||||
@ -83,22 +75,29 @@ _POLL_JS = r"""() => {
|
||||
return msgs;
|
||||
}"""
|
||||
|
||||
_CHAT_TITLE_JS = """() => {
|
||||
const t = document.querySelector(
|
||||
"[data-tid='chat-title'], [data-tid='channelTitle-text'], [data-tid='entity-header'] h1"
|
||||
);
|
||||
return t ? t.innerText.trim() : '';
|
||||
}"""
|
||||
|
||||
|
||||
def _clear_line() -> None:
|
||||
sys.stdout.write('\r\033[K')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def parse_trigger(message: ChatMessage, current_user: str) -> tuple[str, str] | None:
|
||||
if message.sender != current_user:
|
||||
return None
|
||||
|
||||
text = message.text.strip()
|
||||
|
||||
m = _START_RE.match(text)
|
||||
if m:
|
||||
raw = (m.group(1) or "").strip(_QUOTE_CHARS).strip()
|
||||
name = raw or "Unbekannter Vortragender"
|
||||
return ("start", name)
|
||||
|
||||
return ("start", raw or "Unbekannter Vortragender")
|
||||
if _STOP_RE.match(text):
|
||||
return ("stop", "")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@ -109,7 +108,7 @@ class Monitor:
|
||||
self._seen_message_ids: set[str] = set()
|
||||
|
||||
def wait_for_chat(self) -> None:
|
||||
print("Warte auf Teams-Chat-Seite... (bitte zur Meeting-Chat-Seite navigieren)")
|
||||
print("Warte auf Teams-Chat-Seite... (bitte zum Meeting-Chat navigieren)")
|
||||
self._page.wait_for_selector(_MSG_SELECTOR, timeout=300_000)
|
||||
print("Chat erkannt.")
|
||||
|
||||
@ -132,36 +131,13 @@ class Monitor:
|
||||
except Exception:
|
||||
pass
|
||||
self._page.wait_for_timeout(1000)
|
||||
|
||||
name = input("Benutzername nicht erkannt. Bitte deinen Teams-Anzeigenamen eingeben: ").strip()
|
||||
name = input("Benutzername nicht erkannt. Bitte Teams-Anzeigenamen eingeben: ").strip()
|
||||
if not name:
|
||||
raise RuntimeError("Kein Benutzername angegeben.")
|
||||
return name
|
||||
|
||||
def poll_new_messages(self) -> list[ChatMessage]:
|
||||
result = self._page.evaluate(r"""() => {
|
||||
const isVisible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
|
||||
const chatMsgs = Array.from(document.querySelectorAll("[data-tid='chat-pane-message']")).filter(isVisible);
|
||||
const authorNames = Array.from(document.querySelectorAll("[data-tid='message-author-name']"));
|
||||
const sample = chatMsgs.slice(0, 3).map(msg => ({
|
||||
hasAuthorInside: !!msg.querySelector("[data-tid='message-author-name']"),
|
||||
text: msg.innerText.trim().substring(0, 40),
|
||||
}));
|
||||
return {
|
||||
channelAll: document.querySelectorAll("[data-tid='channel-pane-message']").length,
|
||||
chatAll: document.querySelectorAll("[data-tid='chat-pane-message']").length,
|
||||
channelVisible: Array.from(document.querySelectorAll("[data-tid='channel-pane-message']")).filter(isVisible).length,
|
||||
chatVisible: chatMsgs.length,
|
||||
authorNamesTotal: authorNames.length,
|
||||
sample,
|
||||
};
|
||||
}""")
|
||||
print(f" [DOM] channel={result['channelAll']}(vis:{result['channelVisible']}) chat={result['chatAll']}(vis:{result['chatVisible']}) authors={result['authorNamesTotal']}")
|
||||
for s in result['sample']:
|
||||
print(f" msg hasAuthor={s['hasAuthorInside']} text={s['text']!r}")
|
||||
raw = self._page.evaluate(_POLL_JS)
|
||||
if raw:
|
||||
print(f" [POLL] {len(raw)} Elemente, davon {sum(1 for r in raw if r['id'] not in self._seen_message_ids)} neu")
|
||||
new_messages = []
|
||||
for item in raw:
|
||||
if item["id"] in self._seen_message_ids:
|
||||
@ -175,7 +151,7 @@ class Monitor:
|
||||
))
|
||||
return new_messages
|
||||
|
||||
def run(self) -> AuditWindow:
|
||||
def run_once(self) -> AuditWindow:
|
||||
import time
|
||||
|
||||
self.wait_for_chat()
|
||||
@ -183,27 +159,33 @@ class Monitor:
|
||||
presenter: str | None = None
|
||||
start_time: datetime | None = None
|
||||
collected: list[ChatMessage] = []
|
||||
last_url: str = ""
|
||||
last_chat: str = ""
|
||||
spin_idx: int = 0
|
||||
|
||||
print("Monitoring aktiv. Poste '!start Name' im Chat um ein Zeitfenster zu starten.")
|
||||
print("Bereit — poste '!start Name' im Chat um zu beginnen.")
|
||||
|
||||
while True:
|
||||
current_chat = self._page.evaluate("""() => {
|
||||
const t = document.querySelector(
|
||||
"[data-tid='chat-title'], [data-tid='channelTitle-text'], [data-tid='entity-header'] h1"
|
||||
);
|
||||
return t ? t.innerText.trim() : document.title;
|
||||
}""")
|
||||
if current_chat != last_url:
|
||||
print(f" [CHAT] {current_chat}")
|
||||
last_url = current_chat
|
||||
# Chat-Titel prüfen
|
||||
try:
|
||||
current_chat = self._page.evaluate(_CHAT_TITLE_JS)
|
||||
if current_chat and current_chat != last_chat:
|
||||
_clear_line()
|
||||
print(f"Chat: {current_chat}")
|
||||
last_chat = current_chat
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Spinner anzeigen
|
||||
spin = _SPINNER[spin_idx % len(_SPINNER)]
|
||||
spin_idx += 1
|
||||
sys.stdout.write(f'\r {spin}')
|
||||
sys.stdout.flush()
|
||||
|
||||
try:
|
||||
new_msgs = self.poll_new_messages()
|
||||
for m in new_msgs:
|
||||
print(f" [MSG] von='{m.sender}' text={m.text[:60]!r}")
|
||||
except Exception as e:
|
||||
print(f"Verbindung verloren ({type(e).__name__}: {e!s:.120}), warte auf Chat...")
|
||||
_clear_line()
|
||||
print(f"Verbindung unterbrochen, reconnecting...")
|
||||
time.sleep(2)
|
||||
try:
|
||||
self.wait_for_chat()
|
||||
@ -216,23 +198,26 @@ class Monitor:
|
||||
|
||||
if trigger is None:
|
||||
if start_time is not None:
|
||||
_clear_line()
|
||||
print(f" {msg.sender}: {msg.text[:80]}")
|
||||
collected.append(msg)
|
||||
continue
|
||||
|
||||
action, value = trigger
|
||||
_clear_line()
|
||||
|
||||
if action == "start":
|
||||
presenter = value
|
||||
start_time = datetime.now()
|
||||
collected = []
|
||||
print(f"Zeitfenster gestartet — Vortragender: {presenter}")
|
||||
print(f"▶ Zeitfenster gestartet — Vortragender: {presenter}")
|
||||
|
||||
elif action == "stop":
|
||||
if start_time is None:
|
||||
print("!stop ohne vorheriges !start — ignoriert.")
|
||||
continue
|
||||
end_time = datetime.now()
|
||||
print("Zeitfenster beendet.")
|
||||
print("■ Zeitfenster beendet.")
|
||||
seen_senders = list(dict.fromkeys(m.sender for m in collected))
|
||||
entries = [AuditEntry(display_name=s, email="") for s in seen_senders]
|
||||
return AuditWindow(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user