259 lines
7.9 kB
1
defmodule Blog.Chat do
2
@moduledoc """
3
Module for handling chat functionality and message persistence using ETS.
4
"""
5
require Logger
6
7
@table_name :blog_chat_messages
8
@banned_words_table :blog_chat_banned_words
9
@max_messages_per_room 100
10
11
@doc """
12
Ensures the ETS table is started. Should be called during application startup.
13
"""
14
def ensure_started do
15
# Initialize message table
16
case :ets.info(@table_name) do
17
:undefined ->
18
:ets.new(@table_name, [:ordered_set, :public, :named_table])
19
Logger.info("Created new chat message ETS table")
20
initialize_rooms()
21
_ ->
22
Logger.debug("Chat message ETS table already exists")
23
:ok
24
end
25
26
# Initialize banned words table
27
case :ets.info(@banned_words_table) do
28
:undefined ->
29
:ets.new(@banned_words_table, [:set, :protected, :named_table])
30
# Add some initial banned words (these should be severe ones)
31
add_banned_word("somedefaultbannedword")
32
Logger.info("Created new banned words ETS table")
33
_ ->
34
Logger.debug("Banned words ETS table already exists")
35
:ok
36
end
37
end
38
39
@doc """
40
Initialize the default rooms with welcome messages.
41
"""
42
defp initialize_rooms do
43
# Check if rooms are already initialized
44
case get_messages("general") do
45
[] ->
46
# Add welcome messages to each room
47
rooms = ["general", "random", "programming", "music"]
48
49
welcome_messages = %{
50
"general" => "Welcome to the General chat room! This is where everyone hangs out.",
51
"random" => "Welcome to the Random chat room! Random conversations welcome!",
52
"programming" => "Welcome to the Programming chat room! Discuss code, programming languages, and tech.",
53
"music" => "Welcome to the Music chat room! Share your favorite artists, songs, and musical opinions."
54
}
55
56
Enum.each(rooms, fn room ->
57
save_message(%{
58
id: System.os_time(:millisecond),
59
sender_id: "system",
60
sender_name: "ChatBot",
61
sender_color: "hsl(210, 70%, 50%)",
62
content: Map.get(welcome_messages, room),
63
timestamp: DateTime.utc_now(),
64
room: room
65
})
66
Logger.info("Initialized #{room} room with welcome message")
67
end)
68
_ ->
69
Logger.debug("Rooms already initialized with welcome messages")
70
:ok
71
end
72
end
73
74
@doc """
75
Adds a word to the banned list.
76
"""
77
def add_banned_word(word) when is_binary(word) do
78
lowercase_word = String.downcase(String.trim(word))
79
if lowercase_word != "" do
80
:ets.insert(@banned_words_table, {lowercase_word, true})
81
Logger.info("Added new banned word: #{lowercase_word}")
82
{:ok, lowercase_word}
83
else
84
{:error, :empty_word}
85
end
86
end
87
88
@doc """
89
Gets all banned words.
90
"""
91
def get_banned_words do
92
:ets.tab2list(@banned_words_table)
93
|> Enum.map(fn {word, _} -> word end)
94
|> Enum.sort()
95
end
96
97
@doc """
98
Checks if a message contains any banned words.
99
Returns {:ok, message} if no banned words are found.
100
Returns {:error, :contains_banned_words} if banned words are found.
101
"""
102
def check_for_banned_words(message) when is_binary(message) do
103
lowercase_message = String.downcase(message)
104
105
# Get all banned words
106
banned_words = get_banned_words()
107
108
# Check if any banned word is in the message
109
found_banned_word = Enum.find(banned_words, fn word ->
110
String.contains?(lowercase_message, word)
111
end)
112
113
if found_banned_word do
114
Logger.warn("Message contained banned word, rejected")
115
{:error, :contains_banned_words}
116
else
117
{:ok, message}
118
end
119
end
120
121
@doc """
122
Saves a message to ETS storage.
123
"""
124
def save_message(message) do
125
# Use room and timestamp as key for ordering
126
key = {message.room, message.id}
127
128
# Insert into ETS table
129
result = :ets.insert(@table_name, {key, message})
130
131
# Debug the actual key structure to ensure consistency
132
Logger.debug("Saved message to ETS with key structure: #{inspect(key)}, result: #{inspect(result)}")
133
Logger.debug("Message content: #{inspect(message)}")
134
135
# Debug the current state of the table
136
count = :ets.info(@table_name, :size)
137
Logger.debug("ETS table now has #{count} total messages")
138
139
# Trim messages if we have too many
140
trim_messages(message.room)
141
142
# Return the stored message
143
message
144
end
145
146
@doc """
147
Trims messages in a room to keep only the most recent ones.
148
"""
149
defp trim_messages(room) do
150
# Count messages in this room using match_object instead of select_count with fun2ms
151
messages = :ets.match_object(@table_name, {{room, :_}, :_})
152
count = length(messages)
153
154
Logger.debug("Room #{room} has #{count} messages, max is #{@max_messages_per_room}")
155
156
if count > @max_messages_per_room do
157
# Sort messages by ID (timestamp)
158
sorted_messages =
159
messages
160
|> Enum.sort_by(fn {{_, id}, _} -> id end)
161
162
# Delete the oldest messages
163
to_delete = Enum.take(sorted_messages, count - @max_messages_per_room)
164
165
Enum.each(to_delete, fn {{r, id}, _} ->
166
:ets.delete(@table_name, {r, id})
167
Logger.debug("Deleted old message with key {#{r}, #{id}}")
168
end)
169
170
Logger.info("Trimmed #{length(to_delete)} old messages from room #{room}")
171
end
172
end
173
174
@doc """
175
Gets messages for a specific room.
176
"""
177
def get_messages(room) do
178
# Debug the query we're about to run
179
Logger.debug("Fetching messages for room '#{room}' from ETS table #{inspect(@table_name)}")
180
181
# Use match_object to get all messages for the room
182
all_matching = :ets.match_object(@table_name, {{room, :_}, :_})
183
Logger.debug("Found #{length(all_matching)} raw entries for room #{room}")
184
185
# Show raw results for debugging
186
if length(all_matching) > 0 do
187
Logger.debug("First matched entry: #{inspect(hd(all_matching))}")
188
end
189
190
# Extract the messages from the match_object results
191
messages = Enum.map(all_matching, fn {_key, msg} -> msg end)
192
193
# Log what we found
194
Logger.debug("Retrieved #{length(messages)} message structs for room #{room}")
195
196
# Sort and return the messages
197
sorted_messages =
198
messages
199
|> Enum.sort_by(fn msg -> msg.id end, :desc)
200
|> Enum.take(50)
201
202
Logger.debug("Returning #{length(sorted_messages)} sorted messages")
203
sorted_messages
204
end
205
206
@doc """
207
Debug function to list all messages in all rooms.
208
"""
209
def list_all_messages do
210
# Get all objects from the table
211
all_messages = :ets.tab2list(@table_name)
212
Logger.debug("Total messages in ETS: #{length(all_messages)}")
213
214
# Log the raw data
215
if length(all_messages) > 0 do
216
sample = Enum.take(all_messages, 3)
217
Logger.debug("Raw message data sample: #{inspect(sample)}")
218
end
219
220
# Group by room
221
result = all_messages
222
|> Enum.map(fn {{room, _id}, message} -> {room, message} end)
223
|> Enum.group_by(fn {room, _} -> room end, fn {_, message} -> message end)
224
225
# Log the count per room
226
Enum.each(result, fn {room, msgs} ->
227
Logger.debug("Room #{room} has #{length(msgs)} messages")
228
end)
229
230
result
231
end
232
233
@doc """
234
Clears all messages from a room.
235
"""
236
def clear_room(room) do
237
# Get all messages in the room
238
messages = :ets.match_object(@table_name, {{room, :_}, :_})
239
240
# Delete them one by one
241
deleted_count = Enum.reduce(messages, 0, fn {{r, id}, _}, acc ->
242
:ets.delete(@table_name, {r, id})
243
acc + 1
244
end)
245
246
Logger.info("Cleared #{deleted_count} messages from room #{room}")
247
deleted_count
248
end
249
250
@doc """
251
Clears all messages from all rooms.
252
"""
253
def clear_all do
254
count = :ets.info(@table_name, :size)
255
:ets.delete_all_objects(@table_name)
256
Logger.info("Cleared all #{count} chat messages from all rooms")
257
initialize_rooms()
258
end
259
end
260