html.c 71 KB

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