/
telthy
/
SOVA_AI
Обзор
Документация
Войти
/
telthy
/
SOVA_AI
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
server/server.py
595 строк
25 KB
telthy
update server/server.py
18 окт 2025, 16:55
18 окт 2025, 16:55
439db19
Код
Авторство
О чём код?
from flask import Flask, request, jsonify from flask_cors import CORS from caldav import DAVClient from datetime import datetime, timedelta import pytz import re import uuid app = Flask(__name__) CORS(app) # === НАСТРОЙКИ === USERNAME = "caldav_username" APP_PASSWORD = "caldav_password" CALDAV_URL = "caldav_url" MOSCOW = pytz.timezone("Europe/Moscow") WORK_START = 9 WORK_END = 18 DAYS_AHEAD = 7 user_state = {} # Для чата all_slots_cache = {} # Для слотов requests_to_child = [] # ✅ Запросы от родителей # Глобальные переменные для состояния # === Интервалы === INTERVALS = { "на сегодня": 0, "на завтра": 1, "на 3 дня": 3, "на неделю": 7 } def extract_all_vevents(): events_list = [] try: client = DAVClient(url=CALDAV_URL, username=USERNAME, password=APP_PASSWORD) calendar = client.principal().calendars()[0] for obj in calendar.events(): raw_data = obj.data.strip() vevent_blocks = re.findall(r"BEGIN:VEVENT(.*?)END:VEVENT", raw_data, re.DOTALL) for block in vevent_blocks: summary_match = re.search(r"SUMMARY:(.+)", block) location_match = re.search(r"LOCATION:(.+)", block) dtstart_match = re.search(r"DTSTART(?:;[^:]*)?:(\d{8}T\d{6})", block) dtend_match = re.search(r"DTEND(?:;[^:]*)?:(\d{8}T\d{6})", block) if not all([dtstart_match, dtend_match]): continue summary = (summary_match.group(1).strip() if summary_match else "Без названия") location = (location_match.group(1).strip() if location_match else "") dtstart_str = dtstart_match.group(1) dtend_str = dtend_match.group(1) try: start_dt = datetime.strptime(dtstart_str, "%Y%m%dT%H%M%S") end_dt = datetime.strptime(dtend_str, "%Y%m%dT%H%M%S") start_dt = MOSCOW.localize(start_dt) end_dt = MOSCOW.localize(end_dt) interval_days = user_state.get("default", {}).get("interval", 7) now = datetime.now(MOSCOW) if start_dt < now - timedelta(days=1) or start_dt > now + timedelta(days=interval_days): continue events_list.append({ "summary": summary, "location": location, "start": start_dt, "end": end_dt, }) except: continue except Exception as e: print("Ошибка:", e) return sorted(events_list, key=lambda x: x["start"]) def find_free_slots_by_day(interval_days=7, duration_minutes=60): """Возвращает свободные слоты, сгруппированные по дням, разбитые на интервалы""" events = extract_all_vevents() busy = [] for ev in events: start = ev["start"] end = ev["end"] text = (ev["summary"] + " " + ev["location"]).lower() if any(keyword in text for keyword in ["город", "офис", "встреча", "выезд", "поездка", "вых"]): start = start - timedelta(hours=1) end = end + timedelta(hours=1) busy.append({"start": start, "end": end}) busy.sort(key=lambda x: x["start"]) days_with_slots = {} now = datetime.now(MOSCOW) current = now.replace(hour=WORK_START, minute=0, second=0, microsecond=0) duration_td = timedelta(minutes=duration_minutes) for _ in range(interval_days + 1): day_end = current.replace(hour=WORK_END) day_key = current.date() if current.date() == now.date(): current = max(current, now) if current >= day_end: current += timedelta(days=1) current = current.replace(hour=WORK_START, minute=0) continue # Находим свободные интервалы в этот день day_busy_intervals = [item for item in busy if item["start"].date() == day_key] free_intervals = [] temp_current = current if not day_busy_intervals: # Если нет занятых — весь день свободен while temp_current + duration_td <= day_end: free_intervals.append({ "start": temp_current.strftime("%H:%M"), "end": (temp_current + duration_td).strftime("%H:%M"), }) temp_current += duration_td else: for item in day_busy_intervals: # Свободный интервал до занятого while temp_current + duration_td <= item["start"] and temp_current + duration_td <= day_end: free_intervals.append({ "start": temp_current.strftime("%H:%M"), "end": (temp_current + duration_td).strftime("%H:%M"), }) temp_current += duration_td # Перемещаем указатель за занятый интервал temp_current = max(temp_current, item["end"]) # Свободный интервал после последнего занятого while temp_current + duration_td <= day_end: free_intervals.append({ "start": temp_current.strftime("%H:%M"), "end": (temp_current + duration_td).strftime("%H:%M"), }) temp_current += duration_td if free_intervals: day_info = { "date": current.strftime("%d.%m.%Y"), "day": ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"][current.weekday()], "slots": free_intervals } days_with_slots[day_key.strftime("%Y-%m-%d")] = day_info current += timedelta(days=1) current = current.replace(hour=WORK_START, minute=0) return days_with_slots def create_event_in_calendar(summary, start_time, end_time, location="", real_start=None, real_end=None): try: client = DAVClient(url=CALDAV_URL, username=USERNAME, password=APP_PASSWORD) calendar = client.principal().calendars()[0] start_iso = start_time.strftime("%Y%m%dT%H%M%S") end_iso = end_time.strftime("%Y%m%dT%H%M%S") description = "" if real_start and real_end: description = f"Реальное время: {real_start.strftime('%H:%M')} - {real_end.strftime('%H:%M')}" # === ВАЖНО: Кодируем в UTF-8 === ics_data = f"""BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Yandex LLC//Yandex Calendar//EN CALSCALE:GREGORIAN METHOD:PUBLISH BEGIN:VEVENT UID:{str(uuid.uuid4())}@yandex.ru DTSTART;TZID=Europe/Moscow:{start_iso} DTEND;TZID=Europe/Moscow:{end_iso} SUMMARY:{summary} LOCATION:{location} DESCRIPTION:{description} END:VEVENT END:VCALENDAR""".encode('utf-8') # ✅ Кодируем в UTF-8 # === Отправляем как байты === event = calendar.save_event(ics_data) return True except Exception as e: print("Ошибка создания события:", e) return False def bot_response(text, user_id="default"): global user_state, all_slots_cache text = text.lower() events = extract_all_vevents() # === Если пользователь в состоянии выбора типа встречи === if user_id in user_state and user_state[user_id].get("step") == "ask_type": if "играть" in text: user_state[user_id]["meeting_type"] = "играть" elif "учиться" in text: user_state[user_id]["meeting_type"] = "учиться" elif "рисовать" in text: user_state[user_id]["meeting_type"] = "рисовать" else: user_state[user_id]["meeting_type"] = "другое" user_state[user_id]["step"] = "ask_duration" return { "reply": "⏱ Сколько времени ты хочешь на это?", "show_buttons": ["30 минут", "1 час", "2 часа"] } # === Если пользователь в состоянии выбора длительности === elif user_id in user_state and user_state[user_id].get("step") == "ask_duration": if "30" in text: duration = 30 elif "1 час" in text or "1час" in text: duration = 60 elif "2 часа" in text or "2часа" in text: duration = 120 else: return { "reply": "⏱ Сколько времени ты хочешь на это?", "show_buttons": ["30 минут", "1 час", "2 часа"] } user_state[user_id]["duration"] = duration interval_days = user_state[user_id].get("interval", 7) days_data = find_free_slots_by_day(interval_days) if not days_data: del user_state[user_id] return "🥺 К сожалению, нет свободных дней." user_state[user_id]["step"] = "choose_day" user_state[user_id]["days_data"] = days_data return show_days_for_choice(days_data) # === Если пользователь в состоянии выбора дня === elif user_id in user_state and user_state[user_id].get("step") == "choose_day": # Проверяем, выбрал ли пользователь день days_data = user_state[user_id].get("days_data", {}) selected_day = None for day_key, day_info in days_data.items(): if day_info["day"] in text or day_info["date"] in text: selected_day = day_info break if selected_day: user_state[user_id]["selected_day"] = selected_day user_state[user_id]["step"] = "choose_time" # Фильтруем слоты по длительности duration = user_state[user_id].get("duration", 30) duration_td = timedelta(minutes=duration) filtered_slots = [] for slot in selected_day["slots"]: start_time = datetime.strptime(slot['start'], "%H:%M") end_time = datetime.strptime(slot['end'], "%H:%M") slot_duration = end_time - start_time if slot_duration >= duration_td: filtered_slots.append(slot) if not filtered_slots: return "🥺 В этот день нет подходящего времени. Выбери другой день." all_slots_cache[user_id] = filtered_slots reply = f"📅 Вот свободное время в {selected_day['day']}, {selected_day['date']} (минимум {duration} мин):\n\n" for i, slot in enumerate(filtered_slots): reply += f"{i+1}. {slot['start']} — {slot['end']}\n" return { "reply": reply, "show_slot_buttons": True } else: return "🥺 Не понял, какой день ты выбрал. Напиши день недели или дату." # === Если пользователь в состоянии выбора времени === elif user_id in user_state and user_state[user_id].get("step") == "choose_time": if text.isdigit(): index = int(text) - 1 if user_id in all_slots_cache and 0 <= index < len(all_slots_cache[user_id]): selected_slot = all_slots_cache[user_id][index] selected_day = user_state[user_id].get("selected_day", {}) # Сохраняем выбранный слот full_date_str = selected_day['date'] + " " + selected_slot['start'] start_time = datetime.strptime(full_date_str, "%d.%m.%Y %H:%M") duration = (datetime.strptime(selected_slot['end'], "%H:%M") - datetime.strptime(selected_slot['start'], "%H:%M")).seconds // 60 end_time = start_time + timedelta(minutes=duration) start_time = MOSCOW.localize(start_time) end_time = MOSCOW.localize(end_time) # Меняем шаг user_state[user_id]["final_slot"] = { "start_time": start_time, "end_time": end_time, "day_info": selected_day, "slot_info": selected_slot } user_state[user_id]["step"] = "confirm" return { "reply": f"📅 Отлично! Ты хочешь {user_state[user_id].get('meeting_type', 'занятие')}:\n{selected_day['day']}, {selected_day['date']} с {selected_slot['start']} до {selected_slot['end']}\n\nХочешь сохранить это?", "show_buttons": ["✅ Сохранить", "❌ Другое время"] } else: return "🥺 Неверный номер. Попробуй снова." else: return "🥺 Выбери время по номеру." # === Подтверждение === elif user_id in user_state and user_state[user_id].get("step") == "confirm": if "сохранить" in text or "да" in text: final_data = user_state[user_id].get("final_slot", {}) start_time = final_data.get("start_time") end_time = final_data.get("end_time") success = create_event_in_calendar( summary=f"⏰ {user_state[user_id].get('meeting_type', 'Занятие').title()}", start_time=start_time, end_time=end_time, location=user_state[user_id].get('meeting_type', 'Занятие') ) if success: del user_state[user_id] del all_slots_cache[user_id] return "🎉 Ура! Сохранено в твой календарь!" else: return "🥺 Ой! Что-то сломалось. Попробуй снова." elif "другое" in text or "нет" in text: # Возвращаемся к выбору дня user_state[user_id]["step"] = "choose_day" days_data = user_state[user_id].get("days_data", {}) return show_days_for_choice(days_data) else: return { "reply": "Хочешь сохранить это?", "show_buttons": ["✅ Сохранить", "❌ Другое время"] } # === Команда: Обычные свободные слоты === # === Команда: Покажи мой день === if "мой день" in text or "расписание" in text: today = datetime.now(MOSCOW).date() todays = [ev for ev in events if ev["start"].date() == today] if not todays: return "😊 Сегодня твой день свободен! Что хочешь запланировать?" reply = "📋 Вот твой день сегодня:\n\n" for ev in todays: time_str = f"{ev['start'].strftime('%H:%M')} — {ev['end'].strftime('%H:%M')}" reply += f"• {ev['summary']} | {time_str}\n" if ev["location"]: reply += f" 📍 {ev['location']}\n" return{ "reply": reply, "show_buttons": ["Запланировать"] } # === Команда: Запланировать встречу === elif "запланировать" in text or "встреча" in text: user_state[user_id] = {"step": "ask_type"} return { "reply": "📍 Что ты хочешь запланировать?", "show_buttons": ["Играть", "Учиться", "Рисовать", "Другое"] } # === Команда: Сегодняшние встречи === elif "сегодня" in text and ("встреч" in text or "митинг" in text): today = datetime.now(MOSCOW).date() todays = [ev for ev in events if ev["start"].date() == today] if not todays: return "😊 Сегодня твой день свободен! Что хочешь запланировать?" reply = "📋 Вот твой день сегодня:\n\n" for ev in todays: time_str = f"{ev['start'].strftime('%H:%M')} — {ev['end'].strftime('%H:%M')}" reply += f"• {ev['summary']} | {time_str}\n" if ev["location"]: reply += f" 📍 {ev['location']}\n" return{ "reply": reply, "show_buttons": ["Запланировать"] } elif "привет" in text or "прив" in text: return { "reply": "👋 Привет! Я твой друг-помощник! Хочешь посмотреть, когда у тебя свободно?", "show_buttons": ["Покажи мой день", "Запланировать"] } else: return { "reply": "🤔 Не понял. Скажи: 'Привет', 'Когда свободно?' или 'Покажи мой день'.", "show_buttons": ["Покажи мой день", "Запланировать"] } def show_days_for_choice(days_data): """Формирует сообщение с днями для выбора""" reply = "📅 В какие дни у тебя свободно?\n\n" for i, (day_key, day_info) in enumerate(days_data.items()): slot_count = len(day_info["slots"]) reply += f"{i+1}. {day_info['day']}, {day_info['date']} — {slot_count} свободных{'/' if slot_count == 1 else 'ых'} слот{'а' if slot_count == 1 else 'ов'}\n" return { "reply": reply, "show_buttons": [f"{day_info['day']}, {day_info['date']}" for day_info in days_data.values()] } @app.route('/chat', methods=['POST']) def chat(): try: data = request.get_json() user_message = data.get('message', '') user_id = data.get('user_id', 'default') bot_reply = bot_response(user_message, user_id) if isinstance(bot_reply, dict): return jsonify(bot_reply) else: return jsonify({"reply": bot_reply}) except Exception as e: return jsonify({"reply": "Ошибка: " + str(e)}) # Добавь в конец файла @app.route('/clear_chat', methods=['POST']) def clear_chat(): """Очищает состояние пользователя на сервере""" print('clearing') try: data = request.get_json() user_id = data.get('user_id', 'default') # Очищаем состояние пользователя if user_id in user_state: del user_state[user_id] if user_id in all_slots_cache: del all_slots_cache[user_id] return jsonify({"success": True, "message": "Чат очищен"}) except Exception as e: return jsonify({"success": False, "error": str(e)}) @app.route('/stats', methods=['POST']) def get_stats(): """Возвращает статистику по встречам""" try: data = request.get_json() user_id = data.get('user_id', 'default') # Получаем события за последние 7 дней events = extract_all_vevents() now = datetime.now(MOSCOW) week_ago = now - timedelta(days=7) recent_events = [ev for ev in events if ev["start"] >= week_ago] # Считаем статистику total_events = len(recent_events) total_duration = 0 game_time = 0 study_time = 0 rest_time = 0 for ev in recent_events: duration = (ev["end"] - ev["start"]).total_seconds() / 3600 # в часах total_duration += duration summary = ev["summary"].lower() location = ev["location"].lower() if any(word in summary or word in location for word in ["игра", "видео", "ютуб", "развлеч"]): game_time += duration elif any(word in summary or word in location for word in ["учёба", "урок", "дз", "задание"]): study_time += duration else: rest_time += duration # Статистика по дням недели weekdays = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"] daily_stats = {day: 0 for day in weekdays} for ev in recent_events: day_name = weekdays[ev["start"].weekday()] duration = (ev["end"] - ev["start"]).total_seconds() / 3600 daily_stats[day_name] += duration return jsonify({ "success": True, "stats": { "total_events": total_events, "total_duration": round(total_duration, 1), "game_time": round(game_time, 1), "study_time": round(study_time, 1), "rest_time": round(rest_time, 1), "daily_stats": daily_stats } }) except Exception as e: return jsonify({"success": False, "error": str(e)}) @app.route('/send_parent_request', methods=['POST']) def send_parent_request(): """Родитель отправляет запрос ребёнку""" try: data = request.get_json() child_id = data.get('child_id', 'default') event = data.get('event', {}) request_item = { "id": len(requests_to_child) + 1, "title": event.get('title'), "date": event.get('date'), "time": event.get('time'), "type": event.get('type'), "status": "ожидание", # ожидание, принято, отклонено "timestamp": datetime.now(MOSCOW).strftime("%d.%m.%Y %H:%M") } requests_to_child.append(request_item) return jsonify({"success": True, "message": "Запрос отправлен ребёнку"}) except Exception as e: return jsonify({"success": False, "error": str(e)}), 500 @app.route('/get_child_requests', methods=['POST']) def get_child_requests(): """Ребёнок получает свои запросы""" try: # Не нужно data = request.get_json() — можно убрать return jsonify({"success": True, "requests": requests_to_child}) except Exception as e: return jsonify({"success": False, "error": str(e)}), 500 @app.route('/respond_to_request', methods=['POST']) def respond_to_request(): """Ребёнок отвечает на запрос""" try: data = request.get_json() child_id = data.get('child_id', 'default') request_id = data.get('request_id') response = data.get('response') # accepted / rejected for req in requests_to_child: if req["id"] == request_id: if response == "accepted": req["status"] = "принято" # Добавляем в календарь start_str = f"{req['date']} {req['time'].split('-')[0]}" end_str = f"{req['date']} {req['time'].split('-')[1]}" start_time = datetime.strptime(start_str, "%d.%m.%Y %H:%M") end_time = datetime.strptime(end_str, "%d.%m.%Y %H:%M") start_time = MOSCOW.localize(start_time) end_time = MOSCOW.localize(end_time) create_event_in_calendar( summary=f"[Родитель] {req['title']}", start_time=start_time, end_time=end_time, location=req['type'] ) elif response == "rejected": req["status"] = "отклонено" break return jsonify({"success": True, "message": "Ответ отправлен"}) except Exception as e: return jsonify({"success": False, "error": str(e)}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)