-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
268 lines (247 loc) · 12.1 KB
/
main.py
File metadata and controls
268 lines (247 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
import logging
import json
import pytz
import os
from datetime import time, datetime
from telegram import Update
from telegram.constants import ParseMode
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes, MessageHandler, filters, Application
from utils import now_arg, today_str, is_weekday, send_mail
with open('config.json') as f:
config = json.load(f)
logging.basicConfig(level=logging.INFO)
# Estado en memoria
user_decisions = {}
users_today = set()
reminder_sent = False
vacation_until = {}
USER_DECISIONS_FILE = 'user_decisions.json'
VACATION_UNTIL_FILE = 'vacation_until.json'
def save_state():
data = {
'date': now_arg().strftime('%Y-%m-%d'),
'decisions': user_decisions
}
with open(USER_DECISIONS_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False)
with open(VACATION_UNTIL_FILE, 'w', encoding='utf-8') as f:
# Convertir las fechas a string para serializar
json.dump({str(uid): d.strftime('%Y-%m-%d') for uid, d in vacation_until.items()}, f, ensure_ascii=False)
def load_state():
global user_decisions, vacation_until
today_str = now_arg().strftime('%Y-%m-%d')
if os.path.exists(USER_DECISIONS_FILE):
with open(USER_DECISIONS_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
if isinstance(data, dict) and 'date' in data and 'decisions' in data:
if data['date'] == today_str:
user_decisions.clear()
user_decisions.update(data['decisions'])
else:
user_decisions.clear()
else:
# Compatibilidad con formato anterior
user_decisions.clear()
user_decisions.update(data)
if os.path.exists(VACATION_UNTIL_FILE):
with open(VACATION_UNTIL_FILE, 'r', encoding='utf-8') as f:
vacation_until.clear()
for uid, d in json.load(f).items():
vacation_until[int(uid)] = datetime.strptime(d, '%Y-%m-%d').date()
# Cargar estado al iniciar
load_state()
# Cargar correspondencia de nombres formales
USER_NAMES_FILE = 'user_names.json'
if os.path.exists(USER_NAMES_FILE):
with open(USER_NAMES_FILE, 'r', encoding='utf-8') as f:
user_names = json.load(f)
else:
user_names = {}
def reset_day():
global user_decisions, users_today, reminder_sent, vacation_until
user_decisions = {}
users_today = set()
reminder_sent = False
# Elimina vacaciones pasadas
today = now_arg().date()
vacation_until = {uid: until for uid, until in vacation_until.items() if until >= today}
save_state()
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
if update.effective_chat.type == 'group' or update.effective_chat.type == 'supergroup':
members = await context.bot.get_chat_administrators(config['GROUP_CHAT_ID'])
users_today.clear()
for m in members:
if not m.user.is_bot:
users_today.add(m.user.id)
reset_day()
await update.message.reply_text('Bot de asistencia iniciado. Esperando respuestas de todos los miembros humanos.')
async def home(update: Update, context: ContextTypes.DEFAULT_TYPE):
await handle_decision(update, context, 'home')
async def office(update: Update, context: ContextTypes.DEFAULT_TYPE):
await handle_decision(update, context, 'office')
async def vac(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
if user.is_bot:
return
until_date = None
if context.args:
try:
until_date = datetime.strptime(context.args[0], "%d-%m-%Y").date()
except Exception:
await update.message.reply_text("Formato de fecha inválido. Usa /vac dd-mm-yyyy (ejemplo: /vac 27-05-2025)")
return
if until_date:
vacation_until[user.id] = until_date
user_decisions[user.id] = {'name': user.full_name, 'decision': 'vac'}
save_state()
await update.message.reply_text(f"{user.full_name} está de vacaciones hasta el {until_date.strftime('%d-%m-%Y')}")
else:
user_decisions[user.id] = {'name': user.full_name, 'decision': 'vac'}
save_state()
await update.message.reply_text(f"{user.full_name} está de vacaciones hoy.")
if len([u for u in users_today if not (u in vacation_until and vacation_until[u] >= now_arg().date()) and u in user_decisions]) == len([u for u in users_today if not (u in vacation_until and vacation_until[u] >= now_arg().date())]):
await context.bot.send_message(chat_id=config['GROUP_CHAT_ID'], text="Todos respondieron. Escriba /enviar para autorizar el envío del correo.")
async def lic(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
if user.is_bot:
return
until_date = None
if context.args:
try:
until_date = datetime.strptime(context.args[0], "%d-%m-%Y").date()
except Exception:
await update.message.reply_text("Formato de fecha inválido. Usa /lic dd-mm-yyyy (ejemplo: /lic 27-05-2025)")
return
if until_date:
vacation_until[user.id] = until_date # Usamos el mismo dict para vacaciones y licencias
user_decisions[user.id] = {'name': user.full_name, 'decision': 'lic'}
save_state()
await update.message.reply_text(f"{user.full_name} está de licencia hasta el {until_date.strftime('%d-%m-%Y')}")
else:
user_decisions[user.id] = {'name': user.full_name, 'decision': 'lic'}
save_state()
await update.message.reply_text(f"{user.full_name} está de licencia hoy.")
if len([u for u in users_today if not (u in vacation_until and vacation_until[u] >= now_arg().date()) and u in user_decisions]) == len([u for u in users_today if not (u in vacation_until and vacation_until[u] >= now_arg().date())]):
await context.bot.send_message(chat_id=config['GROUP_CHAT_ID'], text="Todos respondieron. Escriba /enviar para autorizar el envío del correo.")
async def handle_decision(update, context, decision):
user = update.effective_user
if user.is_bot:
return
if user.id not in users_today:
users_today.add(user.id)
# Si está de vacaciones, ignorar
if user.id in vacation_until and vacation_until[user.id] >= now_arg().date():
await update.message.reply_text("Estás marcado como de vacaciones hasta el {0}".format(vacation_until[user.id].strftime('%d-%m-%Y')))
return
user_decisions[user.id] = {'name': user.full_name, 'decision': decision}
save_state()
await update.message.reply_text(f"{user.full_name} eligió: {decision.upper()}")
if len([u for u in users_today if not (u in vacation_until and vacation_until[u] >= now_arg().date()) and u in user_decisions]) == len([u for u in users_today if not (u in vacation_until and vacation_until[u] >= now_arg().date())]):
await context.bot.send_message(chat_id=config['GROUP_CHAT_ID'], text="Todos respondieron. Escriba /enviar para autorizar el envío del correo.")
async def enviar(update: Update, context: ContextTypes.DEFAULT_TYPE):
activos = [u for u in users_today if not (u in vacation_until and vacation_until[u] >= now_arg().date())]
if len([u for u in activos if u in user_decisions]) < len(activos):
faltan = [str(uid) for uid in activos if uid not in user_decisions]
await update.message.reply_text(f"Aún faltan respuestas de: {', '.join(faltan)}")
return
# Armar correo
fecha = now_arg()
dia = fecha.strftime('%d')
mes = fecha.strftime('%m')
estados = {
'home': 'Home Office',
'office': 'Presencial',
'lic': 'Licencia',
'vac': 'Vacaciones'
}
body = f"{dia}/{mes}:\n"
for uid in users_today:
if uid in vacation_until and vacation_until[uid] >= now_arg().date():
nombre = user_names.get(str(uid))
if not nombre:
nombre = f"{user_decisions.get(uid, {}).get('name', 'Usuario')} ({uid})"
body += f"{nombre}: Vacaciones\n"
elif uid in user_decisions:
v = user_decisions[uid]
nombre = user_names.get(str(uid))
if not nombre:
nombre = f"{v['name']} ({uid})"
estado = estados.get(v['decision'], v['decision'])
body += f"{nombre}: {estado}\n"
subject = f"asistencia del {dia}/{mes}"
send_mail(subject, body)
await update.message.reply_text("Correo enviado.")
reset_day()
save_state()
async def daily_check(context: ContextTypes.DEFAULT_TYPE):
global reminder_sent
now = now_arg()
if not is_weekday():
return
# Reset a las 8:30
if now.time() >= time(8, 30) and now.time() < time(8, 31):
reset_day()
chat = await context.bot.get_chat(config['GROUP_CHAT_ID'])
members = await context.bot.get_chat_administrators(config['GROUP_CHAT_ID'])
for m in members:
if not m.user.is_bot:
users_today.add(m.user.id)
await context.bot.send_message(chat_id=config['GROUP_CHAT_ID'], text="Nuevo día. Por favor, respondan /home, /office, /lic o /vac.")
# Recordatorio a las 10:00
if now.time() >= time(10, 0) and not reminder_sent:
faltan = [uid for uid in users_today if not (uid in vacation_until and vacation_until[uid] >= now.date()) and uid not in user_decisions]
if faltan:
mentions = ' '.join([f"<a href=\"tg://user?id={uid}\">{uid}</a>" for uid in faltan])
await context.bot.send_message(chat_id=config['GROUP_CHAT_ID'], text=f"Faltan respuestas de: {mentions}", parse_mode=ParseMode.HTML)
reminder_sent = True
# --- Control de polling según horario ---
polling_running = False
async def polling_control(context: ContextTypes.DEFAULT_TYPE):
global polling_running
now = now_arg()
weekday = now.weekday() < 5
in_time = time(8, 30) <= now.time() <= time(11, 0)
if weekday and in_time:
if not polling_running:
context.application.create_task(context.application.start())
polling_running = True
else:
if polling_running:
await context.application.stop()
polling_running = False
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
help_text = (
"Comandos disponibles:\n"
"/home - Indica que hoy trabajas desde casa\n"
"/office - Indica que hoy trabajas en la oficina\n"
"/vac [dd-mm-yyyy] - Indica que estás de vacaciones hasta la fecha de regreso (ejemplo: /vac 27-05-2025). Si no se indica fecha, solo hoy.\n"
"/lic [dd-mm-yyyy] - Indica que estás de licencia hasta la fecha de regreso (ejemplo: /lic 27-05-2025). Si no se indica fecha, solo hoy.\n"
"/enviar - Autoriza el envío del correo (cuando todos respondieron)\n"
"/start - Inicializa el bot y la lista de usuarios\n"
"/help - Muestra este mensaje de ayuda\n\n"
"El bot funciona de lunes a viernes de 8:30 a 11:00.\n"
"Si a las 10:00 faltan respuestas, se recordará a los que no respondieron.\n"
"Cada día se reinician las decisiones."
)
await update.message.reply_text(help_text)
if __name__ == '__main__':
app = ApplicationBuilder().token(config['TELEGRAM_TOKEN']).build()
app.add_handler(CommandHandler('start', start))
app.add_handler(CommandHandler('home', home))
app.add_handler(CommandHandler('office', office))
app.add_handler(CommandHandler('vac', vac))
app.add_handler(CommandHandler('lic', lic))
app.add_handler(CommandHandler('enviar', enviar))
app.add_handler(CommandHandler('help', help_command))
# Tarea periódica cada minuto para daily_check
app.job_queue.run_repeating(daily_check, interval=60, first=0)
# Tarea periódica para controlar el polling
app.job_queue.run_repeating(polling_control, interval=60, first=0)
# Iniciar polling solo si corresponde
now = now_arg()
if now.weekday() < 5 and time(8, 30) <= now.time() <= time(11, 0):
polling_running = True
app.run_polling()
else:
print("Bot en pausa fuera de horario laboral. El polling se iniciará automáticamente en el horario permitido.")
app.run_async()