1 #include "chatlog.h"
2
3 #include "filesys.h"
4 // TODO including native.h files should never be needed, refactor filesys.h to provide necessary API
5 #include "debug.h"
6 #include "messages.h"
7 #include "text.h"
8
9 #include "native/filesys.h"
10
11 #include <stdint.h>
12 #include <stdlib.h>
13
chatlog_get_file(char hex[TOX_PUBLIC_KEY_SIZE * 2],bool append)14 static FILE* chatlog_get_file(char hex[TOX_PUBLIC_KEY_SIZE * 2], bool append) {
15 char name[TOX_PUBLIC_KEY_SIZE * 2 + sizeof(".new.txt")];
16 snprintf(name, sizeof(name), "%.*s.new.txt", TOX_PUBLIC_KEY_SIZE * 2, hex);
17
18 FILE *file;
19 if (append) {
20 file = utox_get_file(name, NULL, UTOX_FILE_OPTS_READ | UTOX_FILE_OPTS_WRITE | UTOX_FILE_OPTS_MKDIR);
21 if (!file) {
22 return NULL;
23 }
24
25 fseek(file, 0, SEEK_END);
26 } else {
27 file = utox_get_file(name, NULL, UTOX_FILE_OPTS_READ);
28 }
29
30 return file;
31 }
32
utox_save_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2],uint8_t * data,size_t length)33 size_t utox_save_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2], uint8_t *data, size_t length) {
34 FILE *fp = chatlog_get_file(hex, true);
35 if (!fp) {
36 LOG_ERR("uTox", "Error getting a file handle for this chatlog!");
37 return 0;
38 }
39 // Seek to the beginning of the file first because grayhatter has had issues with this on Windows.
40 // (and he really doesn't want uTox eating people's chat logs)
41 fseeko(fp, 0, SEEK_SET);
42 fseeko(fp, 0, SEEK_END);
43 off_t offset = ftello(fp);
44 fwrite(data, length, 1, fp);
45 fclose(fp);
46
47 return offset;
48 }
49
utox_count_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2])50 static size_t utox_count_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2]) {
51 FILE *file = chatlog_get_file(hex, false);
52
53 if (!file) {
54 return 0;
55 }
56
57 LOG_FILE_MSG_HEADER header;
58 size_t records_count = 0;
59
60 while (fread(&header, sizeof(header), 1, file) == 1) {
61 fseeko(file, header.author_length + header.msg_length + 1, SEEK_CUR);
62 records_count++;
63 }
64
65
66 if (ferror(file) || !feof(file)) {
67 /* TODO: consider removing or truncating the log file.
68 * If !feof() this means that the file has an incomplete record,
69 * which would prevent it from loading forever, even though
70 * new records will keep being appended as usual. */
71 LOG_ERR("Chatlog", "Log read err; trying to count history for friend %.*s", TOX_PUBLIC_KEY_SIZE * 2, hex);
72 fclose(file);
73 return 0;
74 }
75
76 fclose(file);
77 return records_count;
78 }
79
80 /* TODO create fxn that will try to recover a corrupt chat history.
81 *
82 * In the majority of bug reports the corrupt message is often the first, so in
83 * theory we should be able to trim the start of the chatlog up to and including
84 * the first \n char. We may have to do so multiple times, but once we find the
85 * first valid message everything else should "work" */
utox_load_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2],size_t * size,uint32_t count,uint32_t skip)86 MSG_HEADER **utox_load_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2], size_t *size, uint32_t count, uint32_t skip) {
87 /* Because every platform is different, we have to ask them to open the file for us.
88 * However once we have it, every platform does the same thing, this should prevent issues
89 * from occurring on a single platform. */
90
91 size_t records_count = utox_count_chatlog(hex);
92 if (skip >= records_count) {
93 if (skip > 0) {
94 LOG_ERR("Chatlog", "Error, skipped all records");
95 } else {
96 LOG_INFO("Chatlog", "No log exists.");
97 }
98 return NULL;
99 }
100
101 FILE *file = chatlog_get_file(hex, false);
102 if (!file) {
103 LOG_TRACE("Chatlog", "Log read:\tUnable to access file provided.");
104 return NULL;
105 }
106
107 if (count > (records_count - skip)) {
108 count = records_count - skip;
109 }
110
111 MSG_HEADER **data = calloc(count + 1, sizeof(MSG_HEADER *));
112 MSG_HEADER **start = data;
113
114 if (!data) {
115 LOG_ERR("Chatlog", "Log read:\tCouldn't allocate memory for log entries.");
116 fclose(file);
117 return NULL;
118 }
119
120 size_t start_at = records_count - count - skip;
121 size_t actual_count = 0;
122
123 size_t file_offset = 0;
124
125 LOG_FILE_MSG_HEADER header;
126 while (fread(&header, sizeof(header), 1, file) == 1) {
127 if (start_at) {
128 fseeko(file, header.author_length, SEEK_CUR); /* Skip the recorded author */
129 fseeko(file, header.msg_length, SEEK_CUR); /* Skip the message */
130 fseeko(file, 1, SEEK_CUR); /* Skip the newline char */
131 start_at--;
132 file_offset = ftello(file);
133 continue;
134 }
135
136 if (count) {
137 /* we have to skip the author name for now, it's left here for group chats support in the future */
138 fseeko(file, header.author_length, SEEK_CUR);
139 if (header.msg_length > 1 << 16) {
140 LOG_ERR("Chatlog", "Can't malloc that much, you'll probably have to move or delete your"
141 " history for this peer.\n\t\tFriend number %.*s, count %u,"
142 " actual_count %lu, start at %lu, error size %lu.\n",
143 TOX_PUBLIC_KEY_SIZE * 2, hex, count, actual_count, start_at, header.msg_length);
144 if (size) {
145 *size = 0;
146 }
147
148 fclose(file);
149 return start;
150 }
151
152 MSG_HEADER *msg = calloc(1, sizeof(MSG_HEADER));
153 if (!msg) {
154 LOG_ERR("Chatlog", "Unable to malloc... sorry!");
155 free(start);
156 fclose(file);
157 return NULL;
158 }
159
160 msg->our_msg = header.author;
161 msg->receipt_time = header.receipt;
162 msg->time = header.time;
163 msg->msg_type = header.msg_type;
164 msg->disk_offset = file_offset;
165
166 msg->via.txt.length = header.msg_length;
167 msg->via.txt.msg = calloc(1, msg->via.txt.length);
168 if (!msg->via.txt.msg) {
169 LOG_ERR("Chatlog", "Unable to malloc for via.txt.msg... sorry!");
170 free(start);
171 free(msg);
172 fclose(file);
173 return NULL;
174 }
175
176 msg->via.txt.author_length = header.author_length;
177 // TODO: msg->via.txt.author used to be allocated but left empty. Commented out for now.
178 // msg->via.txt.author = calloc(1, msg->via.txt.author_length);
179 // if (!msg->via.txt.author) {
180 // LOG_ERR("Chatlog", "Unable to malloc for via.txt.author... sorry!");
181 // free(msg->via.txt.msg);
182 // free(msg);
183 // fclose(file);
184 // return NULL;
185 // }
186
187 if (fread(msg->via.txt.msg, msg->via.txt.length, 1, file) != 1) {
188 LOG_ERR("Chatlog", "Log read:\tError reading record %u of length %u at offset %lu: stopping.",
189 count, msg->via.txt.length, msg->disk_offset);
190 // free(msg->via.txt.author);
191 free(msg->via.txt.msg);
192 free(msg);
193 break;
194 }
195
196 msg->via.txt.length = utf8_validate((uint8_t *)msg->via.txt.msg, msg->via.txt.length);
197 *data++ = msg;
198 --count;
199 ++actual_count;
200 fseeko(file, 1, SEEK_CUR); /* seek an extra \n char */
201 file_offset = ftello(file);
202 }
203 }
204
205 fclose(file);
206
207 if (size) {
208 *size = actual_count;
209 }
210
211 return start;
212 }
213
utox_update_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2],size_t offset,uint8_t * data,size_t length)214 bool utox_update_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2], size_t offset, uint8_t *data, size_t length) {
215 FILE *file = chatlog_get_file(hex, true);
216
217 if (!file) {
218 LOG_ERR("History", "Unable to access file provided.");
219 return false;
220 }
221
222 if (fseeko(file, offset, SEEK_SET)) {
223 LOG_ERR("Chatlog", "History:\tUnable to seek to position %lu in file provided.", offset);
224 fclose(file);
225 return false;
226 }
227
228 fwrite(data, length, 1, file);
229 fclose(file);
230
231 return true;
232 }
233
utox_remove_friend_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2])234 bool utox_remove_friend_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2]) {
235 char name[TOX_PUBLIC_KEY_SIZE * 2 + sizeof(".new.txt")];
236
237 snprintf(name, sizeof(name), "%.*s.new.txt", TOX_PUBLIC_KEY_SIZE * 2, hex);
238
239 return utox_remove_file((uint8_t*)name, sizeof(name));
240 }
241
utox_export_chatlog_init(uint32_t friend_number)242 void utox_export_chatlog_init(uint32_t friend_number) {
243 native_export_chatlog_init(friend_number);
244 }
245
utox_export_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2],FILE * dest_file)246 void utox_export_chatlog(char hex[TOX_PUBLIC_KEY_SIZE * 2], FILE *dest_file) {
247 if (!dest_file) {
248 return;
249 }
250
251 LOG_FILE_MSG_HEADER header;
252 FILE *file = chatlog_get_file(hex, false);
253
254 struct tm *tm_curr;
255 struct tm_tmp {
256 int tm_year;
257 int tm_mon;
258 int tm_mday;
259 } tm_prev = { .tm_mday = 1};
260
261 while (fread(&header, sizeof(header), 1, file) == 1) {
262 tm_curr = localtime(&header.time);
263
264 if (tm_curr->tm_year > tm_prev.tm_year
265 || (tm_curr->tm_year == tm_prev.tm_year && tm_curr->tm_mon > tm_prev.tm_mon)
266 || (tm_curr->tm_year == tm_prev.tm_year && tm_curr->tm_mon == tm_prev.tm_mon && tm_curr->tm_mday > tm_prev.tm_mday))
267 {
268 char buffer[128];
269 size_t len = strftime(buffer, 128, "Day has changed to %A %B %d %Y\n", tm_curr);
270 fwrite(buffer, len, 1, dest_file);
271 }
272
273 /* Write Timestamp */
274 fprintf(dest_file, "[%02d:%02d]", tm_curr->tm_hour, tm_curr->tm_min);
275 tm_prev.tm_year = tm_curr->tm_year;
276 tm_prev.tm_mon = tm_curr->tm_mon;
277 tm_prev.tm_mday = tm_curr->tm_mday;
278
279 int c;
280 if (header.msg_type == MSG_TYPE_NOTICE) {
281 fseek(file, header.author_length, SEEK_CUR);
282 } else {
283 /* Write Author */
284 fwrite(" <", 2, 1, dest_file);
285 for (size_t i = 0; i < header.author_length; ++i) {
286 c = fgetc(file);
287 if (c != EOF) {
288 fputc(c, dest_file);
289 }
290 }
291 fwrite(">", 1, 1, dest_file);
292 }
293
294 /* Write text */
295 fwrite(" ", 1, 1, dest_file);
296 for (size_t i = 0; i < header.msg_length; ++i) {
297 c = fgetc(file);
298 if (c != EOF) {
299 fputc(c, dest_file);
300 }
301 }
302 c = fgetc(file); /* the newline char */
303 fputc(c, dest_file);
304 }
305
306 fclose(file);
307 fclose(dest_file);
308 }
309