1 /* ncdc - NCurses Direct Connect client
2
3 Copyright (c) 2011-2019 Yoran Heling
4
5 Permission is hereby granted, free of charge, to any person obtaining
6 a copy of this software and associated documentation files (the
7 "Software"), to deal in the Software without restriction, including
8 without limitation the rights to use, copy, modify, merge, publish,
9 distribute, sublicense, and/or sell copies of the Software, and to
10 permit persons to whom the Software is furnished to do so, subject to
11 the following conditions:
12
13 The above copyright notice and this permission notice shall be included
14 in all copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
24 */
25
26
27 #include "ncdc.h"
28 #include "uit_userlist.h"
29
30
31 ui_tab_type_t uit_userlist[1];
32
33
34 typedef struct tab_t {
35 ui_tab_t tab;
36 ui_listing_t *list;
37 int order;
38 gboolean reverse : 1;
39 gboolean details : 1;
40 gboolean opfirst : 1;
41 gboolean hide_desc : 1;
42 gboolean hide_tag : 1;
43 gboolean hide_mail : 1;
44 gboolean hide_conn : 1;
45 gboolean hide_ip : 1;
46 int cw_user, cw_country, cw_share, cw_conn, cw_desc, cw_mail, cw_tag, cw_ip;
47 } tab_t;
48
49
50
51 // Columns to sort on
52 #define SORT_USER 0
53 #define SORT_SHARE 1
54 #define SORT_CONN 2
55 #define SORT_DESC 3
56 #define SORT_MAIL 4
57 #define SORT_CLIENT 5
58 #define SORT_IP 6
59
60
sort_func(gconstpointer da,gconstpointer db,gpointer dat)61 static gint sort_func(gconstpointer da, gconstpointer db, gpointer dat) {
62 const hub_user_t *a = da;
63 const hub_user_t *b = db;
64 tab_t *t = dat;
65 int p = t->order;
66
67 if(t->opfirst && !a->isop != !b->isop)
68 return a->isop && !b->isop ? -1 : 1;
69
70 // All orders have the username as secondary order.
71 int o = p == SORT_USER ? 0 :
72 p == SORT_SHARE ? a->sharesize > b->sharesize ? 1 : -1:
73 p == SORT_CONN ? (t->tab.hub->adc ? a->conn - b->conn : strcmp(a->conn?a->conn:"", b->conn?b->conn:"")) :
74 p == SORT_DESC ? g_utf8_collate(a->desc?a->desc:"", b->desc?b->desc:"") :
75 p == SORT_MAIL ? g_utf8_collate(a->mail?a->mail:"", b->mail?b->mail:"") :
76 p == SORT_CLIENT ? strcmp(a->client?a->client:"", b->client?b->client:"")
77 : (ip4_cmp(a->ip4, b->ip4) != 0 ? ip4_cmp(a->ip4, b->ip4) : ip6_cmp(a->ip6, b->ip6));
78
79 // Username sort
80 if(!o)
81 o = g_utf8_collate(a->name, b->name);
82 if(!o && a->name_hub && b->name_hub)
83 o = strcmp(a->name_hub, b->name_hub);
84 if(!o)
85 o = a - b;
86 return t->reverse ? -1*o : o;
87 }
88
89
get_name(GSequenceIter * iter)90 static const char *get_name(GSequenceIter *iter) {
91 hub_user_t *u = g_sequence_get(iter);
92 return u->name;
93 }
94
95
96
uit_userlist_create(hub_t * hub)97 ui_tab_t *uit_userlist_create(hub_t *hub) {
98 tab_t *t = g_new0(tab_t, 1);
99 t->tab.name = g_strdup_printf("@%s", hub->tab->name+1);
100 t->tab.type = uit_userlist;
101 t->tab.hub = hub;
102 t->opfirst = TRUE;
103 t->hide_conn = TRUE;
104 t->hide_mail = TRUE;
105 t->hide_ip = TRUE;
106
107 GSequence *users = g_sequence_new(NULL);
108 // populate the list
109 // g_sequence_sort() uses insertion sort? in that case it is faster to insert
110 // all items using g_sequence_insert_sorted() rather than inserting them in
111 // no particular order and then sorting them in one go. (which is faster for
112 // linked lists, since it uses a faster sorting algorithm)
113 GHashTableIter iter;
114 g_hash_table_iter_init(&iter, hub->users);
115 hub_user_t *u;
116 while(g_hash_table_iter_next(&iter, NULL, (gpointer *)&u))
117 u->iter = g_sequence_insert_sorted(users, u, sort_func, t);
118 t->list = ui_listing_create(users, NULL, t, get_name);
119
120 return (ui_tab_t *)t;
121 }
122
123
t_close(ui_tab_t * tab)124 static void t_close(ui_tab_t *tab) {
125 tab_t *t = (tab_t *)tab;
126 uit_hub_set_userlist(t->tab.hub->tab, NULL);
127 ui_tab_remove(tab);
128 // To clean things up, we should also reset all hub_user->iter fields. But
129 // this isn't all that necessary since they won't be used anymore until they
130 // get reset in a subsequent ui_userlist_create().
131 g_sequence_free(t->list->list);
132 ui_listing_free(t->list);
133 g_free(t->tab.name);
134 g_free(t);
135 }
136
137
t_title(ui_tab_t * tab)138 static char *t_title(ui_tab_t *tab) {
139 return g_strdup_printf("%s / User list", tab->hub->tab->name);
140 }
141
142
143 #define DRAW_COL(row, colvar, width, str) do {\
144 if(width > 1)\
145 mvaddnstr(row, colvar, str, str_offset_from_columns(str, width-1));\
146 colvar += width;\
147 } while(0)
148
149
draw_row(ui_listing_t * list,GSequenceIter * iter,int row,void * dat)150 static void draw_row(ui_listing_t *list, GSequenceIter *iter, int row, void *dat) {
151 hub_user_t *user = g_sequence_get(iter);
152 tab_t *t = dat;
153
154 char *tag = hub_user_tag(user);
155 char *conn = hub_user_conn(user);
156
157 attron(iter == list->sel ? UIC(list_select) : UIC(list_default));
158 mvhline(row, 0, ' ', wincols);
159 if(iter == list->sel)
160 mvaddch(row, 0, '>');
161
162 if(user->isop)
163 mvaddch(row, 2, 'o');
164 if(!user->active)
165 mvaddch(row, 3, 'p');
166 if(user->hastls)
167 mvaddch(row, 4, 't');
168
169 int j = 6;
170 const char *cc =
171 !ip4_isany(user->ip4) ? geoip_country(ip4_sockaddr(user->ip4, 0)) :
172 !ip6_isany(user->ip6) ? geoip_country(ip6_sockaddr(user->ip6, 0)) : NULL;
173 DRAW_COL(row, j, t->cw_country, cc?cc:"");
174 if(t->cw_user > 1)
175 ui_listing_draw_match(list, iter, row, j, str_offset_from_columns(user->name, t->cw_user-1));
176 j += t->cw_user;
177 DRAW_COL(row, j, t->cw_share, user->hasinfo ? str_formatsize(user->sharesize) : "");
178 DRAW_COL(row, j, t->cw_desc, user->desc?user->desc:"");
179 DRAW_COL(row, j, t->cw_tag, tag?tag:"");
180 DRAW_COL(row, j, t->cw_mail, user->mail?user->mail:"");
181 DRAW_COL(row, j, t->cw_conn, conn?conn:"");
182 DRAW_COL(row, j, t->cw_ip, hub_user_ip(user, ""));
183 g_free(conn);
184 g_free(tag);
185
186 attroff(iter == list->sel ? UIC(list_select) : UIC(list_default));
187 }
188
189
190 /* Distributing a width among several columns with given weights:
191 * w_t = sum(i=c_v; w_i)
192 * w_s = 1 + sum(i=c_h; w_i/w_t)
193 * b_i = w_i*w_s
194 * Where:
195 * c_v = set of all visible columns
196 * c_h = set of all hidden columns
197 * w_i = weight of column $i
198 * w_t = sum of the weights of all visible columns
199 * w_s = scale factor
200 * b_i = calculated width of column $i, with 0 < b_i <= 1
201 *
202 * TODO: abstract this, so that the weights and such don't need repetition.
203 */
calc_widths(tab_t * t)204 static void calc_widths(tab_t *t) {
205 // available width
206 int w = wincols-6;
207
208 // Country code column (fixed size)
209 t->cw_country = geoip_available ? 3 : 0;
210 w -= t->cw_country;
211
212 // share has a fixed size
213 t->cw_share = 12;
214 w -= 12;
215
216 // IP column as well
217 t->cw_ip = t->hide_ip ? 0 : 39;
218 w -= t->cw_ip;
219
220 // User column has a minimum size (but may grow a bit later on, so will still be counted as a column)
221 t->cw_user = 15;
222 w -= 15;
223
224 // Total weight (first one is for the user column)
225 double wt = 0.02
226 + (t->hide_conn ? 0.0 : 0.16)
227 + (t->hide_desc ? 0.0 : 0.32)
228 + (t->hide_mail ? 0.0 : 0.18)
229 + (t->hide_tag ? 0.0 : 0.32);
230
231 // Scale factor
232 double ws = 1.0 + (
233 + (t->hide_conn ? 0.16: 0.0)
234 + (t->hide_desc ? 0.32: 0.0)
235 + (t->hide_mail ? 0.18: 0.0)
236 + (t->hide_tag ? 0.32: 0.0))/wt;
237 // scale to available width
238 ws *= w;
239
240 // Get the column widths. Note the use of floor() here, this prevents that
241 // the total width exceeds the available width. The remaining columns will be
242 // given to the user column, which is always present anyway.
243 t->cw_conn = t->hide_conn ? 0 : floor(0.16*ws);
244 t->cw_desc = t->hide_desc ? 0 : floor(0.32*ws);
245 t->cw_mail = t->hide_mail ? 0 : floor(0.18*ws);
246 t->cw_tag = t->hide_tag ? 0 : floor(0.32*ws);
247 t->cw_user += w - t->cw_conn - t->cw_desc - t->cw_mail - t->cw_tag;
248 }
249
250
t_draw(ui_tab_t * tab)251 static void t_draw(ui_tab_t *tab) {
252 tab_t *t = (tab_t *)tab;
253
254 calc_widths(t);
255
256 // header
257 attron(UIC(list_header));
258 mvhline(1, 0, ' ', wincols);
259 mvaddstr(1, 2, "opt");
260 int i = 6;
261 DRAW_COL(1, i, t->cw_country, "CC");
262 DRAW_COL(1, i, t->cw_user, "Username");
263 DRAW_COL(1, i, t->cw_share, "Share");
264 DRAW_COL(1, i, t->cw_desc, "Description");
265 DRAW_COL(1, i, t->cw_tag, "Tag");
266 DRAW_COL(1, i, t->cw_mail, "E-Mail");
267 DRAW_COL(1, i, t->cw_conn, "Connection");
268 DRAW_COL(1, i, t->cw_ip, "IP");
269 attroff(UIC(list_header));
270
271 // rows
272 int bottom = t->details ? winrows-7 : winrows-3;
273 ui_cursor_t cursor;
274 int pos = ui_listing_draw(t->list, 2, bottom-1, &cursor, draw_row);
275
276 // footer
277 attron(UIC(separator));
278 mvhline(bottom, 0, ' ', wincols);
279 int count = g_hash_table_size(t->tab.hub->users);
280 mvaddstr(bottom, 0, "Totals:");
281 mvprintw(bottom, t->cw_user+6, "%s%c %d users",
282 str_formatsize(t->tab.hub->sharesize), t->tab.hub->sharecount == count ? ' ' : '+', count);
283 mvprintw(bottom, wincols-6, "%3d%%", pos);
284 attroff(UIC(separator));
285
286 // detailed info box
287 if(t->details && g_sequence_iter_is_end(t->list->sel))
288 mvaddstr(bottom+1, 2, "No user selected.");
289 else if(t->details) {
290 hub_user_t *u = g_sequence_get(t->list->sel);
291 attron(A_BOLD);
292 mvaddstr(bottom+1, 4, "Username:");
293 mvaddstr(bottom+1, 25+16, "Share:");
294 mvaddstr(bottom+2, 2, "Connection:");
295 mvaddstr(bottom+2, 25+19, "IP:");
296 mvaddstr(bottom+3, 6, "E-Mail:");
297 mvaddstr(bottom+3, 25+18, "Tag:");
298 mvaddstr(bottom+4, 1, "Description:");
299 attroff(A_BOLD);
300 mvaddstr(bottom+1, 14, u->name);
301 if(u->hasinfo)
302 mvprintw(bottom+1, 25+23, "%s (%s bytes)", str_formatsize(u->sharesize), str_fullsize(u->sharesize));
303 else
304 mvaddstr(bottom+1, 25+23, "-");
305 char *conn = hub_user_conn(u);
306 mvaddstr(bottom+2, 14, conn?conn:"-");
307 g_free(conn);
308 mvaddstr(bottom+2, 25+23, hub_user_ip(u, "-"));
309 mvaddstr(bottom+3, 14, u->mail?u->mail:"-");
310 char *tag = hub_user_tag(u);
311 mvaddstr(bottom+3, 25+23, tag?tag:"-");
312 g_free(tag);
313 mvaddstr(bottom+4, 14, u->desc?u->desc:"-");
314 // TODO: CID?
315 }
316
317 move(cursor.y, cursor.x);
318 }
319 #undef DRAW_COL
320
321
t_key(ui_tab_t * tab,guint64 key)322 static void t_key(ui_tab_t *tab, guint64 key) {
323 tab_t *t = (tab_t *)tab;
324
325 if(ui_listing_key(t->list, key, winrows/2))
326 return;
327
328 hub_user_t *sel = g_sequence_iter_is_end(t->list->sel) ? NULL : g_sequence_get(t->list->sel);
329 gboolean sort = FALSE;
330 switch(key) {
331 case INPT_CHAR('?'):
332 uit_main_keys("userlist");
333 break;
334
335 // Sorting
336 #define SETSORT(c) \
337 t->reverse = t->order == c ? !t->reverse : FALSE;\
338 t->order = c;\
339 sort = TRUE;
340
341 case INPT_CHAR('s'): // s/S - sort on share size
342 case INPT_CHAR('S'):
343 SETSORT(SORT_SHARE);
344 break;
345 case INPT_CHAR('u'): // u/U - sort on username
346 case INPT_CHAR('U'):
347 SETSORT(SORT_USER)
348 break;
349 case INPT_CHAR('D'): // D - sort on description
350 SETSORT(SORT_DESC)
351 break;
352 case INPT_CHAR('T'): // T - sort on client (= tag)
353 SETSORT(SORT_CLIENT)
354 break;
355 case INPT_CHAR('E'): // E - sort on email
356 SETSORT(SORT_MAIL)
357 break;
358 case INPT_CHAR('C'): // C - sort on connection
359 SETSORT(SORT_CONN)
360 break;
361 case INPT_CHAR('P'): // P - sort on IP
362 SETSORT(SORT_IP)
363 break;
364 case INPT_CHAR('o'): // o - toggle sorting OPs before others
365 t->opfirst = !t->opfirst;
366 sort = TRUE;
367 break;
368 #undef SETSORT
369
370 // Column visibility
371 case INPT_CHAR('d'): // d (toggle description visibility)
372 t->hide_desc = !t->hide_desc;
373 break;
374 case INPT_CHAR('t'): // t (toggle tag visibility)
375 t->hide_tag = !t->hide_tag;
376 break;
377 case INPT_CHAR('e'): // e (toggle e-mail visibility)
378 t->hide_mail = !t->hide_mail;
379 break;
380 case INPT_CHAR('c'): // c (toggle connection visibility)
381 t->hide_conn = !t->hide_conn;
382 break;
383 case INPT_CHAR('p'): // p (toggle IP visibility)
384 t->hide_ip = !t->hide_ip;
385 break;
386
387 case INPT_CTRL('j'): // newline
388 case INPT_CHAR('i'): // i (toggle user info)
389 t->details = !t->details;
390 break;
391 case INPT_CHAR('m'): // m (/msg user)
392 if(!sel)
393 ui_m(NULL, 0, "No user selected.");
394 else
395 uit_msg_open(sel->uid, tab);
396 break;
397 case INPT_CHAR('g'): // g (grant slot)
398 if(!sel)
399 ui_m(NULL, 0, "No user selected.");
400 else {
401 db_users_set(sel->hub->id, sel->uid, sel->name, db_users_get(sel->hub->id, sel->name) | DB_USERFLAG_GRANT);
402 ui_m(NULL, 0, "Slot granted.");
403 }
404 break;
405 case INPT_CHAR('b'): // b (/browse userlist)
406 case INPT_CHAR('B'): // B (force /browse userlist)
407 if(!sel)
408 ui_m(NULL, 0, "No user selected.");
409 else
410 uit_fl_queue(sel->uid, key == INPT_CHAR('B'), NULL, tab, TRUE, FALSE);
411 break;
412 case INPT_CHAR('q'): // q - download filelist and match queue for selected user
413 if(!sel)
414 ui_m(NULL, 0, "No user selected.");
415 else
416 uit_fl_queue(sel->uid, FALSE, NULL, NULL, FALSE, TRUE);
417 break;
418 }
419
420 if(sort) {
421 g_sequence_sort(t->list->list, sort_func, tab);
422 ui_listing_sorted(t->list);
423 ui_mf(NULL, 0, "Ordering by %s (%s%s)",
424 t->order == SORT_USER ? "user name" :
425 t->order == SORT_SHARE ? "share size" :
426 t->order == SORT_CONN ? "connection" :
427 t->order == SORT_DESC ? "description" :
428 t->order == SORT_MAIL ? "e-mail" :
429 t->order == SORT_CLIENT? "tag" : "IP address",
430 t->reverse ? "descending" : "ascending", t->opfirst ? ", OPs first" : "");
431 }
432 }
433
434
435 // Called when the hub is disconnected. All users should be removed in one go,
436 // this is faster than a _userchange() for every user.
uit_userlist_disconnect(ui_tab_t * tab)437 void uit_userlist_disconnect(ui_tab_t *tab) {
438 tab_t *t = (tab_t *)tab;
439
440 g_sequence_free(t->list->list);
441 ui_listing_free(t->list);
442 t->list = ui_listing_create(g_sequence_new(NULL), NULL, t, get_name);
443 }
444
445
446 // Called from the hub tab when something changes to the user list.
uit_userlist_userchange(ui_tab_t * tab,int change,hub_user_t * user)447 void uit_userlist_userchange(ui_tab_t *tab, int change, hub_user_t *user) {
448 tab_t *t = (tab_t *)tab;
449
450 if(change == UIHUB_UC_JOIN) {
451 user->iter = g_sequence_insert_sorted(t->list->list, user, sort_func, t);
452 ui_listing_inserted(t->list);
453 } else if(change == UIHUB_UC_QUIT) {
454 g_return_if_fail(g_sequence_get(user->iter) == (gpointer)user);
455 ui_listing_remove(t->list, user->iter);
456 g_sequence_remove(user->iter);
457 } else {
458 g_sequence_sort_changed(user->iter, sort_func, t);
459 ui_listing_sorted(t->list);
460 }
461 }
462
463
464 // Opens the user list for a hub and selects the user specified by uid or
465 // user/utf8. Returns FALSE if a user was specified but could not be found.
uit_userlist_open(hub_t * hub,guint64 uid,const char * user,gboolean utf8)466 gboolean uit_userlist_open(hub_t *hub, guint64 uid, const char *user, gboolean utf8) {
467 hub_user_t *u =
468 !uid && !user ? NULL :
469 uid ? g_hash_table_lookup(hub_uids, &uid) :
470 utf8 ? hub_user_get(hub, user) : g_hash_table_lookup(hub->users, user);
471 if((uid || user) && (!u || u->hub != hub))
472 return FALSE;
473
474 ui_tab_t *ut = uit_hub_userlist(hub->tab);
475 if(ut)
476 ui_tab_cur = g_list_find(ui_tabs, ut);
477 else {
478 ut = uit_userlist_create(hub);
479 ui_tab_open(ut, TRUE, hub->tab);
480 uit_hub_set_userlist(hub->tab, ut);
481 }
482
483 if(u) {
484 tab_t *t = (tab_t *)ut;
485 // u->iter should be valid at this point.
486 t->list->sel = u->iter;
487 t->details = TRUE;
488 }
489 return TRUE;
490 }
491
492
493 ui_tab_type_t uit_userlist[1] = { { t_draw, t_title, t_key, t_close } };
494
495