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