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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
|
# This file is part of Senator Sperma
# Licensed under the GPL-3.0 License
# Copyright (C) 2026 Salmonik @ salmonik@tuta.io
# https://codeberg.org/salmonik/senator-sperma
import datetime
import logging
import os
import asyncio
import discord
from discord.ext import commands
from gtts import gTTS
from cogs.voice import get_voice_manager
from cogs.utils.settings import get_volume, save_settings
from cogs.utils.database import get_connection, init_database
import tempfile
logger = logging.getLogger(__name__)
init_database()
def load_activity_data():
try:
with get_connection() as conn:
c = conn.cursor()
c.execute("SELECT user_id, join_time, total_active, total_afk, is_afk, afk_start, active_start, channel_name FROM voice_activity")
rows = c.fetchall()
return {
row[0]: {
"join_time": row[1],
"total_active": row[2],
"total_afk": row[3],
"is_afk": bool(row[4]),
"afk_start": row[5],
"active_start": row[6],
"channel_name": row[7],
}
for row in rows
}
except Exception as e:
logger.error(f"Error loading voice activity: {e}")
return {}
def save_activity_data(data):
try:
with get_connection() as conn:
c = conn.cursor()
c.execute("PRAGMA foreign_keys = ON")
c.execute("DELETE FROM voice_activity")
for user_id, user_data in data.items():
c.execute(
"""INSERT INTO voice_activity (user_id, join_time, total_active, total_afk, is_afk, afk_start, active_start, channel_name)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(
user_id,
user_data.get("join_time"),
user_data.get("total_active", 0),
user_data.get("total_afk", 0),
int(user_data.get("is_afk", False)),
user_data.get("afk_start"),
user_data.get("active_start"),
user_data.get("channel_name"),
),
)
conn.commit()
logger.info(f"Saved voice activity for {len(data)} users")
except Exception as e:
logger.error(f"Error saving voice activity: {e}")
def format_duration(seconds):
hours, remainder = divmod(int(seconds), 3600)
minutes, seconds = divmod(remainder, 60)
if hours > 0:
return f"{hours}h {minutes}m {seconds}s"
elif minutes > 0:
return f"{minutes}m {seconds}s"
return f"{seconds}s"
class VcLogs(commands.Cog):
def __init__(self, bot):
self.bot = bot
self._voice_manager = None
def get_voice_manager(self):
if self._voice_manager is None:
self._voice_manager = get_voice_manager(self.bot)
return self._voice_manager
@staticmethod
async def handle_state_change(member, channel, state_type, is_enabled, log_channel):
"""Helper method to handle voice state changes"""
state_messages = {
'deaf': {
True: f"{member.display_name} jest bity przez ojca",
False: f"{member.display_name} uciek od ojca"
},
'mute': {
True: f"{member.display_name} idzie robić jajecznice",
False: f"{member.display_name} zrobił jajecznice"
}
}
tts_text = state_messages[state_type][is_enabled]
return tts_text, channel
async def play_tts(self, channel, text):
from cogs.utils.temp import get_temp_dir
voice_mgr = self.get_voice_manager()
temp_dir = get_temp_dir()
tts_file = os.path.join(temp_dir, f"temp_tts_{os.getpid()}.mp3")
tts = gTTS(text=text, lang='pl')
logger.info("saving file")
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, tts.save, tts_file)
try:
voice_status = await voice_mgr.get_voice_status(channel.guild)
voice_client = voice_status[0] if voice_status else None
is_connected = voice_status[1] if voice_status else False
is_playing = voice_status[2] if voice_status else False
if is_connected and not is_playing:
from cogs.voice import is_voice_client_alive
if not is_voice_client_alive(voice_client, channel.guild):
logger.warning("Stale voice client in play_tts, reconnecting")
try:
await voice_client.disconnect()
except Exception:
pass
is_connected = False
if not is_connected:
voice_client = await voice_mgr.join_channel(channel)
voice_status = await voice_mgr.get_voice_status(channel.guild)
voice_client = voice_status[0] if voice_status else None
is_connected = voice_status[1] if voice_status else False
is_playing = voice_status[2] if voice_status else False
if not is_connected or voice_client is None:
logger.error("Failed to join voice channel")
return
volume = get_volume()
logger.info(f"connected: {voice_client}")
if voice_client:
if is_playing:
logger.info("voice already playing, skipping TTS")
return
logger.info("preparing to play audio")
audio_source = discord.FFmpegPCMAudio(tts_file)
await voice_mgr.play_source(channel, audio_source, volume=volume)
except Exception as e:
logger.error(f"TTS playback error: {e}")
finally:
if os.path.exists(tts_file):
os.remove(tts_file)
@commands.slash_command(
name="log_volume"
)
async def volume(self, ctx: discord.ApplicationContext, volume: int):
from cogs.utils.settings import get_settings
settings = get_settings()
settings.volume = volume
await save_settings()
await ctx.followup.send(f"Volume has been set to: {volume}")
logger.info(f"Volume changed to: {volume}")
@commands.Cog.listener()
async def on_voice_state_update(self, member, before, after):
log_channel_id = os.getenv("STATUS_CHANNEL_ID")
if not log_channel_id:
logger.warning("STATUS_CHANNEL_ID not set in environment")
return
log_channel = self.bot.get_channel(int(log_channel_id))
if log_channel is None:
logger.warning("Log channel not found or not accessible")
return
current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
current_ts = datetime.datetime.now().timestamp()
tts_text = None
tts_channel = None
if member.bot:
return
user_id = str(member.id)
activity_data = load_activity_data()
if before.channel is None and after.channel is not None:
is_afk = after.self_mute or after.self_deaf
activity_data[user_id] = {
'join_time': current_ts,
'total_active': 0,
'total_afk': 0,
'is_afk': is_afk,
'afk_start': current_ts if is_afk else None,
'active_start': current_ts if not is_afk else None,
'channel_name': after.channel.name
}
save_activity_data(activity_data)
message = f"[{current_time}] {member.name} dołączył na {after.channel.name} <t:{int(current_ts)}:R>"
tts_text = f"{member.display_name} sie zespawnił"
tts_channel = after.channel
await log_channel.send(message)
logger.info(f"User joined voice channel: {message}")
elif before.channel is not None and after.channel is None:
if user_id in activity_data:
data = activity_data[user_id]
if data.get('is_afk') and data.get('afk_start'):
afk_duration = current_ts - data['afk_start']
data['total_afk'] += afk_duration
elif data.get('active_start'):
active_since_start = current_ts - data['active_start']
data['total_active'] += active_since_start
active_str = format_duration(data['total_active'])
afk_str = format_duration(data['total_afk'])
message = (
f"[{current_time}] {member.name} rozłączył się z {before.channel.name} <t:{int(current_ts)}:R>\n"
f"Aktywny: {active_str} | AFK: {afk_str}"
)
tts_text = f"{member.display_name} jest chujem"
tts_channel = before.channel
await log_channel.send(message)
logger.info(f"User left voice channel: {message}")
del activity_data[user_id]
save_activity_data(activity_data)
else:
message = f"[{current_time}] {member.name} rozłączył się z {before.channel.name} <t:{int(current_ts)}:R>"
tts_text = f"{member.display_name} jest chujem"
tts_channel = before.channel
await log_channel.send(message)
logger.info(f"User left voice channel: {message}")
elif user_id in activity_data:
data = activity_data[user_id]
if data.get('is_afk') and not (after.self_mute or after.self_deaf):
afk_duration = current_ts - data['afk_start']
data['total_afk'] += afk_duration
data['afk_start'] = None
data['is_afk'] = False
data['active_start'] = current_ts
save_activity_data(activity_data)
afk_str = format_duration(afk_duration)
if after.self_mute:
activity = "odciszył"
else:
activity = "odgłuchnął"
message = ( f"[{current_time}] {member.name} {activity} się na {after.channel.name} <t:{int(current_ts)}:R>\n"
f"AFK: {afk_str}" )
await log_channel.send(message)
logger.info(f"User returned from AFK: {message}")
elif not data.get('is_afk') and (after.self_mute or after.self_deaf):
active_since_start = 0
if data.get('active_start'):
active_since_start = current_ts - data['active_start']
data['total_active'] += active_since_start
data['afk_start'] = current_ts
data['is_afk'] = True
data['active_start'] = None
save_activity_data(activity_data)
if after.self_mute:
activity = "wyciszył"
else:
activity = "ogłuchnął"
active_str = format_duration(active_since_start)
message = ( f"[{current_time}] {member.name} {activity} się na {after.channel.name} <t:{int(current_ts)}:R>\n"
f"Aktywny: {active_str}" )
await log_channel.send(message)
logger.info(f"User went AFK: {message}")
if before.self_deaf != after.self_deaf:
tts_text, tts_channel = await self.handle_state_change(
member, after.channel, 'deaf', after.self_deaf, log_channel
)
if tts_text and tts_channel:
await self.play_tts(tts_channel, tts_text)
return
if before.self_mute != after.self_mute:
tts_text, tts_channel = await self.handle_state_change(
member, after.channel, 'mute', after.self_mute, log_channel
)
if tts_text and tts_channel:
await self.play_tts(tts_channel, tts_text)
def setup(bot):
bot.add_cog(VcLogs(bot))
|