html.c 89 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791
  1. /* snac - A simple, minimalistic ActivityPub instance */
  2. /* copyright (c) 2022 - 2023 grunfink et al. / MIT license */
  3. #include "xs.h"
  4. #include "xs_io.h"
  5. #include "xs_json.h"
  6. #include "xs_regex.h"
  7. #include "xs_set.h"
  8. #include "xs_openssl.h"
  9. #include "xs_time.h"
  10. #include "xs_mime.h"
  11. #include "xs_match.h"
  12. #include "xs_html.h"
  13. #include "snac.h"
  14. int login(snac *snac, const xs_dict *headers)
  15. /* tries a login */
  16. {
  17. int logged_in = 0;
  18. const char *auth = xs_dict_get(headers, "authorization");
  19. if (auth && xs_startswith(auth, "Basic ")) {
  20. int sz;
  21. xs *s1 = xs_crop_i(xs_dup(auth), 6, 0);
  22. xs *s2 = xs_base64_dec(s1, &sz);
  23. xs *l1 = xs_split_n(s2, ":", 1);
  24. if (xs_list_len(l1) == 2) {
  25. logged_in = check_password(
  26. xs_list_get(l1, 0), xs_list_get(l1, 1),
  27. xs_dict_get(snac->config, "passwd"));
  28. }
  29. }
  30. if (logged_in)
  31. lastlog_write(snac, "web");
  32. return logged_in;
  33. }
  34. xs_str *actor_name(xs_dict *actor)
  35. /* gets the actor name */
  36. {
  37. xs_list *p;
  38. char *v;
  39. xs_str *name;
  40. if (xs_is_null((v = xs_dict_get(actor, "name"))) || *v == '\0') {
  41. if (xs_is_null(v = xs_dict_get(actor, "preferredUsername")) || *v == '\0') {
  42. v = "anonymous";
  43. }
  44. }
  45. name = encode_html(v);
  46. /* replace the :shortnames: */
  47. if (!xs_is_null(p = xs_dict_get(actor, "tag"))) {
  48. xs *tag = NULL;
  49. if (xs_type(p) == XSTYPE_DICT) {
  50. /* not a list */
  51. tag = xs_list_new();
  52. tag = xs_list_append(tag, p);
  53. } else {
  54. /* is a list */
  55. tag = xs_dup(p);
  56. }
  57. xs_list *tags = tag;
  58. /* iterate the tags */
  59. while (xs_list_iter(&tags, &v)) {
  60. char *t = xs_dict_get(v, "type");
  61. if (t && strcmp(t, "Emoji") == 0) {
  62. char *n = xs_dict_get(v, "name");
  63. char *i = xs_dict_get(v, "icon");
  64. if (n && i) {
  65. char *u = xs_dict_get(i, "url");
  66. xs *img = xs_fmt("<img loading=\"lazy\" src=\"%s\" style=\"height: 1em; vertical-align: middle;\"/>", u);
  67. name = xs_replace_i(name, n, img);
  68. }
  69. }
  70. }
  71. }
  72. return name;
  73. }
  74. xs_html *html_actor_icon(xs_dict *actor, const char *date,
  75. const char *udate, const char *url, int priv)
  76. {
  77. xs_html *actor_icon = xs_html_tag("p", NULL);
  78. xs *avatar = NULL;
  79. char *v;
  80. xs *name = actor_name(actor);
  81. /* get the avatar */
  82. if ((v = xs_dict_get(actor, "icon")) != NULL &&
  83. (v = xs_dict_get(v, "url")) != NULL) {
  84. avatar = xs_dup(v);
  85. }
  86. if (avatar == NULL)
  87. avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64());
  88. xs_html_add(actor_icon,
  89. xs_html_sctag("img",
  90. xs_html_attr("loading", "lazy"),
  91. xs_html_attr("class", "snac-avatar"),
  92. xs_html_attr("src", avatar),
  93. xs_html_attr("alt", "")),
  94. xs_html_tag("a",
  95. xs_html_attr("href", xs_dict_get(actor, "id")),
  96. xs_html_attr("class", "p-author h-card snac-author"),
  97. xs_html_raw(name))); /* name is already html-escaped */
  98. if (!xs_is_null(url)) {
  99. xs_html_add(actor_icon,
  100. xs_html_text(" "),
  101. xs_html_tag("a",
  102. xs_html_attr("href", (char *)url),
  103. xs_html_text("»")));
  104. }
  105. if (priv) {
  106. xs_html_add(actor_icon,
  107. xs_html_text(" "),
  108. xs_html_tag("span",
  109. xs_html_attr("title", "private"),
  110. xs_html_raw("&#128274;")));
  111. }
  112. if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) {
  113. xs_html_add(actor_icon,
  114. xs_html_text(" "),
  115. xs_html_tag("span",
  116. xs_html_attr("title", "bot"),
  117. xs_html_raw("&#129302;")));
  118. }
  119. if (xs_is_null(date)) {
  120. xs_html_add(actor_icon,
  121. xs_html_raw("&nbsp;"));
  122. }
  123. else {
  124. xs *date_label = xs_crop_i(xs_dup(date), 0, 10);
  125. xs *date_title = xs_dup(date);
  126. if (!xs_is_null(udate)) {
  127. xs *sd = xs_crop_i(xs_dup(udate), 0, 10);
  128. date_label = xs_str_cat(date_label, " / ", sd);
  129. date_title = xs_str_cat(date_title, " / ", udate);
  130. }
  131. xs_html_add(actor_icon,
  132. xs_html_text(" "),
  133. xs_html_tag("time",
  134. xs_html_attr("class", "dt-published snac-pubdate"),
  135. xs_html_attr("title", date_title),
  136. xs_html_text(date_label)));
  137. }
  138. {
  139. char *username, *id;
  140. if (xs_is_null(username = xs_dict_get(actor, "preferredUsername")) || *username == '\0') {
  141. /* This should never be reached */
  142. username = "anonymous";
  143. }
  144. if (xs_is_null(id = xs_dict_get(actor, "id")) || *id == '\0') {
  145. /* This should never be reached */
  146. id = "https://social.example.org/anonymous";
  147. }
  148. /* "LIKE AN ANIMAL" */
  149. xs *domain = xs_split(id, "/");
  150. xs *user = xs_fmt("@%s@%s", username, xs_list_get(domain, 2));
  151. xs_html_add(actor_icon,
  152. xs_html_sctag("br", NULL),
  153. xs_html_tag("a",
  154. xs_html_attr("href", xs_dict_get(actor, "id")),
  155. xs_html_attr("class", "p-author-tag h-card snac-author-tag"),
  156. xs_html_text(user)));
  157. }
  158. return actor_icon;
  159. }
  160. xs_str *html_msg_icon(xs_str *os, const xs_dict *msg)
  161. {
  162. char *actor_id;
  163. xs *actor = NULL;
  164. if ((actor_id = xs_dict_get(msg, "attributedTo")) == NULL)
  165. actor_id = xs_dict_get(msg, "actor");
  166. if (actor_id && valid_status(actor_get(actor_id, &actor))) {
  167. char *date = NULL;
  168. char *udate = NULL;
  169. char *url = NULL;
  170. int priv = 0;
  171. const char *type = xs_dict_get(msg, "type");
  172. if (xs_match(type, "Note|Question|Page|Article"))
  173. url = xs_dict_get(msg, "id");
  174. priv = !is_msg_public(msg);
  175. date = xs_dict_get(msg, "published");
  176. udate = xs_dict_get(msg, "updated");
  177. xs_html *actor_icon = html_actor_icon(actor, date, udate, url, priv);
  178. xs *s1 = xs_html_render(actor_icon);
  179. os = xs_str_cat(os, s1);
  180. }
  181. return os;
  182. }
  183. xs_str *html_base_header(xs_str *s)
  184. {
  185. xs_list *p;
  186. xs_str *v;
  187. s = xs_str_cat(s, "<!DOCTYPE html>\n<html>\n<head>\n");
  188. s = xs_str_cat(s, "<meta name=\"viewport\" "
  189. "content=\"width=device-width, initial-scale=1\"/>\n");
  190. s = xs_str_cat(s, "<meta name=\"generator\" "
  191. "content=\"" USER_AGENT "\"/>\n");
  192. /* add server CSS */
  193. p = xs_dict_get(srv_config, "cssurls");
  194. while (xs_list_iter(&p, &v)) {
  195. xs *s1 = xs_fmt("<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\"/>\n", v);
  196. s = xs_str_cat(s, s1);
  197. }
  198. return s;
  199. }
  200. xs_str *html_instance_header(xs_str *s, char *tag)
  201. {
  202. s = html_base_header(s);
  203. {
  204. FILE *f;
  205. xs *g_css_fn = xs_fmt("%s/style.css", srv_basedir);
  206. if ((f = fopen(g_css_fn, "r")) != NULL) {
  207. xs *css = xs_readall(f);
  208. fclose(f);
  209. xs *s1 = xs_fmt("<style>%s</style>\n", css);
  210. s = xs_str_cat(s, s1);
  211. }
  212. }
  213. const char *host = xs_dict_get(srv_config, "host");
  214. const char *title = xs_dict_get(srv_config, "title");
  215. const char *sdesc = xs_dict_get(srv_config, "short_description");
  216. const char *email = xs_dict_get(srv_config, "admin_email");
  217. const char *acct = xs_dict_get(srv_config, "admin_account");
  218. {
  219. xs *s1 = xs_fmt("<title>%s</title>\n", title && *title ? title : host);
  220. s = xs_str_cat(s, s1);
  221. }
  222. s = xs_str_cat(s, "</head>\n<body>\n");
  223. s = xs_str_cat(s, "<div class=\"snac-instance-blurb\">\n");
  224. {
  225. xs *s1 = xs_replace(snac_blurb, "%host%", host);
  226. s = xs_str_cat(s, s1);
  227. }
  228. s = xs_str_cat(s, "<dl>\n");
  229. if (sdesc && *sdesc) {
  230. xs *s1 = xs_fmt("<di><dt>%s</dt><dd>%s</dd></di>\n", L("Site description"), sdesc);
  231. s = xs_str_cat(s, s1);
  232. }
  233. if (email && *email) {
  234. xs *s1 = xs_fmt("<di><dt>%s</dt><dd>"
  235. "<a href=\"mailto:%s\">%s</a></dd></di>\n",
  236. L("Admin email"), email, email);
  237. s = xs_str_cat(s, s1);
  238. }
  239. if (acct && *acct) {
  240. xs *s1 = xs_fmt("<di><dt>%s</dt><dd>"
  241. "<a href=\"%s/%s\">@%s@%s</a></dd></di>\n",
  242. L("Admin account"), srv_baseurl, acct, acct, host);
  243. s = xs_str_cat(s, s1);
  244. }
  245. s = xs_str_cat(s, "</dl>\n");
  246. s = xs_str_cat(s, "</div>\n");
  247. {
  248. xs *l = tag ? xs_fmt(L("Search results for #%s"), tag) :
  249. xs_dup(L("Recent posts by users in this instance"));
  250. xs *s1 = xs_fmt("<h2 class=\"snac-header\">%s</h2>\n", l);
  251. s = xs_str_cat(s, s1);
  252. }
  253. return s;
  254. }
  255. xs_str *html_user_header(snac *snac, xs_str *s, int local)
  256. /* creates the HTML header */
  257. {
  258. s = html_base_header(s);
  259. /* add the user CSS */
  260. {
  261. xs *css = NULL;
  262. int size;
  263. /* try to open the user css */
  264. if (!valid_status(static_get(snac, "style.css", &css, &size, NULL, NULL))) {
  265. /* it's not there; try to open the server-wide css */
  266. FILE *f;
  267. xs *g_css_fn = xs_fmt("%s/style.css", srv_basedir);
  268. if ((f = fopen(g_css_fn, "r")) != NULL) {
  269. css = xs_readall(f);
  270. fclose(f);
  271. }
  272. }
  273. if (css != NULL) {
  274. xs *s1 = xs_fmt("<style>%s</style>\n", css);
  275. s = xs_str_cat(s, s1);
  276. }
  277. }
  278. {
  279. xs *es1 = encode_html(xs_dict_get(snac->config, "name"));
  280. xs *es2 = encode_html(snac->uid);
  281. xs *es3 = encode_html(xs_dict_get(srv_config, "host"));
  282. xs *s1 = xs_fmt("<title>%s (@%s@%s)</title>\n", es1, es2, es3);
  283. s = xs_str_cat(s, s1);
  284. }
  285. xs *avatar = xs_dup(xs_dict_get(snac->config, "avatar"));
  286. if (avatar == NULL || *avatar == '\0') {
  287. xs_free(avatar);
  288. avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64());
  289. }
  290. {
  291. xs *s_bio = xs_dup(xs_dict_get(snac->config, "bio"));
  292. int n;
  293. /* shorten the bio */
  294. for (n = 0; s_bio[n] && s_bio[n] != '&' && s_bio[n] != '.' &&
  295. s_bio[n] != '\r' && s_bio[n] != '\n' && n < 128; n++);
  296. s_bio[n] = '\0';
  297. xs *s_avatar = xs_dup(avatar);
  298. /* don't inline an empty avatar: create a real link */
  299. if (xs_startswith(s_avatar, "data:")) {
  300. xs_free(s_avatar);
  301. s_avatar = xs_fmt("%s/susie.png", srv_baseurl);
  302. }
  303. /* og properties */
  304. xs *es1 = encode_html(xs_dict_get(srv_config, "host"));
  305. xs *es2 = encode_html(xs_dict_get(snac->config, "name"));
  306. xs *es3 = encode_html(snac->uid);
  307. xs *es4 = encode_html(xs_dict_get(srv_config, "host"));
  308. xs *es5 = encode_html(s_bio);
  309. xs *es6 = encode_html(s_avatar);
  310. xs *s1 = xs_fmt(
  311. "<meta property=\"og:site_name\" content=\"%s\"/>\n"
  312. "<meta property=\"og:title\" content=\"%s (@%s@%s)\"/>\n"
  313. "<meta property=\"og:description\" content=\"%s\"/>\n"
  314. "<meta property=\"og:image\" content=\"%s\"/>\n"
  315. "<meta property=\"og:image:width\" content=\"300\"/>\n"
  316. "<meta property=\"og:image:height\" content=\"300\"/>\n",
  317. es1, es2, es3, es4, es5, es6);
  318. s = xs_str_cat(s, s1);
  319. }
  320. {
  321. xs *s1 = xs_fmt("<link rel=\"alternate\" type=\"application/rss+xml\" "
  322. "title=\"RSS\" href=\"%s.rss\" />\n", snac->actor); /* snac->actor is likely need to be URLEncoded. */
  323. s = xs_str_cat(s, s1);
  324. }
  325. s = xs_str_cat(s, "</head>\n<body>\n");
  326. /* top nav */
  327. s = xs_str_cat(s, "<nav class=\"snac-top-nav\">");
  328. {
  329. xs *s1;
  330. s1 = xs_fmt("<img src=\"%s\" class=\"snac-avatar\" alt=\"\"/>&nbsp;", avatar);
  331. s = xs_str_cat(s, s1);
  332. }
  333. {
  334. xs *s1;
  335. if (local)
  336. s1 = xs_fmt(
  337. "<a href=\"%s.rss\">%s</a> - "
  338. "<a href=\"%s/admin\" rel=\"nofollow\">%s</a></nav>\n",
  339. snac->actor, L("RSS"),
  340. snac->actor, L("private"));
  341. else {
  342. xs *n_list = notify_list(snac, 1);
  343. int n_len = xs_list_len(n_list);
  344. xs *n_str = NULL;
  345. /* show the number of new notifications, if there are any */
  346. if (n_len)
  347. n_str = xs_fmt("<sup style=\"background-color: red; "
  348. "color: white;\"> %d </sup> ", n_len);
  349. else
  350. n_str = xs_str_new("");
  351. s1 = xs_fmt(
  352. "<a href=\"%s\">%s</a> - "
  353. "<a href=\"%s/admin\">%s</a> - "
  354. "<a href=\"%s/notifications\">%s</a>%s - "
  355. "<a href=\"%s/people\">%s</a></nav>\n",
  356. snac->actor, L("public"),
  357. snac->actor, L("private"),
  358. snac->actor, L("notifications"), n_str,
  359. snac->actor, L("people"));
  360. }
  361. s = xs_str_cat(s, s1);
  362. }
  363. /* user info */
  364. {
  365. s = xs_str_cat(s, "<div class=\"h-card snac-top-user\">\n");
  366. if (local) {
  367. const char *header = xs_dict_get(snac->config, "header");
  368. if (header && *header) {
  369. xs *h = encode_html(header);
  370. xs *s1 = xs_fmt("<div class=\"snac-top-user-banner\" style=\"clear: both\">"
  371. "<br><img src=\"%s\"/></div>\n", h);
  372. s = xs_str_cat(s, s1);
  373. }
  374. }
  375. const char *_tmpl =
  376. "<p class=\"p-name snac-top-user-name\">%s</p>\n"
  377. "<p class=\"snac-top-user-id\">@%s@%s</p>\n";
  378. xs *es1 = encode_html(xs_dict_get(snac->config, "name"));
  379. xs *es2 = encode_html(xs_dict_get(snac->config, "uid"));
  380. xs *es3 = encode_html(xs_dict_get(srv_config, "host"));
  381. xs *s1 = xs_fmt(_tmpl, es1, es2, es3);
  382. s = xs_str_cat(s, s1);
  383. if (local) {
  384. xs *es1 = encode_html(xs_dict_get(snac->config, "bio"));
  385. xs *bio1 = not_really_markdown(es1, NULL);
  386. xs *tags = xs_list_new();
  387. xs *bio2 = process_tags(snac, bio1, &tags);
  388. xs *s1 = xs_fmt("<div class=\"p-note snac-top-user-bio\">%s</div>\n", bio2);
  389. s = xs_str_cat(s, s1);
  390. xs_dict *metadata = xs_dict_get(snac->config, "metadata");
  391. if (xs_type(metadata) == XSTYPE_DICT) {
  392. xs_str *k;
  393. xs_str *v;
  394. s = xs_str_cat(s, "<br><div class=\"snac-metadata\">\n");
  395. while (xs_dict_iter(&metadata, &k, &v)) {
  396. xs *k2 = encode_html(k);
  397. xs *v2 = encode_html(v);
  398. xs *v3 = NULL;
  399. if (xs_startswith(v, "https:/" "/"))
  400. v3 = xs_fmt("<a href=\"%s\">%s</a>", v2, v2);
  401. else
  402. v3 = xs_dup(v2);
  403. xs *s1 = xs_fmt("<span class=\"snac-property-name\">%s</span>:<br>"
  404. "<span class=\"snac-property-value\">%s</span><br>\n", k2, v3);
  405. s = xs_str_cat(s, s1);
  406. }
  407. s = xs_str_cat(s, "</div>\n");
  408. }
  409. }
  410. s = xs_str_cat(s, "</div>\n");
  411. }
  412. return s;
  413. }
  414. xs_str *html_top_controls(snac *snac, xs_str *s)
  415. /* generates the top controls */
  416. {
  417. char *_tmpl =
  418. "<div class=\"snac-top-controls\">\n"
  419. "<div class=\"snac-note\">\n"
  420. "<details><summary>%s</summary>\n"
  421. "<form autocomplete=\"off\" method=\"post\" "
  422. "action=\"%s/admin/note\" enctype=\"multipart/form-data\">\n"
  423. "<textarea class=\"snac-textarea\" name=\"content\" "
  424. "rows=\"8\" wrap=\"virtual\" required=\"required\" placeholder=\"What's on your mind?\"></textarea>\n"
  425. "<input type=\"hidden\" name=\"in_reply_to\" value=\"\">\n"
  426. "<p>%s: <input type=\"checkbox\" name=\"sensitive\"> "
  427. "<input type=\"text\" name=\"summary\" placeholder=\"%s\">\n"
  428. "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n"
  429. "<details><summary>%s</summary>\n" /** attach **/
  430. "<p>%s: <input type=\"file\" name=\"attach\">\n"
  431. "<p>%s: <input type=\"text\" name=\"alt_text\" placeholder=\"[Optional]\">\n"
  432. "</details>\n"
  433. "<p>"
  434. "<details><summary>%s</summary>\n" /** poll **/
  435. "<p>%s:<br>\n"
  436. "<textarea class=\"snac-textarea\" name=\"poll_options\" "
  437. "rows=\"6\" wrap=\"virtual\" placeholder=\"Option 1...\nOption 2...\nOption 3...\n...\"></textarea>\n"
  438. "<p><select name=\"poll_multiple\">\n"
  439. "<option value=\"off\">%s</option>\n"
  440. "<option value=\"on\">%s</option>\n"
  441. "</select>\n"
  442. "<select name=\"poll_end_secs\" id=\"poll_end_secs\">\n"
  443. "<option value=\"300\">%s</option>\n"
  444. "<option value=\"3600\" selected>%s</option>\n"
  445. "<option value=\"86400\">%s</option>\n"
  446. "</select>\n"
  447. "</details>\n"
  448. "<p><input type=\"submit\" class=\"button\" value=\"%s\">\n"
  449. "</form><p>\n"
  450. "</details>\n"
  451. "</div>\n"
  452. "<div class=\"snac-top-controls-more\">\n"
  453. "<details><summary>%s</summary>\n"
  454. "<form autocomplete=\"off\" method=\"post\" action=\"%s/admin/action\">\n" /** follow **/
  455. "<input type=\"text\" name=\"actor\" required=\"required\" placeholder=\"bob@example.com\">\n"
  456. "<input type=\"submit\" name=\"action\" value=\"%s\"> %s\n"
  457. "</form><p>\n"
  458. "<form autocomplete=\"off\" method=\"post\" action=\"%s/admin/action\">\n" /** boost **/
  459. "<input type=\"text\" name=\"id\" required=\"required\" placeholder=\"https://fedi.example.com/bob/...\">\n"
  460. "<input type=\"submit\" name=\"action\" value=\"%s\"> %s\n"
  461. "</form><p>\n"
  462. "</details>\n"
  463. "<details><summary>%s</summary>\n"
  464. "<div class=\"snac-user-setup\">\n" /** user setup **/
  465. "<form autocomplete=\"off\" method=\"post\" "
  466. "action=\"%s/admin/user-setup\" enctype=\"multipart/form-data\">\n"
  467. "<p>%s:<br>\n"
  468. "<input type=\"text\" name=\"name\" value=\"%s\" placeholder=\"Your name.\"></p>\n"
  469. "<p>%s: <input type=\"file\" name=\"avatar_file\"></p>\n"
  470. "<p>%s: <input type=\"file\" name=\"header_file\"></p>\n"
  471. "<p>%s:<br>\n"
  472. "<textarea name=\"bio\" cols=\"40\" rows=\"4\" placeholder=\"Write about yourself here...\">%s</textarea></p>\n"
  473. "<p><input type=\"checkbox\" name=\"cw\" id=\"cw\" %s>\n"
  474. "<label for=\"cw\">%s</label></p>\n"
  475. "<p>%s:<br>\n"
  476. "<input type=\"text\" name=\"email\" value=\"%s\" placeholder=\"bob@example.com\"></p>\n"
  477. "<p>%s:<br>\n"
  478. "<input type=\"text\" name=\"telegram_bot\" placeholder=\"Bot API key\" value=\"%s\"> "
  479. "<input type=\"text\" name=\"telegram_chat_id\" placeholder=\"Chat id\" value=\"%s\"></p>\n"
  480. "<p>%s:<br>\n"
  481. "<input type=\"number\" name=\"purge_days\" value=\"%s\"></p>\n"
  482. "<p><input type=\"checkbox\" name=\"drop_dm_from_unknown\" id=\"drop_dm_from_unknown\" %s>\n"
  483. "<label for=\"drop_dm_from_unknown\">%s</label></p>\n"
  484. "<p><input type=\"checkbox\" name=\"bot\" id=\"bot\" %s>\n"
  485. "<label for=\"bot\">%s</label></p>\n"
  486. "<p><input type=\"checkbox\" name=\"private\" id=\"private\" %s>\n"
  487. "<label for=\"private\">%s</label></p>\n"
  488. "<p>%s:<br>\n"
  489. "<textarea name=\"metadata\" cols=\"40\" rows=\"4\" "
  490. "placeholder=\"Blog=https:/" "/example.com/my-blog\nGPG Key=1FA54\n...\">"
  491. "%s</textarea></p>\n"
  492. "<p>%s:<br>\n"
  493. "<input type=\"password\" name=\"passwd1\" value=\"\"></p>\n"
  494. "<p>%s:<br>\n"
  495. "<input type=\"password\" name=\"passwd2\" value=\"\"></p>\n"
  496. "<input type=\"submit\" class=\"button\" value=\"%s\">\n"
  497. "</form>\n"
  498. "</div>\n"
  499. "</details>\n"
  500. "</div>\n"
  501. "</div>\n";
  502. const char *email = "[disabled by admin]";
  503. if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) {
  504. email = xs_dict_get(snac->config_o, "email");
  505. if (xs_is_null(email)) {
  506. email = xs_dict_get(snac->config, "email");
  507. if (xs_is_null(email))
  508. email = "";
  509. }
  510. }
  511. char *cw = xs_dict_get(snac->config, "cw");
  512. if (xs_is_null(cw))
  513. cw = "";
  514. char *telegram_bot = xs_dict_get(snac->config, "telegram_bot");
  515. if (xs_is_null(telegram_bot))
  516. telegram_bot = "";
  517. char *telegram_chat_id = xs_dict_get(snac->config, "telegram_chat_id");
  518. if (xs_is_null(telegram_chat_id))
  519. telegram_chat_id = "";
  520. const char *purge_days = xs_dict_get(snac->config, "purge_days");
  521. if (!xs_is_null(purge_days) && xs_type(purge_days) == XSTYPE_NUMBER)
  522. purge_days = xs_number_str(purge_days);
  523. else
  524. purge_days = "0";
  525. const char *d_dm_f_u = xs_dict_get(snac->config, "drop_dm_from_unknown");
  526. const char *bot = xs_dict_get(snac->config, "bot");
  527. const char *a_private = xs_dict_get(snac->config, "private");
  528. xs *es1 = encode_html(xs_dict_get(snac->config, "name"));
  529. xs *es2 = encode_html(xs_dict_get(snac->config, "bio"));
  530. xs *es3 = encode_html(email);
  531. xs *es4 = encode_html(telegram_bot);
  532. xs *es5 = encode_html(telegram_chat_id);
  533. xs *es6 = encode_html(purge_days);
  534. xs *metadata = xs_str_new(NULL);
  535. xs_dict *md = xs_dict_get(snac->config, "metadata");
  536. xs_str *k;
  537. xs_str *v;
  538. while (xs_dict_iter(&md, &k, &v)) {
  539. xs *kp = xs_fmt("%s=%s", k, v);
  540. if (*metadata)
  541. metadata = xs_str_cat(metadata, "\n");
  542. metadata = xs_str_cat(metadata, kp);
  543. }
  544. xs *s1 = xs_fmt(_tmpl,
  545. L("New Post..."),
  546. snac->actor,
  547. L("Sensitive content"),
  548. L("Sensitive content description"),
  549. L("Only for mentioned people"),
  550. L("Attachment..."),
  551. L("File"),
  552. L("File description"),
  553. L("Poll..."),
  554. L("Poll options (one per line, up to 8)"),
  555. L("One choice"),
  556. L("Multiple choices"),
  557. L("End in 5 minutes"),
  558. L("End in 1 hour"),
  559. L("End in 1 day"),
  560. L("Post"),
  561. L("Operations..."),
  562. snac->actor,
  563. L("Follow"), L("(by URL or user@host)"),
  564. snac->actor,
  565. L("Boost"), L("(by URL)"),
  566. L("User Settings..."),
  567. snac->actor,
  568. L("Display name"),
  569. es1,
  570. L("Avatar"),
  571. L("Header image (banner)"),
  572. L("Bio"),
  573. es2,
  574. strcmp(cw, "open") == 0 ? "checked" : "",
  575. L("Always show sensitive content"),
  576. L("Email address for notifications"),
  577. es3,
  578. L("Telegram notifications (bot key and chat id)"),
  579. es4,
  580. es5,
  581. L("Maximum days to keep posts (0: server settings)"),
  582. es6,
  583. xs_type(d_dm_f_u) == XSTYPE_TRUE ? "checked" : "",
  584. L("Drop direct messages from people you don't follow"),
  585. xs_type(bot) == XSTYPE_TRUE ? "checked" : "",
  586. L("This account is a bot"),
  587. xs_type(a_private) == XSTYPE_TRUE ? "checked" : "",
  588. L("This account is private (posts are not shown through the web)"),
  589. L("Profile metadata (key=value pairs in each line)"),
  590. metadata,
  591. L("New password"),
  592. L("Repeat new password"),
  593. L("Update user info")
  594. );
  595. s = xs_str_cat(s, s1);
  596. return s;
  597. }
  598. static xs_str *html_button(xs_str *s, const char *clss, const char *label, const char *hint)
  599. {
  600. xs *s1 = xs_fmt(
  601. "<input type=\"submit\" name=\"action\" "
  602. "class=\"snac-btn-%s\" value=\"%s\" title=\"%s\">\n",
  603. clss, label, hint);
  604. return xs_str_cat(s, s1);
  605. }
  606. static xs_html *html_button_2(char *clss, char *label, char *hint)
  607. {
  608. xs *c = xs_fmt("snac-btn-%s", clss);
  609. return xs_html_sctag("input",
  610. xs_html_attr("type", "submit"),
  611. xs_html_attr("name", "action"),
  612. xs_html_attr("class", c),
  613. xs_html_attr("value", label),
  614. xs_html_attr("title", hint));
  615. }
  616. xs_str *build_mentions(snac *snac, const xs_dict *msg)
  617. /* returns a string with the mentions in msg */
  618. {
  619. xs_str *s = xs_str_new(NULL);
  620. char *list = xs_dict_get(msg, "tag");
  621. char *v;
  622. while (xs_list_iter(&list, &v)) {
  623. char *type = xs_dict_get(v, "type");
  624. char *href = xs_dict_get(v, "href");
  625. char *name = xs_dict_get(v, "name");
  626. if (type && strcmp(type, "Mention") == 0 &&
  627. href && strcmp(href, snac->actor) != 0 && name) {
  628. xs *s1 = NULL;
  629. if (name[0] != '@') {
  630. s1 = xs_fmt("@%s", name);
  631. name = s1;
  632. }
  633. xs *l = xs_split(name, "@");
  634. /* is it a name without a host? */
  635. if (xs_list_len(l) < 3) {
  636. /* split the href and pick the host name LIKE AN ANIMAL */
  637. /* would be better to query the webfinger but *won't do that* here */
  638. xs *l2 = xs_split(href, "/");
  639. if (xs_list_len(l2) >= 3) {
  640. xs *s1 = xs_fmt("%s@%s ", name, xs_list_get(l2, 2));
  641. s = xs_str_cat(s, s1);
  642. }
  643. }
  644. else {
  645. s = xs_str_cat(s, name);
  646. s = xs_str_cat(s, " ");
  647. }
  648. }
  649. }
  650. if (*s) {
  651. xs *s1 = s;
  652. s = xs_fmt("\n\n\nCC: %s", s1);
  653. }
  654. return s;
  655. }
  656. xs_str *html_entry_controls(snac *snac, xs_str *os, const xs_dict *msg, const char *md5)
  657. {
  658. const char *id = xs_dict_get(msg, "id");
  659. const char *actor = xs_dict_get(msg, "attributedTo");
  660. const char *group = xs_dict_get(msg, "audience");
  661. xs *likes = object_likes(id);
  662. xs *boosts = object_announces(id);
  663. xs *s = xs_str_new(NULL);
  664. s = xs_str_cat(s, "<div class=\"snac-controls\">\n");
  665. {
  666. xs *s1 = xs_fmt(
  667. "<form autocomplete=\"off\" method=\"post\" action=\"%s/admin/action\">\n"
  668. "<input type=\"hidden\" name=\"id\" value=\"%s\">\n"
  669. "<input type=\"hidden\" name=\"actor\" value=\"%s\">\n"
  670. "<input type=\"hidden\" name=\"group\" value=\"%s\">\n"
  671. "<input type=\"hidden\" name=\"redir\" value=\"%s_entry\">\n"
  672. "\n",
  673. snac->actor, id, actor, group ? group : "", md5
  674. );
  675. s = xs_str_cat(s, s1);
  676. }
  677. if (!xs_startswith(id, snac->actor)) {
  678. if (xs_list_in(likes, snac->md5) == -1) {
  679. /* not already liked; add button */
  680. s = html_button(s, "like", L("Like"), L("Say you like this post"));
  681. }
  682. }
  683. else {
  684. if (is_pinned(snac, id))
  685. s = html_button(s, "unpin", L("Unpin"), L("Unpin this post from your timeline"));
  686. else
  687. s = html_button(s, "pin", L("Pin"), L("Pin this post to the top of your timeline"));
  688. }
  689. if (is_msg_public(msg)) {
  690. if (strcmp(actor, snac->actor) == 0 || xs_list_in(boosts, snac->md5) == -1) {
  691. /* not already boosted or us; add button */
  692. s = html_button(s, "boost", L("Boost"), L("Announce this post to your followers"));
  693. }
  694. }
  695. if (strcmp(actor, snac->actor) != 0) {
  696. /* controls for other actors than this one */
  697. if (following_check(snac, actor)) {
  698. s = html_button(s, "unfollow", L("Unfollow"), L("Stop following this user's activity"));
  699. }
  700. else {
  701. s = html_button(s, "follow", L("Follow"), L("Start following this user's activity"));
  702. }
  703. if (!xs_is_null(group)) {
  704. if (following_check(snac, group)) {
  705. s = html_button(s, "unfollow", L("Unfollow Group"),
  706. L("Stop following this group or channel"));
  707. }
  708. else {
  709. s = html_button(s, "follow", L("Follow Group"),
  710. L("Start following this group or channel"));
  711. }
  712. }
  713. s = html_button(s, "mute", L("MUTE"),
  714. L("Block any activity from this user forever"));
  715. }
  716. s = html_button(s, "delete", L("Delete"), L("Delete this post"));
  717. s = html_button(s, "hide", L("Hide"), L("Hide this post and its children"));
  718. s = xs_str_cat(s, "</form>\n");
  719. const char *prev_src1 = xs_dict_get(msg, "sourceContent");
  720. if (!xs_is_null(prev_src1) && strcmp(actor, snac->actor) == 0) { /** edit **/
  721. xs *prev_src = encode_html(prev_src1);
  722. const xs_val *sensitive = xs_dict_get(msg, "sensitive");
  723. const char *summary = xs_dict_get(msg, "summary");
  724. /* post can be edited */
  725. xs *s1 = xs_fmt(
  726. "<p><details><summary>%s</summary>\n"
  727. "<p><div class=\"snac-note\" id=\"%s_edit\">\n"
  728. "<form autocomplete=\"off\" method=\"post\" action=\"%s/admin/note\" "
  729. "enctype=\"multipart/form-data\" id=\"%s_edit_form\">\n"
  730. "<textarea class=\"snac-textarea\" name=\"content\" "
  731. "rows=\"4\" wrap=\"virtual\" required=\"required\">%s</textarea>\n"
  732. "<input type=\"hidden\" name=\"edit_id\" value=\"%s\">\n"
  733. "<p>%s: <input type=\"checkbox\" name=\"sensitive\" %s> "
  734. "<input type=\"text\" name=\"summary\" placeholder=\"%s\" value=\"%s\">\n"
  735. "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n"
  736. "<details><summary>%s</summary>\n"
  737. "<p>%s: <input type=\"file\" name=\"attach\">\n"
  738. "<p>%s: <input type=\"text\" name=\"alt_text\">\n"
  739. "</details>\n"
  740. "<input type=\"hidden\" name=\"redir\" value=\"%s_entry\">\n"
  741. "<p><input type=\"submit\" class=\"button\" value=\"%s\">\n"
  742. "</form><p></div>\n"
  743. "</details><p>"
  744. "\n",
  745. L("Edit..."),
  746. md5,
  747. snac->actor, md5,
  748. prev_src,
  749. id,
  750. L("Sensitive content"),
  751. xs_type(sensitive) == XSTYPE_TRUE ? "checked" : "",
  752. L("Sensitive content description"),
  753. xs_is_null(summary) ? "" : summary,
  754. L("Only for mentioned people"),
  755. L("Attach..."),
  756. L("File"),
  757. L("File description"),
  758. md5,
  759. L("Post")
  760. );
  761. s = xs_str_cat(s, s1);
  762. }
  763. { /** reply **/
  764. /* the post textarea */
  765. xs *ct = build_mentions(snac, msg);
  766. const xs_val *sensitive = xs_dict_get(msg, "sensitive");
  767. const char *summary = xs_dict_get(msg, "summary");
  768. xs *s1 = xs_fmt(
  769. "<p><details><summary>%s</summary>\n"
  770. "<p><div class=\"snac-note\" id=\"%s_reply\">\n"
  771. "<form autocomplete=\"off\" method=\"post\" action=\"%s/admin/note\" "
  772. "enctype=\"multipart/form-data\" id=\"%s_reply_form\">\n"
  773. "<textarea class=\"snac-textarea\" name=\"content\" "
  774. "rows=\"4\" wrap=\"virtual\" required=\"required\">%s</textarea>\n"
  775. "<input type=\"hidden\" name=\"in_reply_to\" value=\"%s\">\n"
  776. "<p>%s: <input type=\"checkbox\" name=\"sensitive\" %s> "
  777. "<input type=\"text\" name=\"summary\" placeholder=\"%s\" value=\"%s\">\n"
  778. "<p>%s: <input type=\"checkbox\" name=\"mentioned_only\">\n"
  779. "<details><summary>%s</summary>\n"
  780. "<p>%s: <input type=\"file\" name=\"attach\">\n"
  781. "<p>%s: <input type=\"text\" name=\"alt_text\">\n"
  782. "</details>\n"
  783. "<input type=\"hidden\" name=\"redir\" value=\"%s_entry\">\n"
  784. "<p><input type=\"submit\" class=\"button\" value=\"%s\">\n"
  785. "</form><p></div>\n"
  786. "</details><p>"
  787. "\n",
  788. L("Reply..."),
  789. md5,
  790. snac->actor, md5,
  791. ct,
  792. id,
  793. L("Sensitive content"),
  794. xs_type(sensitive) == XSTYPE_TRUE ? "checked" : "",
  795. L("Sensitive content description"),
  796. xs_is_null(summary) ? "" : summary,
  797. L("Only for mentioned people"),
  798. L("Attach..."),
  799. L("File"),
  800. L("File description"),
  801. md5,
  802. L("Post")
  803. );
  804. s = xs_str_cat(s, s1);
  805. }
  806. s = xs_str_cat(s, "</div>\n");
  807. return xs_str_cat(os, s);
  808. }
  809. xs_str *html_entry(snac *user, xs_str *os, const xs_dict *msg, int local,
  810. int level, const char *md5, int hide_children)
  811. {
  812. char *id = xs_dict_get(msg, "id");
  813. char *type = xs_dict_get(msg, "type");
  814. char *actor;
  815. int sensitive = 0;
  816. char *v;
  817. xs *boosts = NULL;
  818. /* do not show non-public messages in the public timeline */
  819. if ((local || !user) && !is_msg_public(msg))
  820. return os;
  821. /* hidden? do nothing more for this conversation */
  822. if (user && is_hidden(user, id))
  823. return os;
  824. /* avoid too deep nesting, as it may be a loop */
  825. if (level >= 256)
  826. return os;
  827. xs *s = xs_str_new("<div>\n");
  828. {
  829. xs *s1 = xs_fmt("<a name=\"%s_entry\"></a>\n", md5);
  830. s = xs_str_cat(s, s1);
  831. }
  832. if (strcmp(type, "Follow") == 0) {
  833. s = xs_str_cat(s, "<div class=\"snac-post\">\n<div class=\"snac-post-header\">\n");
  834. xs *s1 = xs_fmt("<div class=\"snac-origin\">%s</div>\n", L("follows you"));
  835. s = xs_str_cat(s, s1);
  836. s = html_msg_icon(s, msg);
  837. s = xs_str_cat(s, "</div>\n</div>\n");
  838. return xs_str_cat(os, s);
  839. }
  840. else
  841. if (!xs_match(type, "Note|Question|Page|Article")) {
  842. /* skip oddities */
  843. return os;
  844. }
  845. /* ignore notes with "name", as they are votes to Questions */
  846. if (strcmp(type, "Note") == 0 && !xs_is_null(xs_dict_get(msg, "name")))
  847. return os;
  848. /* bring the main actor */
  849. if ((actor = xs_dict_get(msg, "attributedTo")) == NULL)
  850. return os;
  851. /* ignore muted morons immediately */
  852. if (user && is_muted(user, actor))
  853. return os;
  854. if ((user == NULL || strcmp(actor, user->actor) != 0)
  855. && !valid_status(actor_get(actor, NULL)))
  856. return os;
  857. if (level == 0)
  858. s = xs_str_cat(s, "<div class=\"snac-post\">\n"); /** **/
  859. else
  860. s = xs_str_cat(s, "<div class=\"snac-child\">\n"); /** **/
  861. s = xs_str_cat(s, "<div class=\"snac-post-header\">\n<div class=\"snac-score\">"); /** **/
  862. if (user && is_pinned(user, id)) {
  863. /* add a pin emoji */
  864. xs *f = xs_fmt("<span title=\"%s\"> &#128204; </span>", L("Pinned"));
  865. s = xs_str_cat(s, f);
  866. }
  867. if (strcmp(type, "Question") == 0) {
  868. /* add the ballot box emoji */
  869. xs *f = xs_fmt("<span title=\"%s\"> &#128499; </span>", L("Poll"));
  870. s = xs_str_cat(s, f);
  871. if (user && was_question_voted(user, id)) {
  872. /* add a check to show this poll was voted */
  873. xs *f2 = xs_fmt("<span title=\"%s\"> &#10003; </span>", L("Voted"));
  874. s = xs_str_cat(s, f2);
  875. }
  876. }
  877. /* if it's a user from this same instance, add the score */
  878. if (xs_startswith(id, srv_baseurl)) {
  879. int n_likes = object_likes_len(id);
  880. int n_boosts = object_announces_len(id);
  881. /* alternate emojis: %d &#128077; %d &#128257; */
  882. xs *s1 = xs_fmt("%d &#9733; %d &#8634;\n", n_likes, n_boosts);
  883. s = xs_str_cat(s, s1);
  884. }
  885. s = xs_str_cat(s, "</div>\n");
  886. if (boosts == NULL)
  887. boosts = object_announces(id);
  888. if (xs_list_len(boosts)) {
  889. /* if somebody boosted this, show as origin */
  890. char *p = xs_list_get(boosts, -1);
  891. xs *actor_r = NULL;
  892. if (user && xs_list_in(boosts, user->md5) != -1) {
  893. /* we boosted this */
  894. xs *es1 = encode_html(xs_dict_get(user->config, "name"));
  895. xs *s1 = xs_fmt(
  896. "<div class=\"snac-origin\">"
  897. "<a href=\"%s\">%s</a> %s</div>",
  898. user->actor, es1, L("boosted")
  899. );
  900. s = xs_str_cat(s, s1);
  901. }
  902. else
  903. if (valid_status(object_get_by_md5(p, &actor_r))) {
  904. xs *name = actor_name(actor_r);
  905. if (!xs_is_null(name)) {
  906. xs *s1 = xs_fmt(
  907. "<div class=\"snac-origin\">"
  908. "<a href=\"%s\">%s</a> %s</div>\n",
  909. xs_dict_get(actor_r, "id"),
  910. name,
  911. L("boosted")
  912. );
  913. s = xs_str_cat(s, s1);
  914. }
  915. }
  916. }
  917. else
  918. if (strcmp(type, "Note") == 0) {
  919. if (level == 0) {
  920. /* is the parent not here? */
  921. char *parent = xs_dict_get(msg, "inReplyTo");
  922. if (user && !xs_is_null(parent) && *parent && !timeline_here(user, parent)) {
  923. xs *s1 = xs_fmt(
  924. "<div class=\"snac-origin\">%s "
  925. "<a href=\"%s\">»</a></div>\n",
  926. L("in reply to"), parent
  927. );
  928. s = xs_str_cat(s, s1);
  929. }
  930. }
  931. }
  932. s = html_msg_icon(s, msg);
  933. /* add the content */
  934. s = xs_str_cat(s, "</div>\n<div class=\"e-content snac-content\">\n"); /** **/
  935. if (!xs_is_null(v = xs_dict_get(msg, "name"))) {
  936. xs *es1 = encode_html(v);
  937. xs *s1 = xs_fmt("<h3 class=\"snac-entry-title\">%s</h3>\n", es1);
  938. s = xs_str_cat(s, s1);
  939. }
  940. /* is it sensitive? */
  941. if (user && xs_type(xs_dict_get(msg, "sensitive")) == XSTYPE_TRUE) {
  942. if (xs_is_null(v = xs_dict_get(msg, "summary")) || *v == '\0')
  943. v = "...";
  944. /* only show it when not in the public timeline and the config setting is "open" */
  945. char *cw = xs_dict_get(user->config, "cw");
  946. if (xs_is_null(cw) || local)
  947. cw = "";
  948. xs *es1 = encode_html(v);
  949. xs *s1 = xs_fmt("<details %s><summary>%s [%s]</summary>\n", cw, es1, L("SENSITIVE CONTENT"));
  950. s = xs_str_cat(s, s1);
  951. sensitive = 1;
  952. }
  953. #if 0
  954. {
  955. xs *md5 = xs_md5_hex(id, strlen(id));
  956. xs *s1 = xs_fmt("<p><code>%s</code></p>\n", md5);
  957. s = xs_str_cat(s, s1);
  958. }
  959. #endif
  960. {
  961. const char *content = xs_dict_get(msg, "content");
  962. xs *c = sanitize(xs_is_null(content) ? "" : content);
  963. char *p, *v;
  964. /* do some tweaks to the content */
  965. c = xs_replace_i(c, "\r", "");
  966. while (xs_endswith(c, "<br><br>"))
  967. c = xs_crop_i(c, 0, -4);
  968. c = xs_replace_i(c, "<br><br>", "<p>");
  969. if (!xs_startswith(c, "<p>")) {
  970. xs *s1 = c;
  971. c = xs_fmt("<p>%s</p>", s1);
  972. }
  973. /* replace the :shortnames: */
  974. if (!xs_is_null(p = xs_dict_get(msg, "tag"))) {
  975. xs *tag = NULL;
  976. if (xs_type(p) == XSTYPE_DICT) {
  977. /* not a list */
  978. tag = xs_list_new();
  979. tag = xs_list_append(tag, p);
  980. }
  981. else
  982. if (xs_type(p) == XSTYPE_LIST)
  983. tag = xs_dup(p);
  984. else
  985. tag = xs_list_new();
  986. xs_list *tags = tag;
  987. /* iterate the tags */
  988. while (xs_list_iter(&tags, &v)) {
  989. char *t = xs_dict_get(v, "type");
  990. if (t && strcmp(t, "Emoji") == 0) {
  991. char *n = xs_dict_get(v, "name");
  992. char *i = xs_dict_get(v, "icon");
  993. if (n && i) {
  994. char *u = xs_dict_get(i, "url");
  995. xs *img = xs_fmt("<img loading=\"lazy\" src=\"%s\" "
  996. "style=\"height: 2em; vertical-align: middle;\" "
  997. "title=\"%s\"/>", u, n);
  998. c = xs_replace_i(c, n, img);
  999. }
  1000. }
  1001. }
  1002. }
  1003. if (strcmp(type, "Question") == 0) { /** question content **/
  1004. xs_list *oo = xs_dict_get(msg, "oneOf");
  1005. xs_list *ao = xs_dict_get(msg, "anyOf");
  1006. xs_list *p;
  1007. xs_dict *v;
  1008. int closed = 0;
  1009. if (xs_dict_get(msg, "closed"))
  1010. closed = 2;
  1011. else
  1012. if (user && xs_startswith(id, user->actor))
  1013. closed = 1; /* we questioned; closed for us */
  1014. else
  1015. if (user && was_question_voted(user, id))
  1016. closed = 1; /* we already voted; closed for us */
  1017. /* get the appropriate list of options */
  1018. p = oo != NULL ? oo : ao;
  1019. if (closed || user == NULL) {
  1020. /* closed poll */
  1021. c = xs_str_cat(c, "<table class=\"snac-poll-result\">\n");
  1022. while (xs_list_iter(&p, &v)) {
  1023. const char *name = xs_dict_get(v, "name");
  1024. const xs_dict *replies = xs_dict_get(v, "replies");
  1025. if (name && replies) {
  1026. int nr = xs_number_get(xs_dict_get(replies, "totalItems"));
  1027. xs *es1 = encode_html(name);
  1028. xs *l = xs_fmt("<tr><td>%s:</td><td>%d</td></tr>\n", es1, nr);
  1029. c = xs_str_cat(c, l);
  1030. }
  1031. }
  1032. c = xs_str_cat(c, "</table>\n");
  1033. }
  1034. else {
  1035. /* poll still active */
  1036. xs *s1 = xs_fmt("<div class=\"snac-poll-form\">\n"
  1037. "<form autocomplete=\"off\" "
  1038. "method=\"post\" action=\"%s/admin/vote\">\n"
  1039. "<input type=\"hidden\" name=\"actor\" value= \"%s\">\n"
  1040. "<input type=\"hidden\" name=\"irt\" value=\"%s\">\n",
  1041. user->actor, actor, id);
  1042. while (xs_list_iter(&p, &v)) {
  1043. const char *name = xs_dict_get(v, "name");
  1044. const xs_dict *replies = xs_dict_get(v, "replies");
  1045. if (name) {
  1046. int nr = xs_number_get(xs_dict_get(replies, "totalItems"));
  1047. xs *es1 = encode_html(name);
  1048. xs *opt = xs_fmt("<input type=\"%s\""
  1049. " id=\"%s\" value=\"%s\""
  1050. " name=\"question\"> <span title=\"%d\">%s</span><br>\n",
  1051. !xs_is_null(oo) ? "radio" : "checkbox",
  1052. es1, es1, nr, es1);
  1053. s1 = xs_str_cat(s1, opt);
  1054. }
  1055. }
  1056. xs *s2 = xs_fmt("<p><input type=\"submit\" "
  1057. "class=\"button\" value=\"%s\">\n</form>\n</div>\n\n", L("Vote"));
  1058. s1 = xs_str_cat(s1, s2);
  1059. c = xs_str_cat(c, s1);
  1060. }
  1061. /* if it's *really* closed, say it */
  1062. if (closed == 2) {
  1063. xs *s1 = xs_fmt("<p>%s</p>\n", L("Closed"));
  1064. c = xs_str_cat(c, s1);
  1065. }
  1066. else {
  1067. /* show when the poll closes */
  1068. const char *end_time = xs_dict_get(msg, "endTime");
  1069. if (!xs_is_null(end_time)) {
  1070. time_t t0 = time(NULL);
  1071. time_t t1 = xs_parse_iso_date(end_time, 0);
  1072. if (t1 > 0 && t1 > t0) {
  1073. time_t diff_time = t1 - t0;
  1074. xs *tf = xs_str_time_diff(diff_time);
  1075. char *p = tf;
  1076. /* skip leading zeros */
  1077. for (; *p == '0' || *p == ':'; p++);
  1078. xs *es1 = encode_html(p);
  1079. xs *s1 = xs_fmt("<p>%s %s</p>", L("Closes in"), es1);
  1080. c = xs_str_cat(c, s1);
  1081. }
  1082. }
  1083. }
  1084. }
  1085. s = xs_str_cat(s, c);
  1086. }
  1087. s = xs_str_cat(s, "\n");
  1088. /* add the attachments */
  1089. v = xs_dict_get(msg, "attachment");
  1090. if (!xs_is_null(v)) { /** attachments **/
  1091. xs *attach = NULL;
  1092. /* ensure it's a list */
  1093. if (xs_type(v) == XSTYPE_DICT) {
  1094. attach = xs_list_new();
  1095. attach = xs_list_append(attach, v);
  1096. }
  1097. else
  1098. if (xs_type(v) == XSTYPE_LIST)
  1099. attach = xs_dup(v);
  1100. else
  1101. attach = xs_list_new();
  1102. /* does the message have an image? */
  1103. if (xs_type(v = xs_dict_get(msg, "image")) == XSTYPE_DICT) {
  1104. /* add it to the attachment list */
  1105. attach = xs_list_append(attach, v);
  1106. }
  1107. /* make custom css for attachments easier */
  1108. s = xs_str_cat(s, "<div class=\"snac-content-attachments\">\n");
  1109. xs_list *p = attach;
  1110. while (xs_list_iter(&p, &v)) {
  1111. const char *t = xs_dict_get(v, "mediaType");
  1112. if (xs_is_null(t))
  1113. t = xs_dict_get(v, "type");
  1114. if (xs_is_null(t))
  1115. continue;
  1116. const char *url = xs_dict_get(v, "url");
  1117. if (xs_is_null(url))
  1118. url = xs_dict_get(v, "href");
  1119. if (xs_is_null(url))
  1120. continue;
  1121. /* infer MIME type from non-specific attachments */
  1122. if (xs_list_len(attach) < 2 && xs_match(t, "Link|Document")) {
  1123. const char *mt = xs_mime_by_ext(url);
  1124. if (xs_match(mt, "image/*|audio/*|video/*")) /* */
  1125. t = mt;
  1126. }
  1127. const char *name = xs_dict_get(v, "name");
  1128. if (xs_is_null(name))
  1129. name = xs_dict_get(msg, "name");
  1130. if (xs_is_null(name))
  1131. name = L("No description");
  1132. xs *es1 = encode_html(name);
  1133. xs *s1 = NULL;
  1134. if (xs_startswith(t, "image/") || strcmp(t, "Image") == 0) {
  1135. s1 = xs_fmt(
  1136. "<a href=\"%s\" target=\"_blank\">"
  1137. "<img loading=\"lazy\" src=\"%s\" alt=\"%s\" title=\"%s\"/></a>\n",
  1138. url, url, es1, es1);
  1139. }
  1140. else
  1141. if (xs_startswith(t, "video/")) {
  1142. s1 = xs_fmt("<video style=\"width: 100%\" class=\"snac-embedded-video\" "
  1143. "controls src=\"%s\">Video: "
  1144. "<a href=\"%s\">%s</a></video>\n", url, url, es1);
  1145. }
  1146. else
  1147. if (xs_startswith(t, "audio/")) {
  1148. s1 = xs_fmt("<audio style=\"width: 100%\" class=\"snac-embedded-audio\" "
  1149. "controls src=\"%s\">Audio: "
  1150. "<a href=\"%s\">%s</a></audio>\n", url, url, es1);
  1151. }
  1152. else
  1153. if (strcmp(t, "Link") == 0) {
  1154. xs *es2 = encode_html(url);
  1155. s1 = xs_fmt("<p><a href=\"%s\">%s</a></p>\n", url, es2);
  1156. }
  1157. else {
  1158. s1 = xs_fmt("<p><a href=\"%s\">Attachment: %s</a></p>\n", url, es1);
  1159. }
  1160. if (!xs_is_null(s1))
  1161. s = xs_str_cat(s, s1);
  1162. }
  1163. s = xs_str_cat(s, "</div>\n");
  1164. }
  1165. /* has this message an audience (i.e., comes from a channel or community)? */
  1166. const char *audience = xs_dict_get(msg, "audience");
  1167. if (strcmp(type, "Page") == 0 && !xs_is_null(audience)) {
  1168. xs *es1 = encode_html(audience);
  1169. xs *s1 = xs_fmt("<p>(<a href=\"%s\" title=\"%s\">%s</a>)</p>\n",
  1170. audience, L("Source channel or community"), es1);
  1171. s = xs_str_cat(s, s1);
  1172. }
  1173. if (sensitive)
  1174. s = xs_str_cat(s, "</details><p>\n");
  1175. s = xs_str_cat(s, "</div>\n");
  1176. /** controls **/
  1177. if (!local && user)
  1178. s = html_entry_controls(user, s, msg, md5);
  1179. /** children **/
  1180. if (!hide_children) {
  1181. xs *children = object_children(id);
  1182. int left = xs_list_len(children);
  1183. if (left) {
  1184. char *p, *cmd5;
  1185. int older_open = 0;
  1186. xs *ss = xs_str_new(NULL);
  1187. int n_children = 0;
  1188. ss = xs_str_cat(ss, "<details open><summary>...</summary><p>\n");
  1189. if (level < 4)
  1190. ss = xs_str_cat(ss, "<div class=\"snac-children\">\n");
  1191. else
  1192. ss = xs_str_cat(ss, "<div>\n");
  1193. if (left > 3) {
  1194. xs *s1 = xs_fmt("<details><summary>%s</summary>\n", L("Older..."));
  1195. ss = xs_str_cat(ss, s1);
  1196. older_open = 1;
  1197. }
  1198. p = children;
  1199. while (xs_list_iter(&p, &cmd5)) {
  1200. xs *chd = NULL;
  1201. if (user)
  1202. timeline_get_by_md5(user, cmd5, &chd);
  1203. else
  1204. object_get_by_md5(cmd5, &chd);
  1205. if (older_open && left <= 3) {
  1206. ss = xs_str_cat(ss, "</details>\n");
  1207. older_open = 0;
  1208. }
  1209. if (chd != NULL && xs_is_null(xs_dict_get(chd, "name"))) {
  1210. ss = html_entry(user, ss, chd, local, level + 1, cmd5, hide_children);
  1211. n_children++;
  1212. }
  1213. else
  1214. srv_debug(2, xs_fmt("cannot read child %s", cmd5));
  1215. left--;
  1216. }
  1217. if (older_open)
  1218. ss = xs_str_cat(ss, "</details>\n");
  1219. ss = xs_str_cat(ss, "</div>\n");
  1220. ss = xs_str_cat(ss, "</details>\n");
  1221. if (n_children)
  1222. s = xs_str_cat(s, ss);
  1223. }
  1224. }
  1225. s = xs_str_cat(s, "</div>\n</div>\n");
  1226. return xs_str_cat(os, s);
  1227. }
  1228. xs_str *html_footer(xs_str *s)
  1229. {
  1230. xs_html *footer = xs_html_tag("div",
  1231. xs_html_attr("class", "snac-footer"),
  1232. xs_html_tag("a",
  1233. xs_html_attr("href", srv_baseurl),
  1234. xs_html_text(L("about this site"))),
  1235. xs_html_text(" - "),
  1236. xs_html_text(L("powered by ")),
  1237. xs_html_tag("a",
  1238. xs_html_attr("href", WHAT_IS_SNAC_URL),
  1239. xs_html_tag("abbr",
  1240. xs_html_attr("title", "Social Network Are Crap"),
  1241. xs_html_text("snac"))));
  1242. xs *s1 = xs_html_render(footer);
  1243. return xs_str_cat(s, s1, "\n");
  1244. }
  1245. xs_str *html_timeline(snac *user, const xs_list *list, int local,
  1246. int skip, int show, int show_more, char *tag)
  1247. /* returns the HTML for the timeline */
  1248. {
  1249. xs_str *s = xs_str_new(NULL);
  1250. xs_list *p = (xs_list *)list;
  1251. char *v;
  1252. double t = ftime();
  1253. if (user)
  1254. s = html_user_header(user, s, local);
  1255. else
  1256. s = html_instance_header(s, tag);
  1257. if (user && !local)
  1258. s = html_top_controls(user, s);
  1259. s = xs_str_cat(s, "<a name=\"snac-posts\"></a>\n");
  1260. s = xs_str_cat(s, "<div class=\"snac-posts\">\n");
  1261. while (xs_list_iter(&p, &v)) {
  1262. xs *msg = NULL;
  1263. int status;
  1264. if (user && !is_pinned_by_md5(user, v))
  1265. status = timeline_get_by_md5(user, v, &msg);
  1266. else
  1267. status = object_get_by_md5(v, &msg);
  1268. if (!valid_status(status))
  1269. continue;
  1270. /* if it's an instance page, discard private users */
  1271. if (user == NULL && xs_startswith(xs_dict_get(msg, "id"), srv_baseurl)) {
  1272. const char *atto = xs_dict_get(msg, "attributedTo");
  1273. xs *l = xs_split(atto, "/");
  1274. const char *uid = xs_list_get(l, -1);
  1275. snac user;
  1276. int skip = 1;
  1277. if (uid && user_open(&user, uid)) {
  1278. if (xs_type(xs_dict_get(user.config, "private")) != XSTYPE_TRUE)
  1279. skip = 0;
  1280. user_free(&user);
  1281. }
  1282. if (skip)
  1283. continue;
  1284. }
  1285. s = html_entry(user, s, msg, local, 0, v, user ? 0 : 1);
  1286. }
  1287. s = xs_str_cat(s, "</div>\n");
  1288. if (list && user && local) {
  1289. if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE) {
  1290. s = xs_str_cat(s, "<!-- history disabled -->\n");
  1291. }
  1292. else {
  1293. xs *s1 = xs_fmt(
  1294. "<div class=\"snac-history\">\n"
  1295. "<p class=\"snac-history-title\">%s</p><ul>\n",
  1296. L("History")
  1297. );
  1298. s = xs_str_cat(s, s1);
  1299. xs *list = history_list(user);
  1300. char *p, *v;
  1301. p = list;
  1302. while (xs_list_iter(&p, &v)) {
  1303. xs *fn = xs_replace(v, ".html", "");
  1304. xs *s1 = xs_fmt(
  1305. "<li><a href=\"%s/h/%s\">%s</a></li>\n",
  1306. user->actor, v, fn);
  1307. s = xs_str_cat(s, s1);
  1308. }
  1309. s = xs_str_cat(s, "</ul></div>\n");
  1310. }
  1311. }
  1312. {
  1313. xs *s1 = xs_fmt("<!-- %lf seconds -->\n", ftime() - t);
  1314. s = xs_str_cat(s, s1);
  1315. }
  1316. if (show_more) {
  1317. xs *t = NULL;
  1318. xs *m = NULL;
  1319. xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
  1320. if (tag) {
  1321. t = xs_fmt("%s?t=%s", srv_baseurl, tag);
  1322. m = xs_fmt("%s&%s", t, ss);
  1323. }
  1324. else {
  1325. t = xs_fmt("%s%s", user ? user->actor : srv_baseurl, local ? "" : "/admin");
  1326. m = xs_fmt("%s?%s", t, ss);
  1327. }
  1328. xs *s1 = xs_fmt(
  1329. "<p>"
  1330. "<a href=\"%s\" name=\"snac-more\">%s</a> - "
  1331. "<a href=\"%s\" name=\"snac-more\">%s</a>"
  1332. "</p>\n",
  1333. t, L("Back to top"),
  1334. m, L("More...")
  1335. );
  1336. s = xs_str_cat(s, s1);
  1337. }
  1338. s = html_footer(s);
  1339. s = xs_str_cat(s, "</body>\n</html>\n");
  1340. return s;
  1341. }
  1342. xs_str *html_people_list(snac *snac, xs_str *os, xs_list *list, const char *header, const char *t)
  1343. {
  1344. xs *s = xs_str_new(NULL);
  1345. xs *es1 = encode_html(header);
  1346. xs *h = xs_fmt("<h2 class=\"snac-header\">%s</h2>\n", es1);
  1347. xs_list *p;
  1348. char *actor_id;
  1349. s = xs_str_cat(s, h);
  1350. s = xs_str_cat(s, "<div class=\"snac-posts\">\n");
  1351. p = list;
  1352. while (xs_list_iter(&p, &actor_id)) {
  1353. xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
  1354. xs *actor = NULL;
  1355. if (valid_status(actor_get(actor_id, &actor))) {
  1356. xs_html *snac_post = xs_html_tag("div",
  1357. xs_html_attr("class", "snac-post"),
  1358. xs_html_tag("div",
  1359. xs_html_attr("class", "snac-post-header"),
  1360. html_actor_icon(actor, xs_dict_get(actor, "published"), NULL, NULL, 0)));
  1361. /* content (user bio) */
  1362. char *c = xs_dict_get(actor, "summary");
  1363. if (!xs_is_null(c)) {
  1364. xs *sc = sanitize(c);
  1365. xs_html *snac_content = xs_html_tag("div",
  1366. xs_html_attr("class", "snac-content"));
  1367. if (xs_startswith(sc, "<p>"))
  1368. xs_html_add(snac_content,
  1369. xs_html_raw(sc)); /* already sanitized */
  1370. else
  1371. xs_html_add(snac_content,
  1372. xs_html_tag("p",
  1373. xs_html_raw(sc))); /* already sanitized */
  1374. xs_html_add(snac_post, snac_content);
  1375. }
  1376. /* buttons */
  1377. xs *btn_form_action = xs_fmt("%s/admin/action", snac->actor);
  1378. xs_html *snac_controls = xs_html_tag("div",
  1379. xs_html_attr("class", "snac-controls"));
  1380. xs_html *form = xs_html_tag("form",
  1381. xs_html_attr("autocomplete", "off"),
  1382. xs_html_attr("method", "post"),
  1383. xs_html_attr("action", btn_form_action),
  1384. xs_html_sctag("input",
  1385. xs_html_attr("type", "hidden"),
  1386. xs_html_attr("name", "actor"),
  1387. xs_html_attr("value", actor_id)),
  1388. xs_html_sctag("input",
  1389. xs_html_attr("type", "hidden"),
  1390. xs_html_attr("name", "actor-form"),
  1391. xs_html_attr("value", "yes")));
  1392. xs_html_add(snac_controls, form);
  1393. if (following_check(snac, actor_id)) {
  1394. xs_html_add(form,
  1395. html_button_2("unfollow", L("Unfollow"),
  1396. L("Stop following this user's activity")));
  1397. if (is_limited(snac, actor_id))
  1398. xs_html_add(form,
  1399. html_button_2("unlimit", L("Unlimit"),
  1400. L("Allow announces (boosts) from this user")));
  1401. else
  1402. xs_html_add(form,
  1403. html_button_2("limit", L("Limit"),
  1404. L("Block announces (boosts) from this user")));
  1405. }
  1406. else {
  1407. xs_html_add(form,
  1408. html_button_2("follow", L("Follow"),
  1409. L("Start following this user's activity")));
  1410. if (follower_check(snac, actor_id))
  1411. xs_html_add(form,
  1412. html_button_2("delete", L("Delete"), L("Delete this user")));
  1413. }
  1414. if (is_muted(snac, actor_id))
  1415. xs_html_add(form,
  1416. html_button_2("unmute", L("Unmute"),
  1417. L("Stop blocking activities from this user")));
  1418. else
  1419. xs_html_add(form,
  1420. html_button_2("mute", L("MUTE"),
  1421. L("Block any activity from this user")));
  1422. /* the post textarea */
  1423. xs *dm_form_id = xs_fmt("%s_%s_dm", md5, t);
  1424. xs *dm_action = xs_fmt("%s/admin/note", snac->actor);
  1425. xs *dm_form_id2 = xs_fmt("%s_reply_form", md5);
  1426. xs_html *dm_textarea = xs_html_tag("div",
  1427. xs_html_tag("details",
  1428. xs_html_tag("summary",
  1429. xs_html_text(L("Direct Message..."))),
  1430. xs_html_sctag("p", NULL),
  1431. xs_html_tag("div",
  1432. xs_html_attr("class", "snac-note"),
  1433. xs_html_attr("id", dm_form_id),
  1434. xs_html_tag("form",
  1435. xs_html_attr("autocomplete", "off"),
  1436. xs_html_attr("method", "post"),
  1437. xs_html_attr("action", dm_action),
  1438. xs_html_attr("enctype", "multipart/form-data"),
  1439. xs_html_attr("id", dm_form_id2),
  1440. xs_html_tag("textarea",
  1441. xs_html_attr("class", "snac-textarea"),
  1442. xs_html_attr("name", "content"),
  1443. xs_html_attr("rows", "4"),
  1444. xs_html_attr("wrap", "virtual"),
  1445. xs_html_attr("required", "required")),
  1446. xs_html_sctag("input",
  1447. xs_html_attr("type", "hidden"),
  1448. xs_html_attr("name", "to"),
  1449. xs_html_attr("value", actor_id)),
  1450. xs_html_sctag("p", NULL),
  1451. xs_html_sctag("input",
  1452. xs_html_attr("type", "file"),
  1453. xs_html_attr("name", "attach")),
  1454. xs_html_sctag("p", NULL),
  1455. xs_html_sctag("input",
  1456. xs_html_attr("type", "submit"),
  1457. xs_html_attr("class", "button"),
  1458. xs_html_attr("value", L("Post")))),
  1459. xs_html_sctag("p", NULL))),
  1460. xs_html_sctag("p", NULL));
  1461. xs_html_add(snac_controls, dm_textarea);
  1462. xs_html_add(snac_post, snac_controls);
  1463. {
  1464. xs *s1 = xs_html_render(snac_post);
  1465. s = xs_str_cat(s, s1);
  1466. }
  1467. }
  1468. }
  1469. s = xs_str_cat(s, "</div>\n");
  1470. return xs_str_cat(os, s);
  1471. }
  1472. xs_str *html_people(snac *snac)
  1473. {
  1474. xs_str *s = xs_str_new(NULL);
  1475. xs *wing = following_list(snac);
  1476. xs *wers = follower_list(snac);
  1477. s = html_user_header(snac, s, 0);
  1478. s = html_people_list(snac, s, wing, L("People you follow"), "i");
  1479. s = html_people_list(snac, s, wers, L("People that follow you"), "e");
  1480. s = html_footer(s);
  1481. s = xs_str_cat(s, "</body>\n</html>\n");
  1482. return s;
  1483. }
  1484. xs_str *html_notifications(snac *snac)
  1485. {
  1486. xs_str *s = xs_str_new(NULL);
  1487. xs *n_list = notify_list(snac, 0);
  1488. xs *n_time = notify_check_time(snac, 0);
  1489. xs_list *p = n_list;
  1490. xs_str *v;
  1491. enum { NHDR_NONE, NHDR_NEW, NHDR_OLD } stage = NHDR_NONE;
  1492. s = html_user_header(snac, s, 0);
  1493. xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", snac->actor);
  1494. xs_html *clear_all_form = xs_html_tag("form",
  1495. xs_html_attr("autocomplete", "off"),
  1496. xs_html_attr("method", "post"),
  1497. xs_html_attr("action", clear_all_action),
  1498. xs_html_attr("id", "clear"),
  1499. xs_html_sctag("input",
  1500. xs_html_attr("type", "submit"),
  1501. xs_html_attr("class", "snac-btn-like"),
  1502. xs_html_attr("value", L("Clear all"))));
  1503. {
  1504. xs *s1 = xs_html_render(clear_all_form);
  1505. s = xs_str_cat(s, s1);
  1506. }
  1507. while (xs_list_iter(&p, &v)) {
  1508. xs *noti = notify_get(snac, v);
  1509. if (noti == NULL)
  1510. continue;
  1511. xs *obj = NULL;
  1512. const char *type = xs_dict_get(noti, "type");
  1513. const char *utype = xs_dict_get(noti, "utype");
  1514. const char *id = xs_dict_get(noti, "objid");
  1515. if (xs_is_null(id) || !valid_status(object_get(id, &obj)))
  1516. continue;
  1517. if (is_hidden(snac, id))
  1518. continue;
  1519. const char *actor_id = xs_dict_get(noti, "actor");
  1520. xs *actor = NULL;
  1521. if (!valid_status(actor_get(actor_id, &actor)))
  1522. continue;
  1523. xs *a_name = actor_name(actor);
  1524. if (strcmp(v, n_time) > 0) {
  1525. /* unseen notification */
  1526. if (stage == NHDR_NONE) {
  1527. xs *s1 = xs_fmt("<h2 class=\"snac-header\">%s</h2>\n", L("New"));
  1528. s = xs_str_cat(s, s1);
  1529. s = xs_str_cat(s, "<div class=\"snac-posts\">\n");
  1530. stage = NHDR_NEW;
  1531. }
  1532. }
  1533. else {
  1534. /* already seen notification */
  1535. if (stage != NHDR_OLD) {
  1536. if (stage == NHDR_NEW)
  1537. s = xs_str_cat(s, "</div>\n");
  1538. xs *s1 = xs_fmt("<h2 class=\"snac-header\">%s</h2>\n", L("Already seen"));
  1539. s = xs_str_cat(s, s1);
  1540. s = xs_str_cat(s, "<div class=\"snac-posts\">\n");
  1541. stage = NHDR_OLD;
  1542. }
  1543. }
  1544. const char *label = type;
  1545. if (strcmp(type, "Create") == 0)
  1546. label = L("Mention");
  1547. else
  1548. if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
  1549. label = L("Finished poll");
  1550. else
  1551. if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") == 0)
  1552. label = L("Unfollow");
  1553. xs *es1 = encode_html(label);
  1554. xs *s1 = xs_fmt("<div class=\"snac-post-with-desc\">\n"
  1555. "<p><b>%s by <a href=\"%s\">%s</a></b>:</p>\n",
  1556. es1, actor_id, a_name);
  1557. s = xs_str_cat(s, s1);
  1558. if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0) {
  1559. xs_html *div = xs_html_tag("div",
  1560. xs_html_attr("class", "snac-post"),
  1561. html_actor_icon(actor, NULL, NULL, NULL, 0));
  1562. xs *s1 = xs_html_render(div);
  1563. s = xs_str_cat(s, s1);
  1564. }
  1565. else {
  1566. xs *md5 = xs_md5_hex(id, strlen(id));
  1567. s = html_entry(snac, s, obj, 0, 0, md5, 1);
  1568. }
  1569. s = xs_str_cat(s, "</div>\n");
  1570. }
  1571. if (stage == NHDR_NONE) {
  1572. xs *s1 = xs_fmt("<h2 class=\"snac-header\">%s</h2>\n", L("None"));
  1573. s = xs_str_cat(s, s1);
  1574. }
  1575. else
  1576. s = xs_str_cat(s, "</div>\n");
  1577. s = html_footer(s);
  1578. s = xs_str_cat(s, "</body>\n</html>\n");
  1579. /* set the check time to now */
  1580. xs *dummy = notify_check_time(snac, 1);
  1581. dummy = xs_free(dummy);
  1582. timeline_touch(snac);
  1583. return s;
  1584. }
  1585. int html_get_handler(const xs_dict *req, const char *q_path,
  1586. char **body, int *b_size, char **ctype, xs_str **etag)
  1587. {
  1588. char *accept = xs_dict_get(req, "accept");
  1589. int status = 404;
  1590. snac snac;
  1591. xs *uid = NULL;
  1592. char *p_path;
  1593. int cache = 1;
  1594. int save = 1;
  1595. char *v;
  1596. xs *l = xs_split_n(q_path, "/", 2);
  1597. v = xs_list_get(l, 1);
  1598. if (xs_is_null(v)) {
  1599. srv_log(xs_fmt("html_get_handler bad query '%s'", q_path));
  1600. return 404;
  1601. }
  1602. uid = xs_dup(v);
  1603. /* rss extension? */
  1604. if (xs_endswith(uid, ".rss")) {
  1605. uid = xs_crop_i(uid, 0, -4);
  1606. p_path = ".rss";
  1607. }
  1608. else
  1609. p_path = xs_list_get(l, 2);
  1610. if (!uid || !user_open(&snac, uid)) {
  1611. /* invalid user */
  1612. srv_debug(1, xs_fmt("html_get_handler bad user %s", uid));
  1613. return 404;
  1614. }
  1615. /* return the RSS if requested by Accept header */
  1616. if (accept != NULL) {
  1617. if (xs_str_in(accept, "text/xml") != -1 ||
  1618. xs_str_in(accept, "application/rss+xml") != -1)
  1619. p_path = ".rss";
  1620. }
  1621. /* check if server config variable 'disable_cache' is set */
  1622. if ((v = xs_dict_get(srv_config, "disable_cache")) && xs_type(v) == XSTYPE_TRUE)
  1623. cache = 0;
  1624. int skip = 0;
  1625. int show = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries"));
  1626. char *q_vars = xs_dict_get(req, "q_vars");
  1627. if ((v = xs_dict_get(q_vars, "skip")) != NULL)
  1628. skip = atoi(v), cache = 0, save = 0;
  1629. if ((v = xs_dict_get(q_vars, "show")) != NULL)
  1630. show = atoi(v), cache = 0, save = 0;
  1631. if (p_path == NULL) { /** public timeline **/
  1632. xs *h = xs_str_localtime(0, "%Y-%m.html");
  1633. if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) {
  1634. *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL);
  1635. *b_size = strlen(*body);
  1636. status = 200;
  1637. }
  1638. else
  1639. if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) {
  1640. snac_debug(&snac, 1, xs_fmt("serving cached local timeline"));
  1641. status = history_get(&snac, h, body, b_size,
  1642. xs_dict_get(req, "if-none-match"), etag);
  1643. }
  1644. else {
  1645. xs *list = timeline_list(&snac, "public", skip, show);
  1646. xs *next = timeline_list(&snac, "public", skip + show, 1);
  1647. xs *pins = pinned_list(&snac);
  1648. pins = xs_list_cat(pins, list);
  1649. *body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL);
  1650. *b_size = strlen(*body);
  1651. status = 200;
  1652. if (save)
  1653. history_add(&snac, h, *body, *b_size, etag);
  1654. }
  1655. }
  1656. else
  1657. if (strcmp(p_path, "admin") == 0) { /** private timeline **/
  1658. if (!login(&snac, req)) {
  1659. *body = xs_dup(uid);
  1660. status = 401;
  1661. }
  1662. else {
  1663. if (cache && history_mtime(&snac, "timeline.html_") > timeline_mtime(&snac)) {
  1664. snac_debug(&snac, 1, xs_fmt("serving cached timeline"));
  1665. status = history_get(&snac, "timeline.html_", body, b_size,
  1666. xs_dict_get(req, "if-none-match"), etag);
  1667. }
  1668. else {
  1669. snac_debug(&snac, 1, xs_fmt("building timeline"));
  1670. xs *list = timeline_list(&snac, "private", skip, show);
  1671. xs *next = timeline_list(&snac, "private", skip + show, 1);
  1672. xs *pins = pinned_list(&snac);
  1673. pins = xs_list_cat(pins, list);
  1674. *body = html_timeline(&snac, pins, 0, skip, show, xs_list_len(next), NULL);
  1675. *b_size = strlen(*body);
  1676. status = 200;
  1677. if (save)
  1678. history_add(&snac, "timeline.html_", *body, *b_size, etag);
  1679. }
  1680. }
  1681. }
  1682. else
  1683. if (strcmp(p_path, "people") == 0) { /** the list of people **/
  1684. if (!login(&snac, req)) {
  1685. *body = xs_dup(uid);
  1686. status = 401;
  1687. }
  1688. else {
  1689. *body = html_people(&snac);
  1690. *b_size = strlen(*body);
  1691. status = 200;
  1692. }
  1693. }
  1694. else
  1695. if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
  1696. if (!login(&snac, req)) {
  1697. *body = xs_dup(uid);
  1698. status = 401;
  1699. }
  1700. else {
  1701. *body = html_notifications(&snac);
  1702. *b_size = strlen(*body);
  1703. status = 200;
  1704. }
  1705. }
  1706. else
  1707. if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
  1708. if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
  1709. return 403;
  1710. xs *id = xs_fmt("%s/%s", snac.actor, p_path);
  1711. xs *msg = NULL;
  1712. if (valid_status(object_get(id, &msg))) {
  1713. xs *md5 = xs_md5_hex(id, strlen(id));
  1714. xs *list = xs_list_new();
  1715. list = xs_list_append(list, md5);
  1716. *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL);
  1717. *b_size = strlen(*body);
  1718. status = 200;
  1719. }
  1720. }
  1721. else
  1722. if (xs_startswith(p_path, "s/")) { /** a static file **/
  1723. xs *l = xs_split(p_path, "/");
  1724. char *id = xs_list_get(l, 1);
  1725. int sz;
  1726. if (id && *id) {
  1727. status = static_get(&snac, id, body, &sz,
  1728. xs_dict_get(req, "if-none-match"), etag);
  1729. if (valid_status(status)) {
  1730. *b_size = sz;
  1731. *ctype = (char *)xs_mime_by_ext(id);
  1732. }
  1733. }
  1734. }
  1735. else
  1736. if (xs_startswith(p_path, "h/")) { /** an entry from the history **/
  1737. if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
  1738. return 403;
  1739. if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
  1740. return 403;
  1741. xs *l = xs_split(p_path, "/");
  1742. char *id = xs_list_get(l, 1);
  1743. if (id && *id) {
  1744. if (xs_endswith(id, "timeline.html_")) {
  1745. /* Don't let them in */
  1746. *b_size = 0;
  1747. status = 404;
  1748. }
  1749. else
  1750. status = history_get(&snac, id, body, b_size,
  1751. xs_dict_get(req, "if-none-match"), etag);
  1752. }
  1753. }
  1754. else
  1755. if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/
  1756. if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
  1757. return 403;
  1758. xs *elems = timeline_simple_list(&snac, "public", 0, 20);
  1759. xs *bio = not_really_markdown(xs_dict_get(snac.config, "bio"), NULL);
  1760. xs *rss_title = xs_fmt("%s (@%s@%s)",
  1761. xs_dict_get(snac.config, "name"),
  1762. snac.uid,
  1763. xs_dict_get(srv_config, "host"));
  1764. xs *rss_link = xs_fmt("%s.rss", snac.actor);
  1765. xs_html *rss = xs_html_tag("rss",
  1766. xs_html_attr("version", "0.91"));
  1767. xs_html *channel = xs_html_tag("channel",
  1768. xs_html_tag("title",
  1769. xs_html_text(rss_title)),
  1770. xs_html_tag("language",
  1771. xs_html_text("en")),
  1772. xs_html_tag("link",
  1773. xs_html_text(rss_link)),
  1774. xs_html_tag("description",
  1775. xs_html_text(bio)));
  1776. xs_html_add(rss, channel);
  1777. xs_list *p = elems;
  1778. char *v;
  1779. while (xs_list_iter(&p, &v)) {
  1780. xs *msg = NULL;
  1781. if (!valid_status(timeline_get_by_md5(&snac, v, &msg)))
  1782. continue;
  1783. char *id = xs_dict_get(msg, "id");
  1784. char *content = xs_dict_get(msg, "content");
  1785. if (!xs_startswith(id, snac.actor))
  1786. continue;
  1787. /* create a title with the first line of the content */
  1788. xs *es_title = xs_replace(content, "<br>", "\n");
  1789. xs *title = xs_str_new(NULL);
  1790. int i;
  1791. for (i = 0; es_title[i] && es_title[i] != '\n' && es_title[i] != '&' && i < 50; i++)
  1792. title = xs_append_m(title, &es_title[i], 1);
  1793. xs_html_add(channel,
  1794. xs_html_tag("item",
  1795. xs_html_tag("title",
  1796. xs_html_text(title)),
  1797. xs_html_tag("link",
  1798. xs_html_text(id)),
  1799. xs_html_tag("description",
  1800. xs_html_text(content))));
  1801. }
  1802. *body = _xs_html_render(rss, xs_dup("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"));
  1803. *b_size = strlen(*body);
  1804. *ctype = "application/rss+xml; charset=utf-8";
  1805. status = 200;
  1806. snac_debug(&snac, 1, xs_fmt("serving RSS"));
  1807. }
  1808. else
  1809. status = 404;
  1810. user_free(&snac);
  1811. if (valid_status(status) && *ctype == NULL) {
  1812. *ctype = "text/html; charset=utf-8";
  1813. }
  1814. return status;
  1815. }
  1816. int html_post_handler(const xs_dict *req, const char *q_path,
  1817. char *payload, int p_size,
  1818. char **body, int *b_size, char **ctype)
  1819. {
  1820. (void)p_size;
  1821. (void)ctype;
  1822. int status = 0;
  1823. snac snac;
  1824. char *uid, *p_path;
  1825. xs_dict *p_vars;
  1826. xs *l = xs_split_n(q_path, "/", 2);
  1827. uid = xs_list_get(l, 1);
  1828. if (!uid || !user_open(&snac, uid)) {
  1829. /* invalid user */
  1830. srv_debug(1, xs_fmt("html_post_handler bad user %s", uid));
  1831. return 404;
  1832. }
  1833. p_path = xs_list_get(l, 2);
  1834. /* all posts must be authenticated */
  1835. if (!login(&snac, req)) {
  1836. user_free(&snac);
  1837. *body = xs_dup(uid);
  1838. return 401;
  1839. }
  1840. p_vars = xs_dict_get(req, "p_vars");
  1841. #if 0
  1842. xs_json_dump(p_vars, 4, stdout);
  1843. #endif
  1844. if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/
  1845. /* post note */
  1846. xs_str *content = xs_dict_get(p_vars, "content");
  1847. xs_str *in_reply_to = xs_dict_get(p_vars, "in_reply_to");
  1848. xs_str *attach_url = xs_dict_get(p_vars, "attach_url");
  1849. xs_list *attach_file = xs_dict_get(p_vars, "attach");
  1850. xs_str *to = xs_dict_get(p_vars, "to");
  1851. xs_str *sensitive = xs_dict_get(p_vars, "sensitive");
  1852. xs_str *summary = xs_dict_get(p_vars, "summary");
  1853. xs_str *edit_id = xs_dict_get(p_vars, "edit_id");
  1854. xs_str *alt_text = xs_dict_get(p_vars, "alt_text");
  1855. int priv = !xs_is_null(xs_dict_get(p_vars, "mentioned_only"));
  1856. xs *attach_list = xs_list_new();
  1857. /* default alt text */
  1858. if (xs_is_null(alt_text))
  1859. alt_text = "";
  1860. /* is attach_url set? */
  1861. if (!xs_is_null(attach_url) && *attach_url != '\0') {
  1862. xs *l = xs_list_new();
  1863. l = xs_list_append(l, attach_url);
  1864. l = xs_list_append(l, alt_text);
  1865. attach_list = xs_list_append(attach_list, l);
  1866. }
  1867. /* is attach_file set? */
  1868. if (!xs_is_null(attach_file) && xs_type(attach_file) == XSTYPE_LIST) {
  1869. char *fn = xs_list_get(attach_file, 0);
  1870. if (*fn != '\0') {
  1871. char *ext = strrchr(fn, '.');
  1872. xs *hash = xs_md5_hex(fn, strlen(fn));
  1873. xs *id = xs_fmt("%s%s", hash, ext);
  1874. xs *url = xs_fmt("%s/s/%s", snac.actor, id);
  1875. int fo = xs_number_get(xs_list_get(attach_file, 1));
  1876. int fs = xs_number_get(xs_list_get(attach_file, 2));
  1877. /* store */
  1878. static_put(&snac, id, payload + fo, fs);
  1879. xs *l = xs_list_new();
  1880. l = xs_list_append(l, url);
  1881. l = xs_list_append(l, alt_text);
  1882. attach_list = xs_list_append(attach_list, l);
  1883. }
  1884. }
  1885. if (content != NULL) {
  1886. xs *msg = NULL;
  1887. xs *c_msg = NULL;
  1888. xs *content_2 = xs_replace(content, "\r", "");
  1889. xs *poll_opts = NULL;
  1890. /* is there a valid set of poll options? */
  1891. const char *v = xs_dict_get(p_vars, "poll_options");
  1892. if (!xs_is_null(v) && *v) {
  1893. xs *v2 = xs_strip_i(xs_replace(v, "\r", ""));
  1894. poll_opts = xs_split(v2, "\n");
  1895. }
  1896. if (!xs_is_null(poll_opts) && xs_list_len(poll_opts)) {
  1897. /* get the rest of poll configuration */
  1898. const char *p_multiple = xs_dict_get(p_vars, "poll_multiple");
  1899. const char *p_end_secs = xs_dict_get(p_vars, "poll_end_secs");
  1900. int multiple = 0;
  1901. int end_secs = atoi(!xs_is_null(p_end_secs) ? p_end_secs : "60");
  1902. if (!xs_is_null(p_multiple) && strcmp(p_multiple, "on") == 0)
  1903. multiple = 1;
  1904. msg = msg_question(&snac, content_2, attach_list,
  1905. poll_opts, multiple, end_secs);
  1906. enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
  1907. }
  1908. else
  1909. msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, priv);
  1910. if (sensitive != NULL) {
  1911. msg = xs_dict_set(msg, "sensitive", xs_stock_true);
  1912. msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary);
  1913. }
  1914. if (xs_is_null(edit_id)) {
  1915. /* new message */
  1916. c_msg = msg_create(&snac, msg);
  1917. timeline_add(&snac, xs_dict_get(msg, "id"), msg);
  1918. }
  1919. else {
  1920. /* an edition of a previous message */
  1921. xs *p_msg = NULL;
  1922. if (valid_status(object_get(edit_id, &p_msg))) {
  1923. /* copy relevant fields from previous version */
  1924. char *fields[] = { "id", "context", "url", "published",
  1925. "to", "inReplyTo", NULL };
  1926. int n;
  1927. for (n = 0; fields[n]; n++) {
  1928. char *v = xs_dict_get(p_msg, fields[n]);
  1929. msg = xs_dict_set(msg, fields[n], v);
  1930. }
  1931. /* set the updated field */
  1932. xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
  1933. msg = xs_dict_set(msg, "updated", updated);
  1934. /* overwrite object, not updating the indexes */
  1935. object_add_ow(edit_id, msg);
  1936. /* update message */
  1937. c_msg = msg_update(&snac, msg);
  1938. }
  1939. else
  1940. snac_log(&snac, xs_fmt("cannot get object '%s' for editing", edit_id));
  1941. }
  1942. if (c_msg != NULL)
  1943. enqueue_message(&snac, c_msg);
  1944. history_del(&snac, "timeline.html_");
  1945. }
  1946. status = 303;
  1947. }
  1948. else
  1949. if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
  1950. /* action on an entry */
  1951. char *id = xs_dict_get(p_vars, "id");
  1952. char *actor = xs_dict_get(p_vars, "actor");
  1953. char *action = xs_dict_get(p_vars, "action");
  1954. char *group = xs_dict_get(p_vars, "group");
  1955. if (action == NULL)
  1956. return 404;
  1957. snac_debug(&snac, 1, xs_fmt("web action '%s' received", action));
  1958. status = 303;
  1959. if (strcmp(action, L("Like")) == 0) { /** **/
  1960. xs *msg = msg_admiration(&snac, id, "Like");
  1961. if (msg != NULL) {
  1962. enqueue_message(&snac, msg);
  1963. timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1);
  1964. }
  1965. }
  1966. else
  1967. if (strcmp(action, L("Boost")) == 0) { /** **/
  1968. xs *msg = msg_admiration(&snac, id, "Announce");
  1969. if (msg != NULL) {
  1970. enqueue_message(&snac, msg);
  1971. timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0);
  1972. }
  1973. }
  1974. else
  1975. if (strcmp(action, L("MUTE")) == 0) { /** **/
  1976. mute(&snac, actor);
  1977. }
  1978. else
  1979. if (strcmp(action, L("Unmute")) == 0) { /** **/
  1980. unmute(&snac, actor);
  1981. }
  1982. else
  1983. if (strcmp(action, L("Hide")) == 0) { /** **/
  1984. hide(&snac, id);
  1985. }
  1986. else
  1987. if (strcmp(action, L("Limit")) == 0) { /** **/
  1988. limit(&snac, actor);
  1989. }
  1990. else
  1991. if (strcmp(action, L("Unlimit")) == 0) { /** **/
  1992. unlimit(&snac, actor);
  1993. }
  1994. else
  1995. if (strcmp(action, L("Follow")) == 0) { /** **/
  1996. xs *msg = msg_follow(&snac, actor);
  1997. if (msg != NULL) {
  1998. /* reload the actor from the message, in may be different */
  1999. actor = xs_dict_get(msg, "object");
  2000. following_add(&snac, actor, msg);
  2001. enqueue_output_by_actor(&snac, msg, actor, 0);
  2002. }
  2003. }
  2004. else
  2005. if (strcmp(action, L("Unfollow")) == 0) { /** **/
  2006. /* get the following object */
  2007. xs *object = NULL;
  2008. if (valid_status(following_get(&snac, actor, &object))) {
  2009. xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
  2010. following_del(&snac, actor);
  2011. enqueue_output_by_actor(&snac, msg, actor, 0);
  2012. snac_log(&snac, xs_fmt("unfollowed actor %s", actor));
  2013. }
  2014. else
  2015. snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
  2016. }
  2017. else
  2018. if (strcmp(action, L("Follow Group")) == 0) { /** **/
  2019. xs *msg = msg_follow(&snac, group);
  2020. if (msg != NULL) {
  2021. /* reload the group from the message, in may be different */
  2022. group = xs_dict_get(msg, "object");
  2023. following_add(&snac, group, msg);
  2024. enqueue_output_by_actor(&snac, msg, group, 0);
  2025. }
  2026. }
  2027. else
  2028. if (strcmp(action, L("Unfollow Group")) == 0) { /** **/
  2029. /* get the following object */
  2030. xs *object = NULL;
  2031. if (valid_status(following_get(&snac, group, &object))) {
  2032. xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
  2033. following_del(&snac, group);
  2034. enqueue_output_by_actor(&snac, msg, group, 0);
  2035. snac_log(&snac, xs_fmt("unfollowed group %s", group));
  2036. }
  2037. else
  2038. snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
  2039. }
  2040. else
  2041. if (strcmp(action, L("Delete")) == 0) { /** **/
  2042. char *actor_form = xs_dict_get(p_vars, "actor-form");
  2043. if (actor_form != NULL) {
  2044. /* delete follower */
  2045. if (valid_status(follower_del(&snac, actor)))
  2046. snac_log(&snac, xs_fmt("deleted follower %s", actor));
  2047. else
  2048. snac_log(&snac, xs_fmt("error deleting follower %s", actor));
  2049. }
  2050. else {
  2051. /* delete an entry */
  2052. if (xs_startswith(id, snac.actor)) {
  2053. /* it's a post by us: generate a delete */
  2054. xs *msg = msg_delete(&snac, id);
  2055. enqueue_message(&snac, msg);
  2056. snac_log(&snac, xs_fmt("posted tombstone for %s", id));
  2057. }
  2058. timeline_del(&snac, id);
  2059. snac_log(&snac, xs_fmt("deleted entry %s", id));
  2060. }
  2061. }
  2062. else
  2063. if (strcmp(action, L("Pin")) == 0) { /** **/
  2064. pin(&snac, id);
  2065. timeline_touch(&snac);
  2066. }
  2067. else
  2068. if (strcmp(action, L("Unpin")) == 0) { /** **/
  2069. unpin(&snac, id);
  2070. timeline_touch(&snac);
  2071. }
  2072. else
  2073. status = 404;
  2074. /* delete the cached timeline */
  2075. if (status == 303)
  2076. history_del(&snac, "timeline.html_");
  2077. }
  2078. else
  2079. if (p_path && strcmp(p_path, "admin/user-setup") == 0) { /** **/
  2080. /* change of user data */
  2081. char *v;
  2082. char *p1, *p2;
  2083. if ((v = xs_dict_get(p_vars, "name")) != NULL)
  2084. snac.config = xs_dict_set(snac.config, "name", v);
  2085. if ((v = xs_dict_get(p_vars, "avatar")) != NULL)
  2086. snac.config = xs_dict_set(snac.config, "avatar", v);
  2087. if ((v = xs_dict_get(p_vars, "bio")) != NULL)
  2088. snac.config = xs_dict_set(snac.config, "bio", v);
  2089. if ((v = xs_dict_get(p_vars, "cw")) != NULL &&
  2090. strcmp(v, "on") == 0) {
  2091. snac.config = xs_dict_set(snac.config, "cw", "open");
  2092. } else { /* if the checkbox is not set, the parameter is missing */
  2093. snac.config = xs_dict_set(snac.config, "cw", "");
  2094. }
  2095. if ((v = xs_dict_get(p_vars, "email")) != NULL)
  2096. snac.config = xs_dict_set(snac.config, "email", v);
  2097. if ((v = xs_dict_get(p_vars, "telegram_bot")) != NULL)
  2098. snac.config = xs_dict_set(snac.config, "telegram_bot", v);
  2099. if ((v = xs_dict_get(p_vars, "telegram_chat_id")) != NULL)
  2100. snac.config = xs_dict_set(snac.config, "telegram_chat_id", v);
  2101. if ((v = xs_dict_get(p_vars, "purge_days")) != NULL) {
  2102. xs *days = xs_number_new(atof(v));
  2103. snac.config = xs_dict_set(snac.config, "purge_days", days);
  2104. }
  2105. if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0)
  2106. snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock_true);
  2107. else
  2108. snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock_false);
  2109. if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0)
  2110. snac.config = xs_dict_set(snac.config, "bot", xs_stock_true);
  2111. else
  2112. snac.config = xs_dict_set(snac.config, "bot", xs_stock_false);
  2113. if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0)
  2114. snac.config = xs_dict_set(snac.config, "private", xs_stock_true);
  2115. else
  2116. snac.config = xs_dict_set(snac.config, "private", xs_stock_false);
  2117. if ((v = xs_dict_get(p_vars, "metadata")) != NULL) {
  2118. /* split the metadata and store it as a dict */
  2119. xs_dict *md = xs_dict_new();
  2120. xs *l = xs_split(v, "\n");
  2121. xs_list *p = l;
  2122. xs_str *kp;
  2123. while (xs_list_iter(&p, &kp)) {
  2124. xs *kpl = xs_split_n(kp, "=", 1);
  2125. if (xs_list_len(kpl) == 2) {
  2126. xs *k2 = xs_strip_i(xs_dup(xs_list_get(kpl, 0)));
  2127. xs *v2 = xs_strip_i(xs_dup(xs_list_get(kpl, 1)));
  2128. md = xs_dict_set(md, k2, v2);
  2129. }
  2130. }
  2131. snac.config = xs_dict_set(snac.config, "metadata", md);
  2132. }
  2133. /* uploads */
  2134. const char *uploads[] = { "avatar", "header", NULL };
  2135. int n;
  2136. for (n = 0; uploads[n]; n++) {
  2137. xs *var_name = xs_fmt("%s_file", uploads[n]);
  2138. xs_list *uploaded_file = xs_dict_get(p_vars, var_name);
  2139. if (xs_type(uploaded_file) == XSTYPE_LIST) {
  2140. const char *fn = xs_list_get(uploaded_file, 0);
  2141. if (fn && *fn) {
  2142. const char *mimetype = xs_mime_by_ext(fn);
  2143. if (xs_startswith(mimetype, "image/")) {
  2144. const char *ext = strrchr(fn, '.');
  2145. xs *hash = xs_md5_hex(fn, strlen(fn));
  2146. xs *id = xs_fmt("%s%s", hash, ext);
  2147. xs *url = xs_fmt("%s/s/%s", snac.actor, id);
  2148. int fo = xs_number_get(xs_list_get(uploaded_file, 1));
  2149. int fs = xs_number_get(xs_list_get(uploaded_file, 2));
  2150. /* store */
  2151. static_put(&snac, id, payload + fo, fs);
  2152. snac.config = xs_dict_set(snac.config, uploads[n], url);
  2153. }
  2154. }
  2155. }
  2156. }
  2157. /* password change? */
  2158. if ((p1 = xs_dict_get(p_vars, "passwd1")) != NULL &&
  2159. (p2 = xs_dict_get(p_vars, "passwd2")) != NULL &&
  2160. *p1 && strcmp(p1, p2) == 0) {
  2161. xs *pw = hash_password(snac.uid, p1, NULL);
  2162. snac.config = xs_dict_set(snac.config, "passwd", pw);
  2163. }
  2164. xs *fn = xs_fmt("%s/user.json", snac.basedir);
  2165. xs *bfn = xs_fmt("%s.bak", fn);
  2166. FILE *f;
  2167. rename(fn, bfn);
  2168. if ((f = fopen(fn, "w")) != NULL) {
  2169. xs_json_dump(snac.config, 4, f);
  2170. fclose(f);
  2171. }
  2172. else
  2173. rename(bfn, fn);
  2174. history_del(&snac, "timeline.html_");
  2175. xs *a_msg = msg_actor(&snac);
  2176. xs *u_msg = msg_update(&snac, a_msg);
  2177. enqueue_message(&snac, u_msg);
  2178. status = 303;
  2179. }
  2180. else
  2181. if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
  2182. notify_clear(&snac);
  2183. timeline_touch(&snac);
  2184. status = 303;
  2185. }
  2186. else
  2187. if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
  2188. char *irt = xs_dict_get(p_vars, "irt");
  2189. const char *opt = xs_dict_get(p_vars, "question");
  2190. const char *actor = xs_dict_get(p_vars, "actor");
  2191. xs *ls = NULL;
  2192. /* multiple choices? */
  2193. if (xs_type(opt) == XSTYPE_LIST)
  2194. ls = xs_dup(opt);
  2195. else {
  2196. ls = xs_list_new();
  2197. ls = xs_list_append(ls, opt);
  2198. }
  2199. xs_list *p = ls;
  2200. xs_str *v;
  2201. while (xs_list_iter(&p, &v)) {
  2202. xs *msg = msg_note(&snac, "", actor, irt, NULL, 1);
  2203. /* set the option */
  2204. msg = xs_dict_append(msg, "name", v);
  2205. xs *c_msg = msg_create(&snac, msg);
  2206. enqueue_message(&snac, c_msg);
  2207. timeline_add(&snac, xs_dict_get(msg, "id"), msg);
  2208. }
  2209. status = 303;
  2210. }
  2211. if (status == 303) {
  2212. char *redir = xs_dict_get(p_vars, "redir");
  2213. if (xs_is_null(redir))
  2214. redir = "top";
  2215. *body = xs_fmt("%s/admin#%s", snac.actor, redir);
  2216. *b_size = strlen(*body);
  2217. }
  2218. user_free(&snac);
  2219. return status;
  2220. }