1
0

html.c 225 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262
  1. /* snac - A simple, minimalistic ActivityPub instance */
  2. /* copyright (c) 2022 - 2026 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 "xs_curl.h"
  14. #include "xs_unicode.h"
  15. #include "xs_url.h"
  16. #include "xs_random.h"
  17. #include "xs_http.h"
  18. #include "xs_list_tools.h"
  19. #include "snac.h"
  20. int login(snac *user, const xs_dict *headers)
  21. /* tries a login */
  22. {
  23. int logged_in = 0;
  24. const char *auth = xs_dict_get(headers, "authorization");
  25. if (auth && xs_startswith(auth, "Basic ")) {
  26. int sz;
  27. xs *s1 = xs_crop_i(xs_dup(auth), 6, 0);
  28. xs *s2 = xs_base64_dec(s1, &sz);
  29. xs *l1 = xs_split_n(s2, ":", 1);
  30. if (xs_list_len(l1) == 2) {
  31. const char *uid = xs_list_get(l1, 0);
  32. const char *pwd = xs_list_get(l1, 1);
  33. const char *addr = xs_or(xs_dict_get(headers, "remote-addr"),
  34. xs_dict_get(headers, "x-forwarded-for"));
  35. if (badlogin_check(uid, addr)) {
  36. logged_in = check_password(uid, pwd,
  37. xs_dict_get(user->config, "passwd"));
  38. if (!logged_in)
  39. badlogin_inc(uid, addr);
  40. }
  41. }
  42. }
  43. if (logged_in)
  44. lastlog_write(user, "web");
  45. return logged_in;
  46. }
  47. xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems,
  48. const char *proxy, const xs_list *cl, const char *act_o)
  49. /* replace but also adds a class list and an actor in its alt text.
  50. * Used for emoji reactions */
  51. {
  52. if (!xs_is_null(tag)) {
  53. xs *tag_list = NULL;
  54. if (xs_type(tag) == XSTYPE_DICT) {
  55. /* not a list */
  56. tag_list = xs_list_new();
  57. tag_list = xs_list_append(tag_list, tag);
  58. } else {
  59. /* is a list */
  60. tag_list = xs_dup(tag);
  61. }
  62. xs *style = xs_fmt("max-height: %dem; max-width: %dem;", ems, ems);
  63. xs *class = xs_fmt("snac-emoji snac-emoji-%d-em", ems);
  64. if (cl) {
  65. xs *l = xs_join(cl, " ");
  66. class = xs_str_cat(class, " ", l);
  67. }
  68. int c = 0;
  69. const xs_val *v;
  70. c = 0;
  71. xs_set rep_emoji;
  72. xs_set_init(&rep_emoji);
  73. while (xs_list_next(tag_list, &v, &c)) {
  74. const char *t = xs_dict_get(v, "type");
  75. if (t && strcmp(t, "Emoji") == 0) {
  76. const char *n = xs_dict_get(v, "name");
  77. const xs_dict *i = xs_dict_get(v, "icon");
  78. /* avoid repeated emojis (Misskey seems to return this) */
  79. if (xs_set_add(&rep_emoji, n) == 0)
  80. continue;
  81. if (xs_is_string(n) && xs_is_dict(i)) {
  82. const char *u = xs_dict_get(i, "url");
  83. const char *mt = xs_dict_get(i, "mediaType");
  84. if (xs_is_string(u)) {
  85. // on akkoma instances mediaType is not present.
  86. // but we need to to know if the image is an svg or not.
  87. // for now, i just use the file extention, which may not be the most reliable...
  88. if (!xs_is_string(mt))
  89. mt = xs_mime_by_ext(u);
  90. xs *act = act_o ? xs_fmt("%s\n%s", n, act_o) : xs_fmt("%s", n);
  91. if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
  92. s = xs_replace_i(s, n, "");
  93. else {
  94. xs *url = make_url(u, proxy, 0);
  95. xs_html *img = xs_html_sctag("img",
  96. xs_html_attr("loading", "lazy"),
  97. xs_html_attr("src", url),
  98. xs_html_attr("alt", act),
  99. xs_html_attr("title", act),
  100. xs_html_attr("class", class),
  101. xs_html_attr("style", style));
  102. xs *s1 = xs_html_render(img);
  103. s = xs_replace_i(s, n, s1);
  104. }
  105. }
  106. else
  107. s = xs_replace_i(s, n, "");
  108. }
  109. }
  110. }
  111. xs_set_free(&rep_emoji);
  112. }
  113. return s;
  114. }
  115. xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy)
  116. /* replaces all the :shortnames: with the emojis in tag */
  117. {
  118. return _replace_shortnames(s, tag, ems, proxy, NULL, NULL);
  119. }
  120. xs_str *actor_name(xs_dict *actor, const char *proxy)
  121. /* gets the actor name */
  122. {
  123. const char *v;
  124. if (xs_is_null((v = xs_dict_get(actor, "name"))) || *v == '\0') {
  125. if (xs_is_null(v = xs_dict_get(actor, "preferredUsername")) || *v == '\0') {
  126. v = "anonymous";
  127. }
  128. }
  129. return replace_shortnames(xs_html_encode(v), xs_dict_get(actor, "tag"), 1, proxy);
  130. }
  131. xs_str *actor_pronouns(xs_dict *actor)
  132. /* gets the actor name */
  133. {
  134. const xs_list *attachment;
  135. const xs_dict *d;
  136. const char *v;
  137. const char *pronouns = "";
  138. xs_str *ret;
  139. if (xs_is_list((attachment = xs_dict_get(actor, "attachment")))) {
  140. xs_list_foreach(attachment, d) {
  141. xs *prop = xs_utf8_to_lower(xs_dict_get(d, "name"));
  142. /* make sure that we are reading the correct metadata */
  143. if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) {
  144. /* safeguard from NULL values */
  145. v = xs_dict_get(d, "value");
  146. pronouns = v ? v : pronouns;
  147. break;
  148. }
  149. }
  150. }
  151. /* strip all HTML tags */
  152. ret = xs_regex_replace(pronouns, "</?[^>]+>", "");
  153. return ret;
  154. }
  155. xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy)
  156. /* needed when we have local text with no tags attached */
  157. {
  158. xs *tags = xs_list_new();
  159. xs *name1 = not_really_markdown(text, NULL, &tags);
  160. xs_str *name3;
  161. if (user) {
  162. xs *name2 = process_tags(user, name1, &tags);
  163. name3 = sanitize(name2);
  164. }
  165. else {
  166. name3 = sanitize(name1);
  167. name3 = xs_replace_i(name3, "<br>", "");
  168. }
  169. return replace_shortnames(name3, tags, ems, proxy);
  170. }
  171. xs_str *html_date_label(snac *user, const char *date)
  172. {
  173. time_t t;
  174. /* check if a user has actually set a timezone */
  175. if (user != NULL && xs_dict_get(user->config, "tz") != NULL &&
  176. (t = xs_parse_iso_date(date, 0)) != 0) {
  177. t += xs_tz_offset(user->tz);
  178. time_t today = time(NULL);
  179. if (today - t >= 3600 * 24 * 30 * 6) {
  180. /* more than half a year ago */
  181. return xs_str_utctime(t, "%Y-%m-%d %H:%M");
  182. }
  183. xs_str *date = xs_str_utctime(t, "%b %d %H:%M");
  184. struct tm tm;
  185. localtime_r(&t, &tm);
  186. if (tm.tm_mon >= 0 && tm.tm_mon <= 11) {
  187. /* copy the 3letter translated month name */
  188. const char *m = L(months[tm.tm_mon]);
  189. if (xs_is_string(m))
  190. date = xs_replace_i(date, months[tm.tm_mon], m);
  191. }
  192. return date;
  193. }
  194. return xs_crop_i(xs_dup(date), 0, 10);
  195. }
  196. xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
  197. const char *udate, const char *url, int scope,
  198. int in_people, const char *proxy, const char *lang,
  199. const char *md5)
  200. {
  201. xs_html *actor_icon = xs_html_tag("p", NULL);
  202. xs *avatar = NULL;
  203. const char *v;
  204. int fwing = 0;
  205. int fwer = 0;
  206. xs *name = actor_name(actor, proxy);
  207. xs *pronouns = actor_pronouns(actor);
  208. /* get the avatar */
  209. if ((v = xs_dict_get(actor, "icon")) != NULL) {
  210. /* if it's a list (Peertube), get the first one */
  211. if (xs_type(v) == XSTYPE_LIST)
  212. v = xs_list_get(v, 0);
  213. if ((v = xs_dict_get(v, "url")) != NULL)
  214. avatar = make_url(v, proxy, 0);
  215. }
  216. if (avatar == NULL)
  217. avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64());
  218. const char *actor_id = xs_dict_get(actor, "id");
  219. const char *html_url = xs_dict_get_def(actor, "url", actor_id);
  220. if (xs_is_list(html_url))
  221. html_url = xs_list_get(html_url, 0);
  222. if (!xs_is_string(html_url))
  223. html_url = actor_id;
  224. xs *href = NULL;
  225. if (user) {
  226. fwer = follower_check(user, actor_id);
  227. fwing = following_check(user, actor_id);
  228. }
  229. if (user && !in_people) {
  230. xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
  231. href = xs_fmt("%s/people/%s", user->actor, md5);
  232. }
  233. if (href == NULL)
  234. href = xs_dup(html_url);
  235. xs_html *name_link = xs_html_tag("a",
  236. xs_html_attr("href", href),
  237. xs_html_attr("class", "p-author h-card snac-author"),
  238. xs_html_raw(name)); /* name is already html-escaped */
  239. if (*pronouns) {
  240. xs_html_add(name_link,
  241. xs_html_text(" ["),
  242. xs_html_tag("span",
  243. xs_html_attr("class", "snac-pronouns"),
  244. xs_html_attr("title", "user's pronouns"),
  245. xs_html_raw(pronouns)),
  246. xs_html_text("]"));
  247. }
  248. xs_html_add(actor_icon,
  249. xs_html_sctag("img",
  250. xs_html_attr("loading", "lazy"),
  251. xs_html_attr("class", "snac-avatar"),
  252. xs_html_attr("src", avatar),
  253. xs_html_attr("alt", "[?]")),
  254. name_link);
  255. if (!xs_is_null(url)) {
  256. xs_html_add(actor_icon,
  257. xs_html_text(" "),
  258. xs_html_tag("a",
  259. xs_html_attr("href", (char *)url),
  260. xs_html_attr("title", xs_is_string(md5) ? md5 : ""),
  261. xs_html_text("»")));
  262. }
  263. if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) {
  264. xs_html_add(actor_icon,
  265. xs_html_text(" "),
  266. xs_html_tag("span",
  267. xs_html_attr("title", "bot"),
  268. xs_html_raw("&#129302;")));
  269. }
  270. if (fwing && fwer) {
  271. xs_html_add(actor_icon,
  272. xs_html_text(" "),
  273. xs_html_tag("span",
  274. xs_html_attr("title", "mutual relation"),
  275. xs_html_raw("&#129309;")));
  276. }
  277. else
  278. if (fwer) {
  279. xs_html_add(actor_icon,
  280. xs_html_text(" "),
  281. xs_html_tag("span",
  282. xs_html_attr("title", "follows you"),
  283. xs_html_raw("&#128077;")));
  284. }
  285. if (instance_failure(actor_id, 0) == -1) {
  286. xs_html_add(actor_icon,
  287. xs_html_text(" "),
  288. xs_html_tag("span",
  289. xs_html_attr("title", "broken instance"),
  290. xs_html_raw("&#128148;")));
  291. }
  292. if (actor_failure(actor_id, 0) == -1) {
  293. xs_html_add(actor_icon,
  294. xs_html_text(" "),
  295. xs_html_tag("span",
  296. xs_html_attr("title", "account no longer exists"),
  297. xs_html_raw("&#128128;")));
  298. }
  299. if (scope != -1) {
  300. if (scope == SCOPE_FOLLOWERS) {
  301. xs_html_add(actor_icon,
  302. xs_html_text(" "),
  303. xs_html_tag("span",
  304. xs_html_attr("title", "followers"),
  305. xs_html_raw("&#128274;"))); // emoji of a lock
  306. }
  307. else if (scope == SCOPE_PUBLIC) {
  308. xs_html_add(actor_icon,
  309. xs_html_text(" "),
  310. xs_html_tag("span",
  311. xs_html_attr("title", "public"),
  312. xs_html_raw("&#127760;"))); // emoji of a globe
  313. }
  314. else if (scope == SCOPE_UNLISTED) {
  315. xs_html_add(actor_icon,
  316. xs_html_text(" "),
  317. xs_html_tag("span",
  318. xs_html_attr("title", "unlisted"),
  319. xs_html_raw("&#128275;"))); // emoji of an unlocked lock
  320. }
  321. else if (scope == SCOPE_MENTIONED) {
  322. xs_html_add(actor_icon,
  323. xs_html_text(" "),
  324. xs_html_tag("span",
  325. xs_html_attr("title", "mentioned"),
  326. xs_html_raw("&#9993;&#65039;"))); // emoji of a mail
  327. }
  328. }
  329. if (xs_is_null(date)) {
  330. xs_html_add(actor_icon,
  331. xs_html_raw("&nbsp;"));
  332. }
  333. else {
  334. xs *date_label = html_date_label(user, date);
  335. xs *date_title = xs_dup(date);
  336. if (!xs_is_null(udate)) {
  337. xs *sd = html_date_label(user, udate);
  338. date_label = xs_str_cat(date_label, " / ", sd);
  339. date_title = xs_str_cat(date_title, " / ", udate);
  340. }
  341. if (xs_is_string(lang))
  342. date_title = xs_str_cat(date_title, " (", lang, ")");
  343. xs_html *date_text = xs_html_text(date_label);
  344. if (user && md5) {
  345. xs *lpost_url = xs_fmt("%s/admin/p/%s#%s_entry",
  346. user->actor, md5, md5);
  347. date_text = xs_html_tag("a",
  348. xs_html_attr("href", lpost_url),
  349. xs_html_attr("class", "snac-pubdate"),
  350. date_text);
  351. }
  352. else if (user && url) {
  353. xs *lpost_url = xs_fmt("%s/admin?q=%s",
  354. user->actor, xs_url_enc(url));
  355. date_text = xs_html_tag("a",
  356. xs_html_attr("href", lpost_url),
  357. xs_html_attr("class", "snac-pubdate"),
  358. date_text);
  359. }
  360. xs_html_add(actor_icon,
  361. xs_html_text(" "),
  362. xs_html_tag("time",
  363. xs_html_attr("class", "dt-published snac-pubdate"),
  364. xs_html_attr("title", date_title),
  365. date_text));
  366. }
  367. {
  368. const char *username, *id;
  369. if (xs_is_null(username = xs_dict_get(actor, "preferredUsername")) || *username == '\0') {
  370. /* This should never be reached */
  371. username = "anonymous";
  372. }
  373. if (xs_is_null(id = xs_dict_get(actor, "id")) || *id == '\0') {
  374. /* This should never be reached */
  375. id = "https://social.example.org/anonymous";
  376. }
  377. /* "LIKE AN ANIMAL" */
  378. xs *domain = xs_split(id, "/");
  379. xs *user = xs_fmt("@%s@%s", username, xs_list_get(domain, 2));
  380. xs_html_add(actor_icon,
  381. xs_html_sctag("br", NULL),
  382. xs_html_tag("a",
  383. xs_html_attr("href", html_url),
  384. xs_html_attr("class", "p-author-tag h-card snac-author-tag"),
  385. xs_html_text(user)));
  386. }
  387. return actor_icon;
  388. }
  389. xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg,
  390. const char *proxy, const char *md5, const char *lang)
  391. {
  392. xs *actor = NULL;
  393. xs_html *actor_icon = NULL;
  394. if (actor_id && valid_status(actor_get_refresh(user, actor_id, &actor))) {
  395. const char *date = NULL;
  396. const char *udate = NULL;
  397. const char *url = NULL;
  398. const char *type = xs_dict_get(msg, "type");
  399. const int scope = get_msg_visibility(msg);
  400. if (xs_match(type, POSTLIKE_OBJECT_TYPE)) {
  401. url = xs_dict_get(msg, "url");
  402. if (xs_is_list(url))
  403. url = xs_list_get(url, 0);
  404. if (!xs_is_string(url))
  405. url = xs_dict_get(msg, "id");
  406. }
  407. date = xs_dict_get(msg, "published");
  408. udate = xs_dict_get(msg, "updated");
  409. actor_icon = html_actor_icon(user, actor, date, udate, url, scope, 0, proxy, lang, md5);
  410. }
  411. return actor_icon;
  412. }
  413. void html_note_render_visibility(snac* user, xs_html *form, const int scope)
  414. {
  415. // scopes aren't sorted by value unfortunately, so simple math won't work to limit them. using map-like thing here
  416. static const int public_scopes[5] = {SCOPE_PUBLIC, SCOPE_UNLISTED, SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1};
  417. static const int unlisted_scopes[4] = {SCOPE_UNLISTED, SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1};
  418. static const int followers_scopes[3] = {SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1};
  419. static const int mentioned_scopes[2] = {SCOPE_MENTIONED, -1};
  420. static const int * const scopes[4] = { public_scopes, mentioned_scopes, unlisted_scopes, followers_scopes};
  421. static const char * const scopes_tags[4] = { "public", "mentioned", "unlisted", "followers"};
  422. static const char * const scopes_names[4] = { "Public", "Direct Message", "Unlisted", "Followers-only"};
  423. xs_html *paragraph = xs_html_tag("p", xs_html_text(L("Visibility: ")));
  424. const int* to_render = scopes[scope];
  425. for( int i = 0; to_render[i] != -1; i++ ) {
  426. const int scope_i = to_render[i];
  427. const char* value = scopes_tags[scope_i];
  428. const char* name = scopes_names[scope_i];
  429. xs_html_add(paragraph,
  430. xs_html_tag("label",
  431. xs_html_sctag("input",
  432. xs_html_attr("type", "radio"),
  433. xs_html_attr("name", "visibility"),
  434. xs_html_attr("value", value),
  435. xs_html_attr(scope == scope_i ? "checked" : "", NULL)),
  436. xs_html_text(" "),
  437. xs_html_text(L(name)),
  438. xs_html_text(" "))
  439. );
  440. }
  441. xs_html_add(form, paragraph);
  442. }
  443. /* html_note but moddled for emoji's needs. here and not bellow, since the
  444. * other one is already so complex. */
  445. xs_html *html_emoji(snac *user, const char *summary,
  446. const char *div_id, const char *form_id,
  447. const char* placeholder, const char *post_id,
  448. const char* eid)
  449. {
  450. xs *action = xs_fmt("%s/admin/action", user->actor);
  451. xs_html *form;
  452. const int react = eid == NULL ? 0 : 1;
  453. xs_html *note = xs_html_tag("div",
  454. xs_html_tag("details",
  455. xs_html_tag("summary",
  456. xs_html_text(summary)),
  457. xs_html_tag("p", NULL),
  458. xs_html_tag("div",
  459. xs_html_attr("class", "snac-note"),
  460. xs_html_attr("id", div_id),
  461. form = xs_html_tag("form",
  462. xs_html_attr("autocomplete", "off"),
  463. xs_html_attr("method", "post"),
  464. xs_html_attr("action", action),
  465. xs_html_attr("enctype", "multipart/form-data"),
  466. xs_html_attr("id", form_id),
  467. xs_html_sctag("input",
  468. xs_html_attr("type", "hidden"),
  469. xs_html_attr("name", "id"),
  470. xs_html_attr("value", post_id)),
  471. xs_html_sctag("input",
  472. xs_html_attr("type", react ? "hidden" : "text"),
  473. xs_html_attr("name", "eid"),
  474. xs_html_attr(react ? "value" : "placeholder", react ? eid : placeholder)),
  475. xs_html_text(" "),
  476. xs_html_sctag("input",
  477. xs_html_attr("type", "submit"),
  478. xs_html_attr("name", "action"),
  479. xs_html_attr("eid", "action"),
  480. xs_html_attr("value", react ? L("EmojiUnreact") : L("EmojiReact"))),
  481. xs_html_text(" "),
  482. xs_html_tag("p", NULL)))));
  483. return note;
  484. }
  485. xs_html *html_note(snac *user, const char *summary,
  486. const char *div_id, const char *form_id,
  487. const char *ta_plh, const char *ta_content,
  488. const char *edit_id, const char *actor_id,
  489. const xs_val *cw_yn, const char *cw_text,
  490. const int scope, const char *redir,
  491. const char *in_reply_to, int poll,
  492. const xs_list *att_files, const xs_list *att_alt_texts,
  493. int is_draft, const char *published,
  494. const char *note_lang)
  495. /* Yes, this is a FUCKTON of arguments and I'm a bit embarrased */
  496. {
  497. xs *action = xs_fmt("%s/admin/note", user->actor);
  498. xs_html *form;
  499. xs_html *note = xs_html_tag("div",
  500. xs_html_tag("details",
  501. xs_html_tag("summary",
  502. xs_html_text(summary)),
  503. xs_html_tag("p", NULL),
  504. xs_html_tag("div",
  505. xs_html_attr("class", "snac-note"),
  506. xs_html_attr("id", div_id),
  507. form = xs_html_tag("form",
  508. xs_html_attr("autocomplete", "off"),
  509. xs_html_attr("method", "post"),
  510. xs_html_attr("action", action),
  511. xs_html_attr("enctype", "multipart/form-data"),
  512. xs_html_attr("id", form_id),
  513. xs_html_tag("textarea",
  514. xs_html_attr("class", "snac-textarea"),
  515. xs_html_attr("name", "content"),
  516. xs_html_attr("rows", "4"),
  517. xs_html_attr("wrap", "virtual"),
  518. xs_html_attr("required", "required"),
  519. xs_html_attr("placeholder", ta_plh),
  520. xs_html_text(ta_content)),
  521. xs_html_tag("p", NULL),
  522. xs_html_text(L("Sensitive content: ")),
  523. xs_html_sctag("input",
  524. xs_html_attr("type", "checkbox"),
  525. xs_html_attr("name", "sensitive"),
  526. xs_html_attr(xs_type(cw_yn) == XSTYPE_TRUE ? "checked" : "", NULL)),
  527. xs_html_sctag("input",
  528. xs_html_attr("type", "text"),
  529. xs_html_attr("name", "summary"),
  530. xs_html_attr("placeholder", L("Sensitive content description")),
  531. xs_html_attr("value", xs_is_null(cw_text) ? "" : cw_text))))));
  532. if (actor_id)
  533. xs_html_add(form,
  534. xs_html_sctag("input",
  535. xs_html_attr("type", "hidden"),
  536. xs_html_attr("name", "to"),
  537. xs_html_attr("value", actor_id)));
  538. if (edit_id == NULL)
  539. html_note_render_visibility(user, form, scope);
  540. if (redir)
  541. xs_html_add(form,
  542. xs_html_sctag("input",
  543. xs_html_attr("type", "hidden"),
  544. xs_html_attr("name", "redir"),
  545. xs_html_attr("value", redir)));
  546. if (in_reply_to)
  547. xs_html_add(form,
  548. xs_html_sctag("input",
  549. xs_html_attr("type", "hidden"),
  550. xs_html_attr("name", "in_reply_to"),
  551. xs_html_attr("value", in_reply_to)));
  552. else
  553. xs_html_add(form,
  554. xs_html_tag("p", NULL),
  555. xs_html_text(L("Reply to (URL): ")),
  556. xs_html_sctag("input",
  557. xs_html_attr("type", "url"),
  558. xs_html_attr("name", "in_reply_to"),
  559. xs_html_attr("placeholder", L("Optional URL to reply to"))));
  560. xs_html_add(form,
  561. xs_html_tag("p", NULL),
  562. xs_html_tag("span",
  563. xs_html_attr("title", L("Don't send, but store as a draft")),
  564. xs_html_text(L("Draft:")),
  565. xs_html_sctag("input",
  566. xs_html_attr("type", "checkbox"),
  567. xs_html_attr("name", "is_draft"),
  568. xs_html_attr(is_draft ? "checked" : "", NULL))));
  569. const char *post_langs = xs_dict_get(user->config, "post_langs");
  570. if (xs_is_string(post_langs) && *post_langs) {
  571. xs *ll = xs_split(post_langs, " ");
  572. const char *lang;
  573. xs_html *post_lang = xs_html_tag("select",
  574. xs_html_attr("name", "post_lang"));
  575. xs_list_foreach(ll, lang) {
  576. if (*lang) {
  577. if (xs_is_string(note_lang) && strcmp(lang, note_lang) == 0) {
  578. xs_html_add(post_lang,
  579. xs_html_tag("option",
  580. xs_html_text(lang),
  581. xs_html_attr("selected", NULL),
  582. xs_html_attr("value", lang)));
  583. }
  584. else {
  585. xs_html_add(post_lang,
  586. xs_html_tag("option",
  587. xs_html_text(lang),
  588. xs_html_attr("value", lang)));
  589. }
  590. }
  591. }
  592. xs_html_add(form,
  593. xs_html_tag("p", NULL),
  594. xs_html_text(L("Language:")),
  595. xs_html_text(" "),
  596. post_lang);
  597. }
  598. /* post date and time */
  599. xs *post_date = NULL;
  600. xs *post_time = NULL;
  601. if (xs_is_string(published)) {
  602. time_t t = xs_parse_iso_date(published, 0);
  603. if (t > 0) {
  604. post_date = xs_str_time(t, "%Y-%m-%d", 1);
  605. post_time = xs_str_time(t, "%H:%M:%S", 1);
  606. }
  607. }
  608. if (edit_id == NULL || is_draft || is_scheduled(user, edit_id)) {
  609. xs *pdat = xs_fmt(L("Post date and time (timezone: %s):"), user->tz);
  610. xs_html_add(form,
  611. xs_html_tag("p",
  612. xs_html_tag("details",
  613. xs_html_tag("summary",
  614. xs_html_text(L("Scheduled post..."))),
  615. xs_html_tag("p",
  616. xs_html_text(pdat),
  617. xs_html_sctag("br", NULL),
  618. xs_html_sctag("input",
  619. xs_html_attr("type", "date"),
  620. xs_html_attr("value", post_date ? post_date : ""),
  621. xs_html_attr("name", "post_date")),
  622. xs_html_text(" "),
  623. xs_html_sctag("input",
  624. xs_html_attr("type", "time"),
  625. xs_html_attr("value", post_time ? post_time : ""),
  626. xs_html_attr("step", "1"),
  627. xs_html_attr("name", "post_time"))))));
  628. }
  629. if (edit_id)
  630. xs_html_add(form,
  631. xs_html_sctag("input",
  632. xs_html_attr("type", "hidden"),
  633. xs_html_attr("name", "edit_id"),
  634. xs_html_attr("value", edit_id)));
  635. /* attachment controls */
  636. xs_html *att;
  637. xs_html_add(form,
  638. xs_html_tag("p", NULL),
  639. att = xs_html_tag("details",
  640. xs_html_tag("summary",
  641. xs_html_text(L("Attachments..."))),
  642. xs_html_tag("p", NULL)));
  643. int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4"));
  644. int att_n = 0;
  645. /* fields for the currently existing attachments */
  646. if (xs_is_list(att_files) && xs_is_list(att_alt_texts)) {
  647. while (att_n < max_attachments) {
  648. const char *att_file = xs_list_get(att_files, att_n);
  649. const char *att_alt_text = xs_list_get(att_alt_texts, att_n);
  650. if (!xs_is_string(att_file) || !xs_is_string(att_alt_text))
  651. break;
  652. xs *att_lbl = xs_fmt("attach_url_%d", att_n);
  653. xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
  654. if (att_n)
  655. xs_html_add(att,
  656. xs_html_sctag("br", NULL));
  657. xs_html_add(att,
  658. xs_html_text(L("File:")),
  659. xs_html_sctag("input",
  660. xs_html_attr("type", "text"),
  661. xs_html_attr("name", att_lbl),
  662. xs_html_attr("title", L("Clear this field to delete the attachment")),
  663. xs_html_attr("value", att_file)));
  664. xs_html_add(att,
  665. xs_html_text(" "),
  666. xs_html_sctag("input",
  667. xs_html_attr("type", "text"),
  668. xs_html_attr("name", alt_lbl),
  669. xs_html_attr("value", att_alt_text),
  670. xs_html_attr("placeholder", L("Attachment description"))));
  671. att_n++;
  672. }
  673. }
  674. /* the rest of possible attachments */
  675. while (att_n < max_attachments) {
  676. xs *att_lbl = xs_fmt("attach_%d", att_n);
  677. xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
  678. if (att_n)
  679. xs_html_add(att,
  680. xs_html_sctag("br", NULL));
  681. xs_html_add(att,
  682. xs_html_sctag("input",
  683. xs_html_attr("type", "file"),
  684. xs_html_attr("name", att_lbl)));
  685. xs_html_add(att,
  686. xs_html_text(" "),
  687. xs_html_sctag("input",
  688. xs_html_attr("type", "text"),
  689. xs_html_attr("name", alt_lbl),
  690. xs_html_attr("placeholder", L("Attachment description"))));
  691. att_n++;
  692. }
  693. /* add poll controls */
  694. if (poll) {
  695. const xs_number *max_options = xs_dict_get(srv_config, "max_poll_options");
  696. xs *poll_limit_str = xs_dup(L("Poll options (one per line, up to 8):"));
  697. if (max_options != NULL)
  698. poll_limit_str = xs_replace_i(poll_limit_str, "8", xs_number_str(max_options));
  699. xs_html_add(form,
  700. xs_html_tag("p", NULL),
  701. xs_html_tag("details",
  702. xs_html_tag("summary",
  703. xs_html_text(L("Poll..."))),
  704. xs_html_tag("p",
  705. xs_html_text(poll_limit_str),
  706. xs_html_sctag("br", NULL),
  707. xs_html_tag("textarea",
  708. xs_html_attr("class", "snac-textarea"),
  709. xs_html_attr("name", "poll_options"),
  710. xs_html_attr("rows", "4"),
  711. xs_html_attr("wrap", "virtual"),
  712. xs_html_attr("placeholder", L("Option 1...\nOption 2...\nOption 3...\n...")))),
  713. xs_html_tag("select",
  714. xs_html_attr("name", "poll_multiple"),
  715. xs_html_tag("option",
  716. xs_html_attr("value", "off"),
  717. xs_html_text(L("One choice"))),
  718. xs_html_tag("option",
  719. xs_html_attr("value", "on"),
  720. xs_html_text(L("Multiple choices")))),
  721. xs_html_text(" "),
  722. xs_html_tag("select",
  723. xs_html_attr("name", "poll_end_secs"),
  724. xs_html_tag("option",
  725. xs_html_attr("value", "300"),
  726. xs_html_text(L("End in 5 minutes"))),
  727. xs_html_tag("option",
  728. xs_html_attr("value", "3600"),
  729. xs_html_attr("selected", NULL),
  730. xs_html_text(L("End in 1 hour"))),
  731. xs_html_tag("option",
  732. xs_html_attr("value", "86400"),
  733. xs_html_text(L("End in 1 day"))),
  734. xs_html_tag("option",
  735. xs_html_attr("value", "259200"),
  736. xs_html_text(L("End in 3 days"))),
  737. xs_html_tag("option",
  738. xs_html_attr("value", "31536000"),
  739. xs_html_text(L("End in 1 year"))))));
  740. }
  741. xs_html_add(form,
  742. xs_html_tag("p", NULL),
  743. xs_html_sctag("input",
  744. xs_html_attr("type", "submit"),
  745. xs_html_attr("class", "button"),
  746. xs_html_attr("value", L("Post"))),
  747. xs_html_tag("p", NULL));
  748. return note;
  749. }
  750. static xs_html *html_base_head(void)
  751. {
  752. xs_html *head = xs_html_tag("head",
  753. xs_html_sctag("meta",
  754. xs_html_attr("name", "viewport"),
  755. xs_html_attr("content", "width=device-width, initial-scale=1")),
  756. xs_html_sctag("meta",
  757. xs_html_attr("name", "generator"),
  758. xs_html_attr("content", USER_AGENT)));
  759. /* add server CSS and favicon */
  760. xs *f = NULL;
  761. const char *favicon = xs_dict_get(srv_config, "favicon_url");
  762. if (xs_is_string(favicon))
  763. f = xs_dup(favicon);
  764. else
  765. f = xs_fmt("%s/favicon.ico", srv_baseurl);
  766. const xs_list *p = xs_dict_get(srv_config, "cssurls");
  767. const char *v;
  768. xs_list_foreach(p, v) {
  769. xs_html_add(head,
  770. xs_html_sctag("link",
  771. xs_html_attr("rel", "stylesheet"),
  772. xs_html_attr("type", "text/css"),
  773. xs_html_attr("href", v)));
  774. }
  775. xs_html_add(head,
  776. xs_html_sctag("link",
  777. xs_html_attr("rel", "icon"),
  778. xs_html_attr("type", "image/x-icon"),
  779. xs_html_attr("href", f)));
  780. return head;
  781. }
  782. xs_html *html_instance_head(void)
  783. {
  784. xs_html *head = html_base_head();
  785. {
  786. FILE *f;
  787. xs *g_css_fn = xs_fmt("%s/style.css", srv_basedir);
  788. if ((f = fopen(g_css_fn, "r")) != NULL) {
  789. xs *css = xs_readall(f);
  790. fclose(f);
  791. xs_html_add(head,
  792. xs_html_tag("style",
  793. xs_html_raw(css)));
  794. }
  795. }
  796. const char *host = xs_dict_get(srv_config, "host");
  797. const char *title = xs_dict_get(srv_config, "title");
  798. xs_html_add(head,
  799. xs_html_tag("title",
  800. xs_html_text(title && *title ? title : host)));
  801. return head;
  802. }
  803. static xs_html *html_instance_body(void)
  804. {
  805. const char *host = xs_dict_get(srv_config, "host");
  806. const char *sdesc = xs_dict_get(srv_config, "short_description");
  807. const char *sdescraw = xs_dict_get(srv_config, "short_description_raw");
  808. const char *email = xs_dict_get(srv_config, "admin_email");
  809. const char *acct = xs_dict_get(srv_config, "admin_account");
  810. /* for L() */
  811. const snac *user = NULL;
  812. xs *blurb = xs_replace(snac_blurb, "%host%", host);
  813. xs_html *dl;
  814. xs_html *body = xs_html_tag("body",
  815. xs_html_tag("div",
  816. xs_html_attr("class", "snac-instance-blurb"),
  817. xs_html_raw(blurb), /* pure html */
  818. dl = xs_html_tag("dl", NULL)));
  819. if (sdesc && *sdesc) {
  820. if (!xs_is_null(sdescraw) && xs_type(sdescraw) == XSTYPE_TRUE) {
  821. xs_html_add(dl,
  822. xs_html_tag("di",
  823. xs_html_tag("dt",
  824. xs_html_text(L("Site description"))),
  825. xs_html_tag("dd",
  826. xs_html_raw(sdesc))));
  827. } else {
  828. xs_html_add(dl,
  829. xs_html_tag("di",
  830. xs_html_tag("dt",
  831. xs_html_text(L("Site description"))),
  832. xs_html_tag("dd",
  833. xs_html_text(sdesc))));
  834. }
  835. }
  836. if (email && *email) {
  837. xs *mailto = xs_fmt("mailto:%s", email);
  838. xs_html_add(dl,
  839. xs_html_tag("di",
  840. xs_html_tag("dt",
  841. xs_html_text(L("Admin email"))),
  842. xs_html_tag("dd",
  843. xs_html_tag("a",
  844. xs_html_attr("href", mailto),
  845. xs_html_text(email)))));
  846. }
  847. if (acct && *acct) {
  848. xs *url = xs_fmt("%s/%s", srv_baseurl, acct);
  849. xs *handle = xs_fmt("@%s@%s", acct, host);
  850. xs_html_add(dl,
  851. xs_html_tag("di",
  852. xs_html_tag("dt",
  853. xs_html_text(L("Admin account"))),
  854. xs_html_tag("dd",
  855. xs_html_tag("a",
  856. xs_html_attr("href", url),
  857. xs_html_text(handle)))));
  858. }
  859. return body;
  860. }
  861. xs_html *html_user_head(snac *user, const char *desc, const char *url)
  862. {
  863. xs_html *head = html_base_head();
  864. /* add the user CSS */
  865. {
  866. xs *css = NULL;
  867. int size;
  868. /* try to open the user css */
  869. if (!valid_status(static_get(user, "style.css", &css, &size, NULL, NULL))) {
  870. /* it's not there; try to open the server-wide css */
  871. FILE *f;
  872. xs *g_css_fn = xs_fmt("%s/style.css", srv_basedir);
  873. if ((f = fopen(g_css_fn, "r")) != NULL) {
  874. css = xs_readall(f);
  875. fclose(f);
  876. }
  877. }
  878. if (css != NULL) {
  879. xs_html_add(head,
  880. xs_html_tag("style",
  881. xs_html_raw(css)));
  882. }
  883. }
  884. /* title */
  885. xs *title = xs_fmt("%s (@%s@%s)", xs_dict_get(user->config, "name"),
  886. user->uid, xs_dict_get(srv_config, "host"));
  887. xs_html_add(head,
  888. xs_html_tag("title",
  889. xs_html_text(title)));
  890. xs *avatar = xs_dup(xs_dict_get(user->config, "avatar"));
  891. if (avatar == NULL || *avatar == '\0') {
  892. xs_free(avatar);
  893. avatar = xs_fmt("%s/susie.png", srv_baseurl);
  894. }
  895. /* create a description field */
  896. xs *s_desc = NULL;
  897. int n;
  898. if (desc == NULL)
  899. s_desc = xs_dup(xs_dict_get(user->config, "bio"));
  900. else
  901. s_desc = xs_dup(desc);
  902. /* show metrics in og:description? */
  903. if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
  904. xs *s1 = xs_fmt(L("%d following, %d followers"), following_list_len(user), follower_list_len(user));
  905. s1 = xs_str_cat(s1, " · ");
  906. s_desc = xs_str_prepend_i(s_desc, s1);
  907. }
  908. /* shorten desc to a reasonable size */
  909. for (n = 0; s_desc[n]; n++) {
  910. if (n > 512 && (s_desc[n] == ' ' || s_desc[n] == '\n'))
  911. break;
  912. }
  913. s_desc[n] = '\0';
  914. /* og properties */
  915. xs_html_add(head,
  916. xs_html_sctag("meta",
  917. xs_html_attr("property", "og:site_name"),
  918. xs_html_attr("content", xs_dict_get(srv_config, "host"))),
  919. xs_html_sctag("meta",
  920. xs_html_attr("property", "og:title"),
  921. xs_html_attr("content", title)),
  922. xs_html_sctag("meta",
  923. xs_html_attr("property", "og:description"),
  924. xs_html_attr("content", s_desc)),
  925. xs_html_sctag("meta",
  926. xs_html_attr("property", "og:image"),
  927. xs_html_attr("content", avatar)),
  928. xs_html_sctag("meta",
  929. xs_html_attr("property", "og:width"),
  930. xs_html_attr("content", "300")),
  931. xs_html_sctag("meta",
  932. xs_html_attr("property", "og:height"),
  933. xs_html_attr("content", "300")));
  934. /* RSS link */
  935. xs *rss_url = xs_fmt("%s.rss", user->actor);
  936. xs_html_add(head,
  937. xs_html_sctag("link",
  938. xs_html_attr("rel", "alternate"),
  939. xs_html_attr("type", "application/rss+xml"),
  940. xs_html_attr("title", "RSS"),
  941. xs_html_attr("href", rss_url)));
  942. /* ActivityPub alternate link (actor id) */
  943. xs_html_add(head,
  944. xs_html_sctag("link",
  945. xs_html_attr("rel", "alternate"),
  946. xs_html_attr("type", "application/activity+json"),
  947. xs_html_attr("href", url ? url : user->actor)));
  948. /* webmention hook */
  949. xs *wbh = xs_fmt("%s/webmention-hook", srv_baseurl);
  950. xs_html_add(head,
  951. xs_html_sctag("link",
  952. xs_html_attr("rel", "webmention"),
  953. xs_html_attr("href", wbh)));
  954. return head;
  955. }
  956. static xs_html *html_user_body(snac *user, int read_only)
  957. {
  958. const char *proxy = NULL;
  959. if (user && !read_only && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
  960. proxy = user->actor;
  961. xs_html *body = xs_html_tag("body", NULL);
  962. /* top nav */
  963. xs_html *top_nav = xs_html_tag("nav",
  964. xs_html_attr("class", "snac-top-nav"));
  965. xs *avatar = xs_dup(xs_dict_get(user->config, "avatar"));
  966. if (avatar == NULL || *avatar == '\0') {
  967. xs_free(avatar);
  968. avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64());
  969. }
  970. xs_html_add(top_nav,
  971. xs_html_sctag("img",
  972. xs_html_attr("src", avatar),
  973. xs_html_attr("class", "snac-avatar"),
  974. xs_html_attr("alt", "")));
  975. if (read_only) {
  976. xs *rss_url = xs_fmt("%s.rss", user->actor);
  977. xs *admin_url = xs_fmt("%s/admin", user->actor);
  978. xs_html_add(top_nav,
  979. xs_html_tag("a",
  980. xs_html_attr("href", rss_url),
  981. xs_html_text(L("RSS"))),
  982. xs_html_text(" - "),
  983. xs_html_tag("a",
  984. xs_html_attr("href", admin_url),
  985. xs_html_attr("rel", "nofollow"),
  986. xs_html_text(L("private"))));
  987. }
  988. else {
  989. int n_len = notify_new_num(user);
  990. int p_len = pending_count(user);
  991. xs_html *notify_count = NULL;
  992. xs_html *pending_follow_count = NULL;
  993. /* show the number of new notifications, if there are any */
  994. if (n_len) {
  995. xs *n_len_str = xs_fmt(" %d ", n_len);
  996. notify_count = xs_html_tag("sup",
  997. xs_html_attr("style", "background-color: red; color: white;"),
  998. xs_html_text(n_len_str));
  999. }
  1000. else
  1001. notify_count = xs_html_text("");
  1002. if (p_len) {
  1003. xs *s = xs_fmt(" %d ", p_len);
  1004. pending_follow_count = xs_html_tag("sup",
  1005. xs_html_attr("style", "background-color: red; color: white;"),
  1006. xs_html_text(s));
  1007. }
  1008. else
  1009. pending_follow_count = xs_html_text("");
  1010. xs *admin_url = xs_fmt("%s/admin", user->actor);
  1011. xs *notify_url = xs_fmt("%s/notifications", user->actor);
  1012. xs *people_url = xs_fmt("%s/people", user->actor);
  1013. xs *instance_url = xs_fmt("%s/instance", user->actor);
  1014. xs_html_add(top_nav,
  1015. xs_html_tag("a",
  1016. xs_html_attr("href", user->actor),
  1017. xs_html_text(L("public"))),
  1018. xs_html_text(" - "),
  1019. xs_html_tag("a",
  1020. xs_html_attr("href", admin_url),
  1021. xs_html_text(L("private"))),
  1022. xs_html_text(" - "),
  1023. xs_html_tag("a",
  1024. xs_html_attr("href", notify_url),
  1025. xs_html_text(L("notifications"))),
  1026. notify_count,
  1027. xs_html_text(" - "),
  1028. xs_html_tag("a",
  1029. xs_html_attr("href", people_url),
  1030. xs_html_text(L("people"))),
  1031. pending_follow_count,
  1032. xs_html_text(" - "),
  1033. xs_html_tag("a",
  1034. xs_html_attr("href", instance_url),
  1035. xs_html_text(L("instance"))),
  1036. xs_html_text(" "),
  1037. xs_html_tag("form",
  1038. xs_html_attr("style", "display: inline!important"),
  1039. xs_html_attr("class", "snac-search-box"),
  1040. xs_html_attr("action", admin_url),
  1041. xs_html_sctag("input",
  1042. xs_html_attr("type", "text"),
  1043. xs_html_attr("name", "q"),
  1044. xs_html_attr("title", L("Search posts by URL or content (regular expression), @user@host accounts, or #tag")),
  1045. xs_html_attr("placeholder", L("Content search")))));
  1046. }
  1047. xs_html_add(body,
  1048. top_nav);
  1049. /* user info */
  1050. xs_html *top_user = xs_html_tag("div",
  1051. xs_html_attr("class", "h-card snac-top-user"));
  1052. if (read_only) {
  1053. const char *header = xs_dict_get(user->config, "header");
  1054. if (header && *header) {
  1055. xs_html_add(top_user,
  1056. xs_html_tag("div",
  1057. xs_html_attr("class", "snac-top-user-banner"),
  1058. xs_html_attr("style", "clear: both"),
  1059. xs_html_sctag("br", NULL),
  1060. xs_html_sctag("img",
  1061. xs_html_attr("src", header))));
  1062. }
  1063. }
  1064. xs *handle = xs_fmt("@%s@%s",
  1065. xs_dict_get(user->config, "uid"),
  1066. xs_dict_get(srv_config, "host"));
  1067. xs *display_name = format_text_with_emoji(NULL, xs_dict_get(user->config, "name"), 1, proxy);
  1068. xs_html_add(top_user,
  1069. xs_html_tag("p",
  1070. xs_html_attr("class", "p-name snac-top-user-name"),
  1071. xs_html_raw(display_name)),
  1072. xs_html_tag("p",
  1073. xs_html_attr("class", "snac-top-user-id"),
  1074. xs_html_text(handle)));
  1075. /** instance announcement **/
  1076. if (!read_only) {
  1077. double la = 0.0;
  1078. xs *user_la = xs_dup(xs_dict_get(user->config, "last_announcement"));
  1079. if (user_la != NULL)
  1080. la = xs_number_get(user_la);
  1081. const t_announcement *an = announcement(la);
  1082. if (an != NULL && (an->text != NULL)) {
  1083. xs *s = xs_fmt("?da=%.0f", an->timestamp);
  1084. xs_html_add(top_user, xs_html_tag("div",
  1085. xs_html_attr("class", "snac-announcement"),
  1086. xs_html_text(an->text),
  1087. xs_html_text(" "),
  1088. xs_html_tag("a",
  1089. xs_html_attr("href", s),
  1090. xs_html_text("Dismiss"))));
  1091. }
  1092. }
  1093. if (read_only) {
  1094. xs *bio = format_text_with_emoji(user, xs_dict_get(user->config, "bio"), 2, proxy);
  1095. xs_html *top_user_bio = xs_html_tag("div",
  1096. xs_html_attr("class", "p-note snac-top-user-bio"),
  1097. xs_html_raw(bio)); /* already sanitized */
  1098. xs_html_add(top_user,
  1099. top_user_bio);
  1100. xs *metadata = NULL;
  1101. const xs_dict *md = xs_dict_get(user->config, "metadata");
  1102. if (xs_type(md) == XSTYPE_DICT)
  1103. metadata = xs_dup(md);
  1104. else
  1105. if (xs_type(md) == XSTYPE_STRING) {
  1106. /* convert to dict for easier iteration */
  1107. metadata = xs_dict_new();
  1108. xs *l = xs_split(md, "\n");
  1109. const char *ll;
  1110. xs_list_foreach(l, ll) {
  1111. xs *kv = xs_split_n(ll, "=", 1);
  1112. const char *k = xs_list_get(kv, 0);
  1113. const char *v = xs_list_get(kv, 1);
  1114. if (k && v) {
  1115. xs *kk = xs_strip_i(xs_dup(k));
  1116. xs *vv = xs_strip_i(xs_dup(v));
  1117. metadata = xs_dict_set(metadata, kk, vv);
  1118. }
  1119. }
  1120. }
  1121. if (xs_type(metadata) == XSTYPE_DICT) {
  1122. const xs_str *k;
  1123. const xs_str *v;
  1124. xs_dict *val_links = user->links;
  1125. if (xs_is_null(val_links))
  1126. val_links = xs_stock(XSTYPE_DICT);
  1127. xs_html *snac_metadata = xs_html_tag("div",
  1128. xs_html_attr("class", "snac-metadata"));
  1129. int c = 0;
  1130. while (xs_dict_next(metadata, &k, &v, &c)) {
  1131. xs_html *value;
  1132. if (xs_startswith(v, "https:/") || xs_startswith(v, "http:/") || *v == '@') {
  1133. /* is this link validated? */
  1134. xs *verified_link = NULL;
  1135. const xs_number *val_time = xs_dict_get(val_links, v);
  1136. const char *url = NULL;
  1137. if (xs_is_string(val_time)) {
  1138. /* resolve again, as it may be an account handle */
  1139. url = val_time;
  1140. val_time = xs_dict_get(val_links, val_time);
  1141. }
  1142. if (xs_type(val_time) == XSTYPE_NUMBER) {
  1143. time_t t = xs_number_get(val_time);
  1144. if (t > 0) {
  1145. xs *s1 = xs_str_utctime(t, ISO_DATE_SPEC);
  1146. verified_link = xs_fmt("%s (%s)", L("verified link"), s1);
  1147. }
  1148. }
  1149. if (!xs_is_null(verified_link)) {
  1150. value = xs_html_tag("span",
  1151. xs_html_attr("title", verified_link),
  1152. xs_html_raw("&#10004; "),
  1153. xs_html_tag("a",
  1154. xs_html_attr("rel", "me"),
  1155. xs_html_attr("target", "_blank"),
  1156. xs_html_attr("href", url ? url : v),
  1157. xs_html_text(v)));
  1158. }
  1159. else {
  1160. value = xs_html_tag("a",
  1161. xs_html_attr("rel", "me"),
  1162. xs_html_attr("href", url ? url : v),
  1163. xs_html_text(v));
  1164. }
  1165. }
  1166. else
  1167. if (xs_startswith(v, "gemini:/") || xs_startswith(v, "xmpp:")) {
  1168. value = xs_html_tag("a",
  1169. xs_html_attr("rel", "me"),
  1170. xs_html_attr("href", v),
  1171. xs_html_text(v));
  1172. }
  1173. else
  1174. value = xs_html_text(v);
  1175. xs_html_add(snac_metadata,
  1176. xs_html_tag("span",
  1177. xs_html_attr("class", "snac-property-name"),
  1178. xs_html_text(k)),
  1179. xs_html_text(":"),
  1180. xs_html_raw("&nbsp;"),
  1181. xs_html_tag("span",
  1182. xs_html_attr("class", "snac-property-value"),
  1183. value),
  1184. xs_html_sctag("br", NULL));
  1185. }
  1186. xs_html_add(top_user,
  1187. snac_metadata);
  1188. }
  1189. const char *latitude = xs_dict_get_def(user->config, "latitude", "");
  1190. const char *longitude = xs_dict_get_def(user->config, "longitude", "");
  1191. if (*latitude && *longitude) {
  1192. xs *label = xs_fmt("%s,%s", latitude, longitude);
  1193. xs *url = xs_fmt("https://openstreetmap.org/search?query=%s,%s",
  1194. latitude, longitude);
  1195. xs_html_add(top_user,
  1196. xs_html_tag("p",
  1197. xs_html_text(L("Location: ")),
  1198. xs_html_tag("a",
  1199. xs_html_attr("href", url),
  1200. xs_html_attr("target", "_blank"),
  1201. xs_html_text(label))));
  1202. }
  1203. if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
  1204. xs *s1 = xs_fmt(L("%d following, %d followers"), following_list_len(user), follower_list_len(user));
  1205. xs_html_add(top_user,
  1206. xs_html_tag("p",
  1207. xs_html_text(s1)));
  1208. }
  1209. }
  1210. xs_html_add(body,
  1211. top_user);
  1212. return body;
  1213. }
  1214. xs_html *html_checkbox(const char *form_name, const char *label, int flag)
  1215. /* helper for checkbox rendering */
  1216. {
  1217. return xs_html_tag("p",
  1218. xs_html_sctag("input",
  1219. xs_html_attr("type", "checkbox"),
  1220. xs_html_attr("name", form_name),
  1221. xs_html_attr("id", form_name),
  1222. xs_html_attr(flag ? "checked" : "", NULL)),
  1223. xs_html_tag("label",
  1224. xs_html_attr("for", form_name),
  1225. xs_html_text(label)));
  1226. }
  1227. xs_html *html_top_controls(snac *user)
  1228. /* generates the top controls */
  1229. {
  1230. xs *ops_action = xs_fmt("%s/admin/action", user->actor);
  1231. xs_html *top_controls = xs_html_tag("div",
  1232. xs_html_attr("class", "snac-top-controls"),
  1233. /** new post **/
  1234. html_note(user, L("New Post..."),
  1235. "new_post_div", "new_post_form",
  1236. L("What's on your mind?"), "",
  1237. NULL, NULL,
  1238. xs_stock(XSTYPE_FALSE), "",
  1239. SCOPE_PUBLIC, NULL,
  1240. NULL, 1, NULL, NULL, 0, NULL, NULL),
  1241. /** operations **/
  1242. xs_html_tag("details",
  1243. xs_html_tag("summary",
  1244. xs_html_text(L("Operations..."))),
  1245. xs_html_tag("p", NULL),
  1246. xs_html_tag("form",
  1247. xs_html_attr("autocomplete", "off"),
  1248. xs_html_attr("method", "post"),
  1249. xs_html_attr("action", ops_action),
  1250. xs_html_sctag("input",
  1251. xs_html_attr("type", "text"),
  1252. xs_html_attr("name", "actor"),
  1253. xs_html_attr("required", "required"),
  1254. xs_html_attr("placeholder", "bob@example.com")),
  1255. xs_html_text(" "),
  1256. xs_html_sctag("input",
  1257. xs_html_attr("type", "submit"),
  1258. xs_html_attr("name", "action"),
  1259. xs_html_attr("value", L("Follow"))),
  1260. xs_html_text(" "),
  1261. xs_html_text(L("(by URL or user@host)"))),
  1262. xs_html_tag("p", NULL),
  1263. xs_html_tag("form",
  1264. xs_html_attr("autocomplete", "off"),
  1265. xs_html_attr("method", "post"),
  1266. xs_html_attr("action", ops_action),
  1267. xs_html_sctag("input",
  1268. xs_html_attr("type", "url"),
  1269. xs_html_attr("name", "id"),
  1270. xs_html_attr("required", "required"),
  1271. xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
  1272. xs_html_text(" "),
  1273. xs_html_sctag("input",
  1274. xs_html_attr("type", "submit"),
  1275. xs_html_attr("name", "action"),
  1276. xs_html_attr("value", L("Boost"))),
  1277. xs_html_text(" "),
  1278. xs_html_text(L("(by URL)"))),
  1279. xs_html_tag("p", NULL),
  1280. xs_html_tag("form",
  1281. xs_html_attr("autocomplete", "off"),
  1282. xs_html_attr("method", "post"),
  1283. xs_html_attr("action", ops_action),
  1284. xs_html_sctag("input",
  1285. xs_html_attr("type", "text"),
  1286. xs_html_attr("name", "id"),
  1287. xs_html_attr("required", "required"),
  1288. xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
  1289. xs_html_text(" "),
  1290. xs_html_sctag("input",
  1291. xs_html_attr("type", "submit"),
  1292. xs_html_attr("name", "action"),
  1293. xs_html_attr("value", L("Like"))),
  1294. xs_html_text(" "),
  1295. xs_html_text(L("(by URL)"))),
  1296. xs_html_tag("form",
  1297. xs_html_attr("autocomplete", "off"),
  1298. xs_html_attr("method", "post"),
  1299. xs_html_attr("action", ops_action),
  1300. xs_html_sctag("input",
  1301. xs_html_attr("type", "text"),
  1302. xs_html_attr("name", "eid"),
  1303. xs_html_attr("required", "required"),
  1304. xs_html_attr("placeholder", ":neocat:")),
  1305. xs_html_text(" "),
  1306. xs_html_sctag("input",
  1307. xs_html_attr("type", "text"),
  1308. xs_html_attr("name", "id"),
  1309. xs_html_attr("required", "required"),
  1310. xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
  1311. xs_html_text(" "),
  1312. xs_html_sctag("input",
  1313. xs_html_attr("type", "submit"),
  1314. xs_html_attr("name", "action"),
  1315. xs_html_attr("value", L("EmojiReact"))),
  1316. xs_html_text(" "),
  1317. xs_html_text(L("(by URL)"))),
  1318. xs_html_tag("p", NULL)));
  1319. /** user settings **/
  1320. const char *email = "[disabled by admin]";
  1321. if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) {
  1322. email = xs_dict_get(user->config_o, "email");
  1323. if (xs_is_null(email)) {
  1324. email = xs_dict_get(user->config, "email");
  1325. if (xs_is_null(email))
  1326. email = "";
  1327. }
  1328. }
  1329. const char *cw = xs_dict_get(user->config, "cw");
  1330. if (xs_is_null(cw))
  1331. cw = "";
  1332. const char *telegram_bot = xs_dict_get(user->config, "telegram_bot");
  1333. if (xs_is_null(telegram_bot))
  1334. telegram_bot = "";
  1335. const char *telegram_chat_id = xs_dict_get(user->config, "telegram_chat_id");
  1336. if (xs_is_null(telegram_chat_id))
  1337. telegram_chat_id = "";
  1338. const char *ntfy_server = xs_dict_get(user->config, "ntfy_server");
  1339. if (xs_is_null(ntfy_server))
  1340. ntfy_server = "";
  1341. const char *ntfy_token = xs_dict_get(user->config, "ntfy_token");
  1342. if (xs_is_null(ntfy_token))
  1343. ntfy_token = "";
  1344. const char *purge_days = xs_dict_get(user->config, "purge_days");
  1345. if (!xs_is_null(purge_days) && xs_type(purge_days) == XSTYPE_NUMBER)
  1346. purge_days = (char *)xs_number_str(purge_days);
  1347. else
  1348. purge_days = "0";
  1349. const xs_val *d_dm_f_u = xs_dict_get(user->config, "drop_dm_from_unknown");
  1350. const xs_val *bot = xs_dict_get(user->config, "bot");
  1351. const xs_val *a_private = xs_dict_get(user->config, "private");
  1352. const xs_val *auto_boost = xs_dict_get(user->config, "auto_boost");
  1353. const xs_val *coll_thrds = xs_dict_get(user->config, "collapse_threads");
  1354. const xs_val *pending = xs_dict_get(user->config, "approve_followers");
  1355. const xs_val *show_foll = xs_dict_get(user->config, "show_contact_metrics");
  1356. const xs_val *show_unlisted = xs_dict_get(user->config, "show_unlisted");
  1357. const char *latitude = xs_dict_get_def(user->config, "latitude", "");
  1358. const char *longitude = xs_dict_get_def(user->config, "longitude", "");
  1359. const char *webhook = xs_dict_get_def(user->config, "notify_webhook", "");
  1360. const char *post_langs = xs_dict_get_def(user->config, "post_langs", "");
  1361. const char *excluded_langs = xs_dict_get_def(user->config, "excluded_langs", "");
  1362. xs *metadata = NULL;
  1363. const xs_dict *md = xs_dict_get(user->config, "metadata");
  1364. if (xs_type(md) == XSTYPE_DICT) {
  1365. const xs_str *k;
  1366. const xs_str *v;
  1367. metadata = xs_str_new(NULL);
  1368. xs_dict_foreach(md, k, v) {
  1369. xs *kp = xs_fmt("%s=%s", k, v);
  1370. if (*metadata)
  1371. metadata = xs_str_cat(metadata, "\n");
  1372. metadata = xs_str_cat(metadata, kp);
  1373. }
  1374. }
  1375. else
  1376. if (xs_type(md) == XSTYPE_STRING)
  1377. metadata = xs_dup(md);
  1378. else
  1379. metadata = xs_str_new(NULL);
  1380. /* ui language */
  1381. xs_html *lang_select = xs_html_tag("select",
  1382. xs_html_attr("name", "web_ui_lang"));
  1383. const char *u_lang = xs_dict_get_def(user->config, "lang", "en");
  1384. const char *lang;
  1385. const xs_dict *langs;
  1386. xs_dict_foreach(srv_langs, lang, langs) {
  1387. if (strcmp(u_lang, lang) == 0)
  1388. xs_html_add(lang_select,
  1389. xs_html_tag("option",
  1390. xs_html_text(lang),
  1391. xs_html_attr("value", lang),
  1392. xs_html_attr("selected", "selected")));
  1393. else
  1394. xs_html_add(lang_select,
  1395. xs_html_tag("option",
  1396. xs_html_text(lang),
  1397. xs_html_attr("value", lang)));
  1398. }
  1399. /* timezone */
  1400. xs_html *tz_select = xs_html_tag("select",
  1401. xs_html_attr("name", "tz"));
  1402. xs *tzs = xs_tz_list();
  1403. const char *tz;
  1404. xs_list_foreach(tzs, tz) {
  1405. if (strcmp(tz, user->tz) == 0)
  1406. xs_html_add(tz_select,
  1407. xs_html_tag("option",
  1408. xs_html_text(tz),
  1409. xs_html_attr("value", tz),
  1410. xs_html_attr("selected", "selected")));
  1411. else
  1412. xs_html_add(tz_select,
  1413. xs_html_tag("option",
  1414. xs_html_text(tz),
  1415. xs_html_attr("value", tz)));
  1416. }
  1417. xs *user_setup_action = xs_fmt("%s/admin/user-setup", user->actor);
  1418. xs_html_add(top_controls,
  1419. xs_html_tag("details",
  1420. xs_html_tag("summary",
  1421. xs_html_text(L("User Settings..."))),
  1422. xs_html_tag("div",
  1423. xs_html_attr("class", "snac-user-setup"),
  1424. xs_html_tag("form",
  1425. xs_html_attr("autocomplete", "off"),
  1426. xs_html_attr("method", "post"),
  1427. xs_html_attr("action", user_setup_action),
  1428. xs_html_attr("enctype", "multipart/form-data"),
  1429. xs_html_tag("p",
  1430. xs_html_text(L("Display name:")),
  1431. xs_html_sctag("br", NULL),
  1432. xs_html_sctag("input",
  1433. xs_html_attr("type", "text"),
  1434. xs_html_attr("name", "name"),
  1435. xs_html_attr("value", xs_dict_get(user->config, "name")),
  1436. xs_html_attr("placeholder", L("Your name")))),
  1437. xs_html_tag("p",
  1438. xs_html_text(L("Avatar: ")),
  1439. xs_html_sctag("input",
  1440. xs_html_attr("type", "file"),
  1441. xs_html_attr("name", "avatar_file"))),
  1442. xs_html_tag("p",
  1443. xs_html_sctag("input",
  1444. xs_html_attr("type", "checkbox"),
  1445. xs_html_attr("name", "avatar_delete")),
  1446. xs_html_text(L("Delete current avatar"))),
  1447. xs_html_tag("p",
  1448. xs_html_text(L("Header image (banner): ")),
  1449. xs_html_sctag("input",
  1450. xs_html_attr("type", "file"),
  1451. xs_html_attr("name", "header_file"))),
  1452. xs_html_tag("p",
  1453. xs_html_sctag("input",
  1454. xs_html_attr("type", "checkbox"),
  1455. xs_html_attr("name", "header_delete")),
  1456. xs_html_text(L("Delete current header image"))),
  1457. xs_html_tag("p",
  1458. xs_html_text(L("Bio:")),
  1459. xs_html_sctag("br", NULL),
  1460. xs_html_tag("textarea",
  1461. xs_html_attr("name", "bio"),
  1462. xs_html_attr("cols", "40"),
  1463. xs_html_attr("rows", "4"),
  1464. xs_html_attr("placeholder", L("Write about yourself here...")),
  1465. xs_html_text(xs_dict_get(user->config, "bio")))),
  1466. xs_html_sctag("input",
  1467. xs_html_attr("type", "checkbox"),
  1468. xs_html_attr("name", "cw"),
  1469. xs_html_attr("id", "cw"),
  1470. xs_html_attr(strcmp(cw, "open") == 0 ? "checked" : "", NULL)),
  1471. xs_html_tag("label",
  1472. xs_html_attr("for", "cw"),
  1473. xs_html_text(L("Always show sensitive content"))),
  1474. xs_html_tag("p",
  1475. xs_html_text(L("Email address for notifications:")),
  1476. xs_html_sctag("br", NULL),
  1477. xs_html_sctag("input",
  1478. xs_html_attr("type", "text"),
  1479. xs_html_attr("name", "email"),
  1480. xs_html_attr("value", email),
  1481. xs_html_attr("placeholder", "bob@example.com"))),
  1482. xs_html_tag("p",
  1483. xs_html_text(L("Telegram notifications (bot key and chat id):")),
  1484. xs_html_sctag("br", NULL),
  1485. xs_html_sctag("input",
  1486. xs_html_attr("type", "text"),
  1487. xs_html_attr("name", "telegram_bot"),
  1488. xs_html_attr("value", telegram_bot),
  1489. xs_html_attr("placeholder", L("Bot API key"))),
  1490. xs_html_text(" "),
  1491. xs_html_sctag("input",
  1492. xs_html_attr("type", "text"),
  1493. xs_html_attr("name", "telegram_chat_id"),
  1494. xs_html_attr("value", telegram_chat_id),
  1495. xs_html_attr("placeholder", L("Chat id")))),
  1496. xs_html_tag("p",
  1497. xs_html_text(L("ntfy notifications (ntfy server and token):")),
  1498. xs_html_sctag("br", NULL),
  1499. xs_html_sctag("input",
  1500. xs_html_attr("type", "text"),
  1501. xs_html_attr("name", "ntfy_server"),
  1502. xs_html_attr("value", ntfy_server),
  1503. xs_html_attr("placeholder", L("ntfy server - full URL (example: https://ntfy.sh/YourTopic)"))),
  1504. xs_html_text(" "),
  1505. xs_html_sctag("input",
  1506. xs_html_attr("type", "text"),
  1507. xs_html_attr("name", "ntfy_token"),
  1508. xs_html_attr("value", ntfy_token),
  1509. xs_html_attr("placeholder", L("ntfy token - if needed")))),
  1510. xs_html_tag("p",
  1511. xs_html_text(L("Notify webhook:")),
  1512. xs_html_sctag("br", NULL),
  1513. xs_html_sctag("input",
  1514. xs_html_attr("type", "url"),
  1515. xs_html_attr("name", "notify_webhook"),
  1516. xs_html_attr("value", webhook),
  1517. xs_html_attr("placeholder", L("http://example.com/webhook")))),
  1518. xs_html_tag("p",
  1519. xs_html_text(L("Maximum days to keep posts (0: server settings):")),
  1520. xs_html_sctag("br", NULL),
  1521. xs_html_sctag("input",
  1522. xs_html_attr("type", "number"),
  1523. xs_html_attr("name", "purge_days"),
  1524. xs_html_attr("value", purge_days))),
  1525. html_checkbox("drop_dm_from_unknown", L("Drop direct messages from people you don't follow"),
  1526. xs_is_true(d_dm_f_u)),
  1527. html_checkbox("bot", L("This account is a bot"),
  1528. xs_is_true(bot)),
  1529. html_checkbox("auto_boost", L("Auto-boost all mentions to this account"),
  1530. xs_is_true(auto_boost)),
  1531. html_checkbox("private", L("This account is private "
  1532. "(posts are not shown through the web)"),
  1533. xs_is_true(a_private)),
  1534. html_checkbox("collapse_threads", L("Collapse top threads by default"),
  1535. xs_is_true(coll_thrds)),
  1536. html_checkbox("approve_followers", L("Follow requests must be approved"),
  1537. xs_is_true(pending)),
  1538. html_checkbox("show_contact_metrics", L("Publish follower and following metrics"),
  1539. xs_is_true(show_foll)),
  1540. html_checkbox("show_unlisted", L("Show unlisted posts in the public page and RSS feed"),
  1541. xs_is_true(show_unlisted)),
  1542. xs_html_tag("p",
  1543. xs_html_text(L("Current location:")),
  1544. xs_html_sctag("br", NULL),
  1545. xs_html_sctag("input",
  1546. xs_html_attr("type", "text"),
  1547. xs_html_attr("name", "latitude"),
  1548. xs_html_attr("value", latitude),
  1549. xs_html_attr("placeholder", "latitude")),
  1550. xs_html_text(" "),
  1551. xs_html_sctag("input",
  1552. xs_html_attr("type", "text"),
  1553. xs_html_attr("name", "longitude"),
  1554. xs_html_attr("value", longitude),
  1555. xs_html_attr("placeholder", "longitude"))),
  1556. xs_html_tag("p",
  1557. xs_html_text(L("Profile metadata (key=value pairs in each line):")),
  1558. xs_html_sctag("br", NULL),
  1559. xs_html_tag("textarea",
  1560. xs_html_attr("name", "metadata"),
  1561. xs_html_attr("cols", "40"),
  1562. xs_html_attr("rows", "4"),
  1563. xs_html_attr("placeholder", "Blog=https:/"
  1564. "/example.com/my-blog\nGPG Key=1FA54\n..."),
  1565. xs_html_text(metadata))),
  1566. xs_html_tag("p",
  1567. xs_html_text(L("Web interface language:")),
  1568. xs_html_sctag("br", NULL),
  1569. lang_select),
  1570. xs_html_tag("p",
  1571. xs_html_text(L("Time zone:")),
  1572. xs_html_sctag("br", NULL),
  1573. tz_select),
  1574. xs_html_tag("p",
  1575. xs_html_text(L("Languages you usually post in:")),
  1576. xs_html_sctag("br", NULL),
  1577. xs_html_sctag("input",
  1578. xs_html_attr("type", "text"),
  1579. xs_html_attr("name", "post_langs"),
  1580. xs_html_attr("value", post_langs),
  1581. xs_html_attr("placeholder", L("en fr es de_AT")))),
  1582. xs_html_tag("p",
  1583. xs_html_text(L("Don't show posts written in these languages:")),
  1584. xs_html_sctag("br", NULL),
  1585. xs_html_sctag("input",
  1586. xs_html_attr("type", "text"),
  1587. xs_html_attr("name", "excluded_langs"),
  1588. xs_html_attr("value", excluded_langs),
  1589. xs_html_attr("placeholder", L("en fr es de_AT")))),
  1590. xs_html_tag("p",
  1591. xs_html_text(L("New password:")),
  1592. xs_html_sctag("br", NULL),
  1593. xs_html_sctag("input",
  1594. xs_html_attr("type", "password"),
  1595. xs_html_attr("name", "passwd1"),
  1596. xs_html_attr("value", ""))),
  1597. xs_html_tag("p",
  1598. xs_html_text(L("Repeat new password:")),
  1599. xs_html_sctag("br", NULL),
  1600. xs_html_sctag("input",
  1601. xs_html_attr("type", "password"),
  1602. xs_html_attr("name", "passwd2"),
  1603. xs_html_attr("value", ""))),
  1604. xs_html_sctag("input",
  1605. xs_html_attr("type", "submit"),
  1606. xs_html_attr("class", "button"),
  1607. xs_html_attr("value", L("Update user info"))),
  1608. xs_html_tag("p", NULL)))));
  1609. xs *followed_hashtags_action = xs_fmt("%s/admin/followed-hashtags", user->actor);
  1610. xs *followed_hashtags = xs_join(xs_dict_get_def(user->config,
  1611. "followed_hashtags", xs_stock(XSTYPE_LIST)), "\n");
  1612. xs_html_add(top_controls,
  1613. xs_html_tag("details",
  1614. xs_html_tag("summary",
  1615. xs_html_text(L("Followed hashtags..."))),
  1616. xs_html_tag("p",
  1617. xs_html_text(L("One hashtag per line"))),
  1618. xs_html_tag("div",
  1619. xs_html_attr("class", "snac-followed-hashtags"),
  1620. xs_html_tag("form",
  1621. xs_html_attr("autocomplete", "off"),
  1622. xs_html_attr("method", "post"),
  1623. xs_html_attr("action", followed_hashtags_action),
  1624. xs_html_attr("enctype", "multipart/form-data"),
  1625. xs_html_tag("textarea",
  1626. xs_html_attr("name", "followed_hashtags"),
  1627. xs_html_attr("cols", "40"),
  1628. xs_html_attr("rows", "4"),
  1629. xs_html_attr("placeholder", "#cats\n#windowfriday\n#classicalmusic\nhttps:/"
  1630. "/mastodon.social/tags/dogs"),
  1631. xs_html_text(followed_hashtags)),
  1632. xs_html_tag("br", NULL),
  1633. xs_html_sctag("input",
  1634. xs_html_attr("type", "submit"),
  1635. xs_html_attr("class", "button"),
  1636. xs_html_attr("value", L("Update hashtags")))))));
  1637. xs *blocked_hashtags_action = xs_fmt("%s/admin/blocked-hashtags", user->actor);
  1638. xs *blocked_hashtags = xs_join(xs_dict_get_def(user->config,
  1639. "blocked_hashtags", xs_stock(XSTYPE_LIST)), "\n");
  1640. xs_html_add(top_controls,
  1641. xs_html_tag("details",
  1642. xs_html_tag("summary",
  1643. xs_html_text(L("Blocked hashtags..."))),
  1644. xs_html_tag("p",
  1645. xs_html_text(L("One hashtag per line"))),
  1646. xs_html_tag("div",
  1647. xs_html_attr("class", "snac-blocked-hashtags"),
  1648. xs_html_tag("form",
  1649. xs_html_attr("autocomplete", "off"),
  1650. xs_html_attr("method", "post"),
  1651. xs_html_attr("action", blocked_hashtags_action),
  1652. xs_html_attr("enctype", "multipart/form-data"),
  1653. xs_html_tag("textarea",
  1654. xs_html_attr("name", "blocked_hashtags"),
  1655. xs_html_attr("cols", "40"),
  1656. xs_html_attr("rows", "4"),
  1657. xs_html_attr("placeholder", "#cats\n#windowfriday\n#classicalmusic"),
  1658. xs_html_text(blocked_hashtags)),
  1659. xs_html_tag("br", NULL),
  1660. xs_html_sctag("input",
  1661. xs_html_attr("type", "submit"),
  1662. xs_html_attr("class", "button"),
  1663. xs_html_attr("value", L("Update hashtags")))))));
  1664. xs *muted_words_action = xs_fmt("%s/admin/muted-words", user->actor);
  1665. xs *muted_words = xs_join(xs_dict_get_def(user->config,
  1666. "muted_words", xs_stock(XSTYPE_LIST)), "\n");
  1667. xs_html_add(top_controls,
  1668. xs_html_tag("details",
  1669. xs_html_tag("summary",
  1670. xs_html_text(L("Muted words..."))),
  1671. xs_html_tag("p",
  1672. xs_html_text(L("One word per line, partial matches count"))),
  1673. xs_html_tag("div",
  1674. xs_html_attr("class", "snac-muted-words"),
  1675. xs_html_tag("form",
  1676. xs_html_attr("autocomplete", "off"),
  1677. xs_html_attr("method", "post"),
  1678. xs_html_attr("action", muted_words_action),
  1679. xs_html_attr("enctype", "multipart/form-data"),
  1680. xs_html_tag("textarea",
  1681. xs_html_attr("name", "muted_words"),
  1682. xs_html_attr("cols", "40"),
  1683. xs_html_attr("rows", "4"),
  1684. xs_html_attr("placeholder", "nascar\nsuperbowl\nFIFA"),
  1685. xs_html_text(muted_words)),
  1686. xs_html_tag("br", NULL),
  1687. xs_html_sctag("input",
  1688. xs_html_attr("type", "submit"),
  1689. xs_html_attr("class", "button"),
  1690. xs_html_attr("value", L("Update muted words")))))));
  1691. return top_controls;
  1692. }
  1693. static xs_html *html_button(const char *clss, const char *label, const char *hint)
  1694. {
  1695. xs *c = xs_fmt("snac-btn-%s", clss);
  1696. /* use an NULL tag to separate non-css-classed buttons from one another */
  1697. return xs_html_container(
  1698. xs_html_sctag("input",
  1699. xs_html_attr("type", "submit"),
  1700. xs_html_attr("name", "action"),
  1701. xs_html_attr("class", c),
  1702. xs_html_attr("value", label),
  1703. xs_html_attr("title", hint)),
  1704. xs_html_text("\n"));
  1705. }
  1706. xs_str *build_mentions(snac *user, const xs_dict *msg)
  1707. /* returns a string with the mentions in msg */
  1708. {
  1709. xs_str *s = xs_str_new(NULL);
  1710. const char *list = xs_dict_get(msg, "tag");
  1711. const char *v;
  1712. int c = 0;
  1713. while (xs_list_next(list, &v, &c)) {
  1714. const char *type = xs_dict_get(v, "type");
  1715. const char *href = xs_dict_get(v, "href");
  1716. const char *name = xs_dict_get(v, "name");
  1717. if (type && strcmp(type, "Mention") == 0 &&
  1718. href && strcmp(href, user->actor) != 0 && name) {
  1719. xs *s1 = NULL;
  1720. if (name[0] != '@') {
  1721. s1 = xs_fmt("@%s", name);
  1722. name = s1;
  1723. }
  1724. xs *l = xs_split(name, "@");
  1725. /* is it a name without a host? */
  1726. if (xs_list_len(l) < 3) {
  1727. /* split the href and pick the host name LIKE AN ANIMAL */
  1728. /* would be better to query the webfinger but *won't do that* here */
  1729. xs *l2 = xs_split(href, "/");
  1730. if (xs_list_len(l2) >= 3) {
  1731. xs *s1 = xs_fmt("%s@%s ", name, xs_list_get(l2, 2));
  1732. if (xs_str_in(s, s1) == -1)
  1733. s = xs_str_cat(s, s1);
  1734. }
  1735. }
  1736. else {
  1737. if (xs_str_in(s, name) == -1) {
  1738. s = xs_str_cat(s, name);
  1739. s = xs_str_cat(s, " ");
  1740. }
  1741. }
  1742. }
  1743. }
  1744. #if 0
  1745. /* disabled by now */
  1746. /* also add the author of this post as a mention */
  1747. const char *atto = get_atto(msg);
  1748. if (xs_is_string(atto) && *atto && strcmp(atto, user->actor) != 0) {
  1749. /* check if this author is already in the mentions string */
  1750. xs *l = xs_split(atto, "/");
  1751. if (xs_list_len(l) > 3) {
  1752. xs *actor_o = NULL;
  1753. if (valid_status(object_get(atto, &actor_o))) {
  1754. const char *uname = xs_dict_get(actor_o, "preferredUsername");
  1755. if (!xs_is_null(uname) && *uname) {
  1756. xs *handle = xs_fmt("@%s@%s ", uname, xs_list_get(l, 2));
  1757. if (xs_str_in(s, handle) == -1) {
  1758. s = xs_str_cat(s, handle);
  1759. }
  1760. }
  1761. }
  1762. }
  1763. }
  1764. #endif
  1765. if (*s) {
  1766. xs *s1 = s;
  1767. s = xs_fmt("\n\n\nCC: %s", s1);
  1768. }
  1769. return s;
  1770. }
  1771. xs_html *html_entry_controls(snac *user, const char *actor,
  1772. const xs_dict *msg, const char *md5)
  1773. {
  1774. const char *id = xs_dict_get(msg, "id");
  1775. const char *group = xs_dict_get(msg, "audience");
  1776. xs *likes = object_likes(id);
  1777. xs *boosts = object_announces(id);
  1778. xs *action = xs_fmt("%s/admin/action", user->actor);
  1779. xs *redir = xs_fmt("%s_entry", md5);
  1780. xs_html *form;
  1781. xs_html *controls = xs_html_tag("div",
  1782. xs_html_attr("class", "snac-controls"),
  1783. form = xs_html_tag("form",
  1784. xs_html_attr("autocomplete", "off"),
  1785. xs_html_attr("method", "post"),
  1786. xs_html_attr("action", action),
  1787. xs_html_sctag("input",
  1788. xs_html_attr("type", "hidden"),
  1789. xs_html_attr("name", "id"),
  1790. xs_html_attr("value", id)),
  1791. xs_html_sctag("input",
  1792. xs_html_attr("type", "hidden"),
  1793. xs_html_attr("name", "actor"),
  1794. xs_html_attr("value", actor)),
  1795. xs_html_sctag("input",
  1796. xs_html_attr("type", "hidden"),
  1797. xs_html_attr("name", "group"),
  1798. xs_html_attr("value", xs_is_null(group) ? "" : group)),
  1799. xs_html_sctag("input",
  1800. xs_html_attr("type", "hidden"),
  1801. xs_html_attr("name", "redir"),
  1802. xs_html_attr("value", redir))));
  1803. if (!is_msg_mine(user, id)) {
  1804. if (xs_list_in(likes, user->md5) == -1) {
  1805. /* not already liked; add button */
  1806. xs_html_add(form,
  1807. html_button("like", L("Like"), L("Say you like this post")));
  1808. }
  1809. else {
  1810. /* not like it anymore */
  1811. xs_html_add(form,
  1812. html_button("unlike", L("Unlike"), L("Nah don't like it that much")));
  1813. }
  1814. }
  1815. else {
  1816. if (is_pinned(user, id))
  1817. xs_html_add(form,
  1818. html_button("unpin", L("Unpin"), L("Unpin this post from your timeline")));
  1819. else
  1820. xs_html_add(form,
  1821. html_button("pin", L("Pin"), L("Pin this post to the top of your timeline")));
  1822. }
  1823. if (is_msg_public(msg)) {
  1824. if (xs_list_in(boosts, user->md5) == -1) {
  1825. /* not already boosted; add button */
  1826. xs_html_add(form,
  1827. html_button("boost", L("Boost"), L("Announce this post to your followers")));
  1828. }
  1829. else {
  1830. /* already boosted; add button to regret */
  1831. xs_html_add(form,
  1832. html_button("unboost", L("Unboost"), L("I regret I boosted this")));
  1833. }
  1834. }
  1835. if (is_bookmarked(user, id))
  1836. xs_html_add(form,
  1837. html_button("unbookmark", L("Unbookmark"), L("Delete this post from your bookmarks")));
  1838. else
  1839. xs_html_add(form,
  1840. html_button("bookmark", L("Bookmark"), L("Add this post to your bookmarks")));
  1841. if (strcmp(actor, user->actor) != 0) {
  1842. /* controls for other actors than this one */
  1843. if (following_check(user, actor)) {
  1844. xs_html_add(form,
  1845. html_button("unfollow", L("Unfollow"), L("Stop following this user's activity")));
  1846. }
  1847. else {
  1848. xs_html_add(form,
  1849. html_button("follow", L("Follow"), L("Start following this user's activity")));
  1850. }
  1851. if (!xs_is_null(group)) {
  1852. if (following_check(user, group)) {
  1853. xs_html_add(form,
  1854. html_button("unfollow", L("Unfollow Group"),
  1855. L("Stop following this group or channel")));
  1856. }
  1857. else {
  1858. xs_html_add(form,
  1859. html_button("follow", L("Follow Group"),
  1860. L("Start following this group or channel")));
  1861. }
  1862. }
  1863. xs_html_add(form,
  1864. html_button("mute", L("MUTE"),
  1865. L("Block any activity from this user forever")));
  1866. }
  1867. if (!xs_is_true(xs_dict_get(srv_config, "hide_delete_post_button")))
  1868. xs_html_add(form,
  1869. html_button("delete", L("Delete"), L("Delete this post")));
  1870. xs_html_add(form,
  1871. html_button("hide", L("Hide"), L("Hide this post and its children")));
  1872. const char *prev_src = xs_dict_get(msg, "sourceContent");
  1873. if (!xs_is_null(prev_src) && strcmp(actor, user->actor) == 0) { /** edit **/
  1874. /* post can be edited */
  1875. xs *div_id = xs_fmt("%s_edit", md5);
  1876. xs *form_id = xs_fmt("%s_edit_form", md5);
  1877. xs *redir = xs_fmt("%s_entry", md5);
  1878. xs *att_files = xs_list_new();
  1879. xs *att_alt_texts = xs_list_new();
  1880. const xs_list *att_list = xs_dict_get(msg, "attachment");
  1881. if (xs_is_list(att_list)) {
  1882. const xs_dict *d;
  1883. xs_list_foreach(att_list, d) {
  1884. const char *att_file = xs_dict_get(d, "url");
  1885. const char *att_alt_text = xs_dict_get(d, "name");
  1886. if (xs_is_string(att_file) && xs_is_string(att_alt_text)) {
  1887. att_files = xs_list_append(att_files, att_file);
  1888. att_alt_texts = xs_list_append(att_alt_texts, att_alt_text);
  1889. }
  1890. }
  1891. }
  1892. const char *note_lang = NULL;
  1893. const xs_dict *cmap = xs_dict_get(msg, "contentMap");
  1894. if (xs_is_dict(cmap)) {
  1895. const char *dummy;
  1896. int c = 0;
  1897. xs_dict_next(cmap, &note_lang, &dummy, &c);
  1898. }
  1899. const int edit_scope = get_msg_visibility(msg);
  1900. xs_html_add(controls, xs_html_tag("div",
  1901. xs_html_tag("p", NULL),
  1902. html_note(user, L("Edit..."),
  1903. div_id, form_id,
  1904. "", prev_src,
  1905. id, NULL,
  1906. xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
  1907. edit_scope, redir,
  1908. NULL, 0, att_files, att_alt_texts, is_draft(user, id),
  1909. xs_dict_get(msg, "published"), note_lang)),
  1910. xs_html_tag("p", NULL));
  1911. }
  1912. if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) { /** emoji react **/
  1913. /* the post textarea */
  1914. xs *div_id = xs_fmt("%s_reply", md5);
  1915. xs *form_id = xs_fmt("%s_reply_form", md5);
  1916. xs *e_react = emoji_reacted(user, id);
  1917. xs_html_add(controls, xs_html_tag("div",
  1918. xs_html_tag("p", NULL),
  1919. html_emoji(
  1920. user, L("Emoji react..."),
  1921. div_id, form_id,
  1922. ":neocat:", id,
  1923. e_react)),
  1924. xs_html_tag("p", NULL));
  1925. }
  1926. { /** reply **/
  1927. /* the post textarea */
  1928. xs *ct = build_mentions(user, msg);
  1929. xs *div_id = xs_fmt("%s_reply", md5);
  1930. xs *form_id = xs_fmt("%s_reply_form", md5);
  1931. xs *redir = xs_fmt("%s_entry", md5);
  1932. const int scope = get_msg_visibility(msg);
  1933. xs_html_add(controls, xs_html_tag("div",
  1934. xs_html_tag("p", NULL),
  1935. html_note(user, L("Reply..."),
  1936. div_id, form_id,
  1937. "", ct,
  1938. NULL, NULL,
  1939. xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
  1940. scope, redir,
  1941. id, 0, NULL, NULL, 0, NULL, NULL)),
  1942. xs_html_tag("p", NULL));
  1943. }
  1944. return controls;
  1945. }
  1946. static const xs_str* words_in_content(const xs_list *words, const xs_val *content)
  1947. /* returns a word that matches any of the words in content */
  1948. {
  1949. if (!xs_is_list(words) || !xs_is_string(content)) {
  1950. return NULL;
  1951. }
  1952. xs *c = xs_split(content, " ");
  1953. xs *sc = xs_list_sort(c, NULL);
  1954. const xs_str *wv;
  1955. const xs_str *cv;
  1956. xs_list_foreach(words, wv) {
  1957. xs_list_foreach(sc, cv) {
  1958. xs_tolower_i((xs_str*)cv);
  1959. if(xs_str_in(cv, wv) != -1)
  1960. return wv;
  1961. }
  1962. }
  1963. return NULL;
  1964. }
  1965. xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
  1966. int level, const char *md5, int hide_children)
  1967. {
  1968. const char *id = xs_dict_get(msg, "id");
  1969. const char *type = xs_dict_get(msg, "type");
  1970. const char *actor;
  1971. const char *v;
  1972. int has_title = 0;
  1973. int collapse_threads = 0;
  1974. const char *proxy = NULL;
  1975. if (user && !read_only && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
  1976. proxy = user->actor;
  1977. /* do not show non-public messages in the public timeline */
  1978. if ((read_only || !user) && !is_msg_public(msg))
  1979. return NULL;
  1980. if (id && is_instance_blocked(id))
  1981. return NULL;
  1982. if (user && level == 0 && xs_is_true(xs_dict_get(user->config, "collapse_threads")))
  1983. collapse_threads = 1;
  1984. /* hidden? do nothing more for this conversation */
  1985. if (user && is_hidden(user, id)) {
  1986. xs *s1 = xs_fmt("%s_entry", md5);
  1987. /* return just an dummy anchor, to keep position after hitting 'Hide' */
  1988. return xs_html_tag("div",
  1989. xs_html_tag("a",
  1990. xs_html_attr("name", s1)));
  1991. }
  1992. /* avoid too deep nesting, as it may be a loop */
  1993. if (level >= MAX_CONVERSATION_LEVELS)
  1994. return xs_html_tag("mark",
  1995. xs_html_text(L("Truncated (too deep)")));
  1996. const char *lang = NULL;
  1997. const xs_dict *cmap = xs_dict_get(msg, "contentMap");
  1998. if (xs_is_dict(cmap)) {
  1999. const char *dummy;
  2000. int c = 0;
  2001. xs_dict_next(cmap, &lang, &dummy, &c);
  2002. if (user && xs_is_string(lang)) {
  2003. /* discard posts in excluded languages */
  2004. const char *excluded_langs = xs_dict_get(user->config, "excluded_langs");
  2005. if (xs_is_string(excluded_langs) &&
  2006. xs_str_in(excluded_langs, lang) != -1) {
  2007. snac_debug(user, 1, xs_fmt("excluded post in language '%s'", lang));
  2008. return NULL;
  2009. }
  2010. }
  2011. }
  2012. if (strcmp(type, "Follow") == 0) {
  2013. return xs_html_tag("div",
  2014. xs_html_attr("class", "snac-post"),
  2015. xs_html_tag("div",
  2016. xs_html_attr("class", "snac-post-header"),
  2017. xs_html_tag("div",
  2018. xs_html_attr("class", "snac-origin"),
  2019. xs_html_text(L("follows you"))),
  2020. html_msg_icon(read_only ? NULL : user, xs_dict_get(msg, "actor"), msg, proxy, NULL, lang)));
  2021. }
  2022. else
  2023. if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) {
  2024. /* skip oddities */
  2025. snac_debug(user, 1, xs_fmt("html_entry: ignoring object type '%s' %s", type, id));
  2026. return NULL;
  2027. }
  2028. /* ignore notes with "name", as they are votes to Questions */
  2029. if (strcmp(type, "Note") == 0 && !xs_is_null(xs_dict_get(msg, "name")))
  2030. return NULL;
  2031. /* get the attributedTo */
  2032. if ((actor = get_atto(msg)) == NULL)
  2033. return NULL;
  2034. /* ignore muted morons immediately */
  2035. if (user && is_muted(user, actor)) {
  2036. xs *s1 = xs_fmt("%s_entry", md5);
  2037. /* return just an dummy anchor, to keep position after hitting 'MUTE' */
  2038. return xs_html_tag("div",
  2039. xs_html_tag("a",
  2040. xs_html_attr("name", s1)));
  2041. }
  2042. /* don't show followers-only notes from not followed users */
  2043. if (user && get_msg_visibility(msg) == SCOPE_FOLLOWERS &&
  2044. strcmp(user->actor, actor) != 0 && following_check(user, actor) == 0) {
  2045. return NULL;
  2046. }
  2047. if ((user == NULL || strcmp(actor, user->actor) != 0)
  2048. && !valid_status(actor_get(actor, NULL))) {
  2049. if (user)
  2050. enqueue_actor_refresh(user, actor, 0);
  2051. return NULL;
  2052. }
  2053. /** html_entry top tag **/
  2054. xs_html *entry_top = xs_html_tag("div", NULL);
  2055. {
  2056. xs *s1 = xs_fmt("%s_entry", md5);
  2057. xs_html_add(entry_top,
  2058. xs_html_tag("a",
  2059. xs_html_attr("name", s1)));
  2060. }
  2061. xs_html *entry = xs_html_tag("div",
  2062. xs_html_attr("class", level == 0 ? "snac-post" : "snac-child"));
  2063. xs_html_add(entry_top,
  2064. entry);
  2065. /** post header **/
  2066. xs_html *score;
  2067. xs_html *post_header = xs_html_tag("div",
  2068. xs_html_attr("class", "snac-post-header"),
  2069. score = xs_html_tag("div",
  2070. xs_html_attr("class", "snac-score")));
  2071. xs_html_add(entry,
  2072. post_header);
  2073. if (user && is_pinned(user, id)) {
  2074. /* add a pin emoji */
  2075. xs_html_add(score,
  2076. xs_html_tag("span",
  2077. xs_html_attr("title", L("Pinned")),
  2078. xs_html_raw(" &#128204; ")));
  2079. }
  2080. if (user && !read_only && is_bookmarked(user, id)) {
  2081. /* add a bookmark emoji */
  2082. xs_html_add(score,
  2083. xs_html_tag("span",
  2084. xs_html_attr("title", L("Bookmarked")),
  2085. xs_html_raw(" &#128278; ")));
  2086. }
  2087. if (strcmp(type, "Question") == 0) {
  2088. /* add the ballot box emoji */
  2089. xs_html_add(score,
  2090. xs_html_tag("span",
  2091. xs_html_attr("title", L("Poll")),
  2092. xs_html_raw(" &#128499; ")));
  2093. if (user && was_question_voted(user, id)) {
  2094. /* add a check to show this poll was voted */
  2095. xs_html_add(score,
  2096. xs_html_tag("span",
  2097. xs_html_attr("title", L("Voted")),
  2098. xs_html_raw(" &#10003; ")));
  2099. }
  2100. }
  2101. if (strcmp(type, "Event") == 0) {
  2102. /* add the calendar emoji */
  2103. xs_html_add(score,
  2104. xs_html_tag("span",
  2105. xs_html_attr("title", L("Event")),
  2106. xs_html_raw(" &#128197; ")));
  2107. }
  2108. /* if it's a user from this same instance, add the score */
  2109. if (xs_startswith(id, srv_baseurl)) {
  2110. int n_likes = object_likes_len(id);
  2111. int n_boosts = object_announces_len(id);
  2112. /* alternate emojis: %d &#128077; %d &#128257; */
  2113. xs *s1 = xs_fmt("%d &#9733; %d &#8634;\n", n_likes, n_boosts);
  2114. xs_html_add(score,
  2115. xs_html_raw(s1));
  2116. }
  2117. xs *boosts = object_announces(id);
  2118. if (xs_list_len(boosts)) {
  2119. /* if somebody boosted this, show as origin */
  2120. const char *p = xs_list_get(boosts, -1);
  2121. xs *actor_r = NULL;
  2122. if (user && xs_list_in(boosts, user->md5) != -1) {
  2123. /* we boosted this */
  2124. xs_html_add(post_header,
  2125. xs_html_tag("div",
  2126. xs_html_attr("class", "snac-origin"),
  2127. xs_html_tag("a",
  2128. xs_html_attr("href", user->actor),
  2129. xs_html_text(xs_dict_get(user->config, "name"))),
  2130. xs_html_text(" "),
  2131. xs_html_text(L("boosted"))));
  2132. }
  2133. else
  2134. if (valid_status(object_get_by_md5(p, &actor_r))) {
  2135. xs *name = actor_name(actor_r, proxy);
  2136. if (!xs_is_null(name)) {
  2137. xs *href = NULL;
  2138. const char *id = xs_dict_get(actor_r, "id");
  2139. int fwers = 0;
  2140. int fwing = 0;
  2141. if (user != NULL) {
  2142. fwers = follower_check(user, id);
  2143. fwing = following_check(user, id);
  2144. }
  2145. if (!read_only && (fwers || fwing))
  2146. href = xs_fmt("%s/people/%s", user->actor, p);
  2147. else
  2148. href = xs_dup(id);
  2149. xs_html_add(post_header,
  2150. xs_html_tag("div",
  2151. xs_html_attr("class", "snac-origin"),
  2152. xs_html_tag("a",
  2153. xs_html_attr("href", href),
  2154. xs_html_raw(name)), /* already sanitized */
  2155. xs_html_text(" "),
  2156. xs_html_text(L("boosted"))));
  2157. }
  2158. }
  2159. }
  2160. if (user && strcmp(type, "Note") == 0) {
  2161. /* is the parent not here? */
  2162. const char *parent = get_in_reply_to(msg);
  2163. if (!xs_is_null(parent) && *parent) {
  2164. if (!timeline_here(user, parent)) {
  2165. xs_html_add(post_header,
  2166. xs_html_tag("div",
  2167. xs_html_attr("class", "snac-origin"),
  2168. xs_html_text(L("in reply to")),
  2169. xs_html_text(" "),
  2170. xs_html_tag("a",
  2171. xs_html_attr("href", parent),
  2172. xs_html_text("»"))));
  2173. }
  2174. }
  2175. }
  2176. xs_html_add(post_header,
  2177. html_msg_icon(read_only ? NULL : user, actor, msg, proxy, md5, lang));
  2178. /** post content **/
  2179. xs_html *snac_content_wrap = xs_html_tag("div",
  2180. xs_html_attr("class", "e-content snac-content"));
  2181. if (xs_is_string(lang))
  2182. xs_html_add(snac_content_wrap,
  2183. xs_html_attr("lang", lang));
  2184. xs_html_add(entry,
  2185. snac_content_wrap);
  2186. if (!has_title && !xs_is_null(v = xs_dict_get(msg, "name"))) {
  2187. xs_html_add(snac_content_wrap,
  2188. xs_html_tag("h3",
  2189. xs_html_attr("class", "snac-entry-title"),
  2190. xs_html_text(v)));
  2191. has_title = 1;
  2192. }
  2193. xs_html *snac_content = NULL;
  2194. v = xs_dict_get(msg, "summary");
  2195. /* if it has summary or marked as sensitive - add CW */
  2196. /* come clients don't set "sensitive" flag properly */
  2197. if ((!xs_is_null(v) && *v) || xs_type(xs_dict_get(msg, "sensitive")) == XSTYPE_TRUE) {
  2198. if (xs_is_null(v) || *v == '\0')
  2199. v = "...";
  2200. const char *cw = "";
  2201. if (user) {
  2202. /* only show it when not in the public timeline and the config setting is "open" */
  2203. cw = xs_dict_get(user->config, "cw");
  2204. if (xs_is_null(cw) || read_only)
  2205. cw = "";
  2206. }
  2207. /* summary can't contain HTML */
  2208. xs *s1 = xs_regex_replace(v, "<[^>]+>", "");
  2209. /* crop stupidly long summaries */
  2210. if (xs_utf8_len(s1) > 1024) {
  2211. s1 = xs_utf8_crop_i(s1, 0, 1024);
  2212. s1 = xs_str_cat(s1, "...");
  2213. }
  2214. snac_content = xs_html_tag("details",
  2215. xs_html_attr(cw, NULL),
  2216. xs_html_tag("summary",
  2217. xs_html_text(s1),
  2218. xs_html_text(L(" [SENSITIVE CONTENT]"))));
  2219. }
  2220. else
  2221. if (user &&
  2222. /* muted_words is all lowercase and sorted for performance */
  2223. (v = words_in_content(xs_dict_get(user->config, "muted_words"),
  2224. xs_dict_get(msg, "content"))) != NULL) {
  2225. snac_debug(user, 1, xs_fmt("word %s muted by user preferences: %s", v, id));
  2226. snac_content = xs_html_tag("details",
  2227. xs_html_tag("summary",
  2228. xs_html_text(L("Muted: ")),
  2229. xs_html_text(v)));
  2230. }
  2231. else {
  2232. snac_content = xs_html_tag("div", NULL);
  2233. }
  2234. xs_html_add(snac_content_wrap,
  2235. snac_content);
  2236. /* add all emoji reacts */
  2237. int is_emoji = 0;
  2238. if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) {
  2239. int c = 0;
  2240. const xs_dict *k;
  2241. xs *ls = xs_list_new();
  2242. xs *sfrl = xs_dict_new();
  2243. xs *rl = object_get_emoji_reacts(id);
  2244. while (xs_list_next(rl, &v, &c)) {
  2245. xs *m = NULL;
  2246. if (valid_status(object_get_by_md5(v, &m))) {
  2247. const char *content = xs_dict_get(m, "content");
  2248. const char *actor = xs_dict_get(m, "actor");
  2249. const xs_list *contentl = xs_dict_get(sfrl, content);
  2250. if ((user && is_muted(user, actor)) || is_instance_blocked(actor))
  2251. continue;
  2252. xs *actors = xs_list_new();
  2253. actors = xs_list_append(actors, actor);
  2254. char me = actor && user && strcmp(actor, user->actor) == 0;
  2255. int count = 1;
  2256. if (contentl) {
  2257. count = atoi(xs_list_get(contentl, 0)) + 1;
  2258. const xs_list *actorsc = xs_list_get(contentl, 1);
  2259. if (strncmp(xs_list_get(contentl, 2), "1", 1) == 0)
  2260. me = 1;
  2261. if (xs_list_in(actorsc, actor) != -1) {
  2262. xs_free(actors);
  2263. actors = xs_dup(actorsc);
  2264. }
  2265. else
  2266. actors = xs_list_cat(actors, actorsc);
  2267. }
  2268. xs *fl = xs_list_new();
  2269. xs *c1 = xs_fmt("%d", count);
  2270. xs *c2 = xs_fmt("%d", me);
  2271. fl = xs_list_append(fl, c1, actors, c2);
  2272. sfrl = xs_dict_append(sfrl, content, fl);
  2273. }
  2274. }
  2275. c = 0;
  2276. while (xs_list_next(rl, &k, &c)) {
  2277. xs *m = NULL;
  2278. if (valid_status(object_get_by_md5(k, &m))) {
  2279. const xs_dict *tag = xs_dict_get(m, "tag");
  2280. const xs_dict *ide = xs_dict_get(m, "id");
  2281. const char *content = xs_dict_get(m, "content");
  2282. const char *shortname;
  2283. shortname = xs_dict_get(m, "content");
  2284. const xs_list *items = xs_dict_get(sfrl, content);
  2285. if (!xs_is_null(items)) {
  2286. const char *nb = xs_list_get(items, 0);
  2287. const xs_list *actors = xs_list_get(items, 1);
  2288. const char me = *xs_list_get(items, 2) == '1';
  2289. is_emoji = 1;
  2290. xs *al = xs_join(actors, ",\n\t");
  2291. xs *act = atoi(nb) > 1 ?
  2292. xs_fmt("%d different actors \n\t%s", atoi(nb), al) :
  2293. xs_dup(xs_dict_get(m, "actor"));
  2294. xs *class = xs_list_new();
  2295. class = xs_list_append(class, "snac-reaction");
  2296. xs_html *ret = NULL;
  2297. if (tag && shortname) {
  2298. xs *cl = xs_list_new();
  2299. cl = xs_list_append(cl, "snac-reaction-image");
  2300. xs *emoji = _replace_shortnames(xs_dup(shortname), tag, 2, proxy, cl, act);
  2301. emoji = xs_strip_chars_i(emoji, ":");
  2302. if (me)
  2303. class = xs_list_append(class, "snac-reacted");
  2304. xs *l1 = xs_join(class, " ");
  2305. ret = xs_html_tag("button",
  2306. xs_html_attr("type", "submit"),
  2307. xs_html_attr("name", "action"),
  2308. xs_html_attr("value", me ? L("EmojiReact") : L("EmojiUnreact")),
  2309. xs_html_raw(emoji),
  2310. xs_html_tag("span",
  2311. xs_html_raw(nb),
  2312. xs_html_attr("style", "padding-left: 5px;")),
  2313. xs_html_attr("title", act),
  2314. xs_html_attr("class", l1));
  2315. if (!(ide && xs_startswith(ide, srv_baseurl)))
  2316. xs_html_add(ret, xs_html_attr("disabled", "true"));
  2317. }
  2318. else if (shortname) {
  2319. xs *sn = xs_dup(shortname);
  2320. const char *sna = sn;
  2321. unsigned int utf = xs_utf8_dec((const char **)&sna);
  2322. if (xs_is_emoji(utf)) {
  2323. const char *style = "font-size: large;";
  2324. if (me)
  2325. class = xs_list_append(class, "snac-reacted");
  2326. xs *l1 = xs_join(class, " ");
  2327. xs *s1 = xs_fmt("&#%d", utf);
  2328. ret = xs_html_tag("button",
  2329. xs_html_attr("type", "submit"),
  2330. xs_html_attr("name", "action"),
  2331. xs_html_attr("value", me ? L("EmojiUnreact") : L("EmojiReact")),
  2332. xs_html_raw(s1),
  2333. xs_html_tag("span",
  2334. xs_html_raw(nb),
  2335. xs_html_attr("style", "font-size: initial; padding-left: 5px;")),
  2336. xs_html_attr("title", act),
  2337. xs_html_attr("class", l1),
  2338. xs_html_attr("style", style));
  2339. }
  2340. }
  2341. if (ret) {
  2342. xs *s1;
  2343. if (user) {
  2344. xs *action = xs_fmt("%s/admin/action", user->actor);
  2345. xs *form_id = xs_fmt("%s_reply_form", md5);
  2346. xs_html *form =
  2347. xs_html_tag("form",
  2348. xs_html_attr("autocomplete", "off"),
  2349. xs_html_attr("method", "post"),
  2350. xs_html_attr("action", action),
  2351. xs_html_attr("enctype", "multipart/form-data"),
  2352. xs_html_attr("style", "display: inline-flex;"
  2353. "vertical-align: middle;"),
  2354. xs_html_attr("id", form_id),
  2355. xs_html_sctag("input",
  2356. xs_html_attr("type", "hidden"),
  2357. xs_html_attr("name", "id"),
  2358. xs_html_attr("value", id)),
  2359. xs_html_sctag("input",
  2360. xs_html_attr("type", "hidden"),
  2361. xs_html_attr("name", "eid"),
  2362. xs_html_attr("value", shortname)),
  2363. ret);
  2364. s1 = xs_html_render(form);
  2365. }
  2366. else
  2367. s1 = xs_html_render(ret);
  2368. ls = xs_list_append(ls, s1);
  2369. sfrl = xs_dict_del(sfrl, content);
  2370. }
  2371. }
  2372. }
  2373. }
  2374. c = 0;
  2375. xs_html *emoji_div;
  2376. if (xs_list_len(ls) > 0) {
  2377. emoji_div = xs_html_tag("div", xs_html_text(L("Emoji reactions: ")),
  2378. xs_html_attr("class", "snac-reaction-div"));
  2379. while (ls != NULL && xs_list_next(ls, &k, &c))
  2380. xs_html_add(emoji_div, xs_html_raw(k));
  2381. xs_html_add(snac_content_wrap, emoji_div);
  2382. }
  2383. }
  2384. {
  2385. /** build the content string **/
  2386. const char *content = xs_dict_get(msg, "content");
  2387. if (xs_type(content) != XSTYPE_STRING) {
  2388. if (!xs_is_null(content))
  2389. srv_archive_error("unexpected_content_xstype",
  2390. "content field type", xs_stock(XSTYPE_DICT), msg);
  2391. content = "";
  2392. }
  2393. /* skip ugly line breaks at the beginning */
  2394. while (xs_startswith(content, "<br>"))
  2395. content += 4;
  2396. xs *c = sanitize(content);
  2397. /* do some tweaks to the content */
  2398. c = xs_replace_i(c, "\r", "");
  2399. while (xs_endswith(c, "<br><br>"))
  2400. c = xs_crop_i(c, 0, -4);
  2401. c = xs_replace_i(c, "<br><br>", "<p>");
  2402. if (is_emoji == 0)
  2403. c = xs_str_cat(c, "<p>");
  2404. /* replace the :shortnames: */
  2405. c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2, proxy);
  2406. /* Peertube videos content is in markdown */
  2407. const char *mtype = xs_dict_get(msg, "mediaType");
  2408. if (xs_type(mtype) == XSTYPE_STRING && strcmp(mtype, "text/markdown") == 0) {
  2409. /* a full conversion could be better */
  2410. c = xs_replace_i(c, "\r", "");
  2411. c = xs_replace_i(c, "\n", "<br>");
  2412. }
  2413. /* c contains sanitized HTML */
  2414. xs_html_add(snac_content,
  2415. xs_html_raw(c));
  2416. /* quoted post */
  2417. const char *quoted_id = xs_or(xs_dict_get(msg, "quoteUri"), xs_dict_get(msg, "quoteUrl"));
  2418. if (level < 3 && xs_is_string(quoted_id) && xs_match(quoted_id, "https://*|http://*")) { /** **/
  2419. xs *quoted_post = NULL;
  2420. if (valid_status(object_get(quoted_id, &quoted_post))) {
  2421. xs *md5 = xs_md5_hex(quoted_id, strlen(quoted_id));
  2422. xs_html_add(snac_content,
  2423. xs_html_tag("blockquote",
  2424. xs_html_attr("class", "snac-quoted-post"),
  2425. html_entry(user, quoted_post, 1, level + 1, md5, 1)));
  2426. }
  2427. else
  2428. if (user)
  2429. enqueue_object_request(user, quoted_id, 0);
  2430. }
  2431. }
  2432. if (strcmp(type, "Question") == 0) { /** question content **/
  2433. const xs_list *oo = xs_dict_get(msg, "oneOf");
  2434. const xs_list *ao = xs_dict_get(msg, "anyOf");
  2435. const xs_list *p;
  2436. const xs_dict *v;
  2437. int closed = 0;
  2438. const char *f_closed = NULL;
  2439. xs_html *poll = xs_html_tag("div", NULL);
  2440. if (read_only)
  2441. closed = 1; /* non-identified page; show as closed */
  2442. else
  2443. if (user && is_msg_mine(user, id))
  2444. closed = 1; /* we questioned; closed for us */
  2445. else
  2446. if (user && was_question_voted(user, id))
  2447. closed = 1; /* we already voted; closed for us */
  2448. if ((f_closed = xs_dict_get(msg, "closed")) != NULL) {
  2449. /* it has a closed date... but is it in the past? */
  2450. time_t t0 = time(NULL);
  2451. time_t t1 = xs_parse_iso_date(f_closed, 0);
  2452. if (t1 < t0)
  2453. closed = 2;
  2454. }
  2455. /* get the appropriate list of options */
  2456. p = oo != NULL ? oo : ao;
  2457. if (closed || user == NULL) {
  2458. /* closed poll */
  2459. xs_html *poll_result = xs_html_tag("table",
  2460. xs_html_attr("class", "snac-poll-result"));
  2461. int c = 0;
  2462. while (xs_list_next(p, &v, &c)) {
  2463. const char *name = xs_dict_get(v, "name");
  2464. const xs_dict *replies = xs_dict_get(v, "replies");
  2465. if (xs_is_string(name) && xs_is_dict(replies)) {
  2466. const char *ti = xs_number_str(xs_dict_get(replies, "totalItems"));
  2467. if (xs_is_string(ti))
  2468. xs_html_add(poll_result,
  2469. xs_html_tag("tr",
  2470. xs_html_tag("td",
  2471. xs_html_text(name),
  2472. xs_html_text(":")),
  2473. xs_html_tag("td",
  2474. xs_html_text(ti))));
  2475. }
  2476. }
  2477. xs_html_add(poll,
  2478. poll_result);
  2479. }
  2480. else {
  2481. /* poll still active */
  2482. xs *vote_action = xs_fmt("%s/admin/vote", user->actor);
  2483. xs_html *form;
  2484. xs_html *poll_form = xs_html_tag("div",
  2485. xs_html_attr("class", "snac-poll-form"),
  2486. form = xs_html_tag("form",
  2487. xs_html_attr("autocomplete", "off"),
  2488. xs_html_attr("method", "post"),
  2489. xs_html_attr("action", vote_action),
  2490. xs_html_sctag("input",
  2491. xs_html_attr("type", "hidden"),
  2492. xs_html_attr("name", "actor"),
  2493. xs_html_attr("value", actor)),
  2494. xs_html_sctag("input",
  2495. xs_html_attr("type", "hidden"),
  2496. xs_html_attr("name", "irt"),
  2497. xs_html_attr("value", id))));
  2498. int c = 0;
  2499. while (xs_list_next(p, &v, &c)) {
  2500. const char *name = xs_dict_get(v, "name");
  2501. const xs_dict *replies = xs_dict_get(v, "replies");
  2502. if (name) {
  2503. char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems"));
  2504. xs_html *btn = xs_html_sctag("input",
  2505. xs_html_attr("id", name),
  2506. xs_html_attr("value", name),
  2507. xs_html_attr("name", "question"));
  2508. if (!xs_is_null(oo)) {
  2509. xs_html_add(btn,
  2510. xs_html_attr("type", "radio"),
  2511. xs_html_attr("required", "required"));
  2512. }
  2513. else
  2514. xs_html_add(btn,
  2515. xs_html_attr("type", "checkbox"));
  2516. xs_html_add(form,
  2517. btn,
  2518. xs_html_text(" "),
  2519. xs_html_tag("span",
  2520. xs_html_attr("title", ti),
  2521. xs_html_text(name)),
  2522. xs_html_sctag("br", NULL));
  2523. }
  2524. }
  2525. xs_html_add(form,
  2526. xs_html_tag("p", NULL),
  2527. xs_html_sctag("input",
  2528. xs_html_attr("type", "submit"),
  2529. xs_html_attr("class", "button"),
  2530. xs_html_attr("value", L("Vote"))));
  2531. xs_html_add(poll,
  2532. poll_form);
  2533. }
  2534. /* if it's *really* closed, say it */
  2535. if (closed == 2) {
  2536. xs_html_add(poll,
  2537. xs_html_tag("p",
  2538. xs_html_text(L("Closed"))));
  2539. }
  2540. else {
  2541. /* show when the poll closes */
  2542. const char *end_time = xs_dict_get(msg, "endTime");
  2543. /* Pleroma does not have an endTime field;
  2544. it has a closed time in the future */
  2545. if (xs_is_null(end_time))
  2546. end_time = xs_dict_get(msg, "closed");
  2547. if (!xs_is_null(end_time)) {
  2548. time_t t0 = time(NULL);
  2549. time_t t1 = xs_parse_iso_date(end_time, 0);
  2550. if (t1 > 0 && t1 > t0) {
  2551. time_t diff_time = t1 - t0;
  2552. xs *tf = xs_str_time_diff(diff_time);
  2553. char *p = tf;
  2554. /* skip leading zeros */
  2555. for (; *p == '0' || *p == ':'; p++);
  2556. xs_html_add(poll,
  2557. xs_html_tag("p",
  2558. xs_html_text(L("Closes in")),
  2559. xs_html_text(" "),
  2560. xs_html_text(p)));
  2561. }
  2562. }
  2563. }
  2564. xs_html_add(snac_content,
  2565. poll);
  2566. }
  2567. /** attachments **/
  2568. xs *attach = get_attachments(msg);
  2569. {
  2570. /* make custom css for attachments easier */
  2571. xs_html *content_attachments = xs_html_tag("div",
  2572. xs_html_attr("class", "snac-content-attachments"));
  2573. xs_html_add(snac_content,
  2574. content_attachments);
  2575. const char *content = xs_dict_get(msg, "content");
  2576. int c = 0;
  2577. const xs_dict *a;
  2578. while (xs_list_next(attach, &a, &c)) {
  2579. const char *type = xs_dict_get(a, "type");
  2580. const char *o_href = xs_dict_get(a, "href");
  2581. const char *name = xs_dict_get(a, "name");
  2582. if (!xs_is_string(type) || !xs_is_string(o_href))
  2583. continue;
  2584. /* if this URL is already in the post content, skip */
  2585. if (content && xs_str_in(content, o_href) != -1)
  2586. continue;
  2587. if (strcmp(type, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
  2588. continue;
  2589. /* do this attachment include an icon? */
  2590. const xs_dict *icon = xs_dict_get(a, "icon");
  2591. if (xs_type(icon) == XSTYPE_DICT) {
  2592. const char *icon_mtype = xs_dict_get(icon, "mediaType");
  2593. const char *icon_url = xs_dict_get(icon, "url");
  2594. if (icon_mtype && icon_url && xs_startswith(icon_mtype, "image/")) {
  2595. xs_html_add(content_attachments,
  2596. xs_html_tag("a",
  2597. xs_html_attr("href", icon_url),
  2598. xs_html_attr("target", "_blank"),
  2599. xs_html_sctag("img",
  2600. xs_html_attr("loading", "lazy"),
  2601. xs_html_attr("src", icon_url))));
  2602. }
  2603. }
  2604. xs *href = make_url(o_href, proxy, 0);
  2605. if (xs_startswith(type, "image/") || strcmp(type, "Image") == 0) {
  2606. xs_html_add(content_attachments,
  2607. xs_html_tag("a",
  2608. xs_html_attr("href", href),
  2609. xs_html_attr("target", "_blank"),
  2610. xs_html_sctag("img",
  2611. xs_html_attr("loading", "lazy"),
  2612. xs_html_attr("src", href),
  2613. xs_html_attr("alt", name),
  2614. xs_html_attr("title", name))));
  2615. }
  2616. else
  2617. if (xs_startswith(type, "video/") || strcmp(type, "Video") == 0) {
  2618. xs_html_add(content_attachments,
  2619. xs_html_tag("video",
  2620. xs_html_attr("preload", "none"),
  2621. xs_html_attr("style", "width: 100%"),
  2622. xs_html_attr("class", "snac-embedded-video"),
  2623. xs_html_attr("controls", NULL),
  2624. xs_html_attr("src", href),
  2625. xs_html_text(L("Video")),
  2626. xs_html_text(": "),
  2627. xs_html_tag("a",
  2628. xs_html_attr("href", href),
  2629. xs_html_text(name))));
  2630. }
  2631. else
  2632. if (xs_startswith(type, "audio/")) {
  2633. xs_html_add(content_attachments,
  2634. xs_html_tag("audio",
  2635. xs_html_attr("preload", "none"),
  2636. xs_html_attr("style", "width: 100%"),
  2637. xs_html_attr("class", "snac-embedded-audio"),
  2638. xs_html_attr("controls", NULL),
  2639. xs_html_attr("src", href),
  2640. xs_html_text(L("Audio")),
  2641. xs_html_text(": "),
  2642. xs_html_tag("a",
  2643. xs_html_attr("href", href),
  2644. xs_html_text(name))));
  2645. }
  2646. else
  2647. if (strcmp(type, "Link") == 0) {
  2648. xs_html_add(content_attachments,
  2649. xs_html_tag("p",
  2650. xs_html_tag("a",
  2651. xs_html_attr("href", o_href),
  2652. xs_html_text(href))));
  2653. /* do not generate an Alt... */
  2654. name = NULL;
  2655. }
  2656. else {
  2657. xs *d_href = xs_dup(o_href);
  2658. if (strlen(d_href) > 64) {
  2659. d_href[64] = '\0';
  2660. d_href = xs_str_cat(d_href, "...");
  2661. }
  2662. xs_html_add(content_attachments,
  2663. xs_html_tag("p",
  2664. xs_html_tag("a",
  2665. xs_html_attr("href", o_href),
  2666. xs_html_text(L("Attachment")),
  2667. xs_html_text(": "),
  2668. xs_html_text(d_href))));
  2669. /* do not generate an Alt... */
  2670. name = NULL;
  2671. }
  2672. if (name != NULL && *name) {
  2673. xs_html_add(content_attachments,
  2674. xs_html_tag("p",
  2675. xs_html_attr("class", "snac-alt-text"),
  2676. xs_html_tag("details",
  2677. xs_html_tag("summary",
  2678. xs_html_text(L("Alt..."))),
  2679. xs_html_text(name))));
  2680. }
  2681. }
  2682. }
  2683. /* has this message an audience (i.e., comes from a channel or community)? */
  2684. const char *audience = xs_dict_get(msg, "audience");
  2685. if (strcmp(type, "Page") == 0 && !xs_is_null(audience)) {
  2686. xs_html *au_tag = xs_html_tag("p",
  2687. xs_html_text("("),
  2688. xs_html_tag("a",
  2689. xs_html_attr("href", audience),
  2690. xs_html_attr("title", L("Source channel or community")),
  2691. xs_html_text(audience)),
  2692. xs_html_text(")"));
  2693. xs_html_add(snac_content_wrap,
  2694. au_tag);
  2695. }
  2696. /* does it have a location? */
  2697. const xs_dict *location = xs_dict_get(msg, "location");
  2698. if (xs_type(location) == XSTYPE_DICT) {
  2699. const xs_number *latitude = xs_dict_get(location, "latitude");
  2700. const xs_number *longitude = xs_dict_get(location, "longitude");
  2701. const char *name = xs_dict_get(location, "name");
  2702. const char *address = xs_dict_get(location, "address");
  2703. xs *label_list = xs_list_new();
  2704. if (xs_type(name) == XSTYPE_STRING)
  2705. label_list = xs_list_append(label_list, name);
  2706. if (xs_type(address) == XSTYPE_STRING)
  2707. label_list = xs_list_append(label_list, address);
  2708. if (xs_list_len(label_list)) {
  2709. const char *url = xs_dict_get(location, "url");
  2710. xs *label = xs_join(label_list, ", ");
  2711. if (xs_type(url) == XSTYPE_STRING) {
  2712. xs_html_add(snac_content_wrap,
  2713. xs_html_tag("p",
  2714. xs_html_text(L("Location: ")),
  2715. xs_html_tag("a",
  2716. xs_html_attr("href", url),
  2717. xs_html_attr("target", "_blank"),
  2718. xs_html_text(label))));
  2719. }
  2720. else
  2721. if (!xs_is_null(latitude) && !xs_is_null(longitude)) {
  2722. xs *url = xs_fmt("https://openstreetmap.org/search/?query=%s,%s",
  2723. xs_or(xs_number_str(latitude), latitude), xs_or(xs_number_str(longitude), longitude));
  2724. xs_html_add(snac_content_wrap,
  2725. xs_html_tag("p",
  2726. xs_html_text(L("Location: ")),
  2727. xs_html_tag("a",
  2728. xs_html_attr("href", url),
  2729. xs_html_attr("target", "_blank"),
  2730. xs_html_text(label))));
  2731. }
  2732. else
  2733. xs_html_add(snac_content_wrap,
  2734. xs_html_tag("p",
  2735. xs_html_text(L("Location: ")),
  2736. xs_html_text(label)));
  2737. }
  2738. }
  2739. if (strcmp(type, "Event") == 0) { /** Event start and end times **/
  2740. const char *s_time = xs_dict_get(msg, "startTime");
  2741. if (xs_is_string(s_time) && strlen(s_time) > 20) {
  2742. const char *e_time = xs_dict_get(msg, "endTime");
  2743. const char *tz = xs_dict_get(msg, "timezone");
  2744. xs *s = xs_replace_i(xs_dup(s_time), "T", " ");
  2745. xs *e = NULL;
  2746. if (xs_is_string(e_time) && strlen(e_time) > 20)
  2747. e = xs_replace_i(xs_dup(e_time), "T", " ");
  2748. /* if the event has a timezone, crop the offsets */
  2749. if (xs_is_string(tz)) {
  2750. s = xs_crop_i(s, 0, 19);
  2751. if (e)
  2752. e = xs_crop_i(e, 0, 19);
  2753. }
  2754. else
  2755. tz = "";
  2756. /* if start and end share the same day, crop it from the end */
  2757. if (e && memcmp(s, e, 11) == 0)
  2758. e = xs_crop_i(e, 11, 0);
  2759. if (e)
  2760. s = xs_str_cat(s, " / ", e);
  2761. if (*tz)
  2762. s = xs_str_cat(s, " (", tz, ")");
  2763. /* replace ugly decimals */
  2764. s = xs_replace_i(s, ".000", "");
  2765. xs_html_add(snac_content_wrap,
  2766. xs_html_tag("p",
  2767. xs_html_text(L("Time: ")),
  2768. xs_html_text(s)));
  2769. }
  2770. }
  2771. /* show all hashtags that has not been shown previously in the content */
  2772. const xs_list *tags = xs_dict_get(msg, "tag");
  2773. const char *o_content = xs_dict_get_def(msg, "content", "");
  2774. if (xs_is_string(o_content) && xs_is_list(tags) && xs_list_len(tags)) {
  2775. xs *content = xs_utf8_to_lower(o_content);
  2776. const xs_dict *tag;
  2777. xs_html *add_hashtags = xs_html_tag("ul",
  2778. xs_html_attr("class", "snac-more-hashtags"));
  2779. xs_list_foreach(tags, tag) {
  2780. const char *type = xs_dict_get(tag, "type");
  2781. if (xs_is_string(type) && strcmp(type, "Hashtag") == 0) {
  2782. const char *o_href = xs_dict_get(tag, "href");
  2783. const char *name = xs_dict_get(tag, "name");
  2784. if (xs_is_string(o_href) && xs_is_string(name)) {
  2785. xs *href = xs_utf8_to_lower(o_href);
  2786. if (xs_str_in(content, href) == -1 && xs_str_in(content, name) == -1) {
  2787. /* not in the content: add here */
  2788. xs_html_add(add_hashtags,
  2789. xs_html_tag("li",
  2790. xs_html_tag("a",
  2791. xs_html_attr("href", href),
  2792. xs_html_text(name),
  2793. xs_html_text(" "))));
  2794. }
  2795. }
  2796. }
  2797. }
  2798. xs_html_add(snac_content_wrap,
  2799. add_hashtags);
  2800. }
  2801. /** controls **/
  2802. if (!read_only && user) {
  2803. xs_html_add(entry,
  2804. html_entry_controls(user, actor, msg, md5));
  2805. }
  2806. /** children **/
  2807. if (!hide_children) {
  2808. xs *children = object_children(id);
  2809. int left = xs_list_len(children);
  2810. if (left) {
  2811. xs_html *ch_details = xs_html_tag("details",
  2812. xs_html_attr(collapse_threads ? "" : "open", NULL),
  2813. xs_html_tag("summary",
  2814. xs_html_text("...")));
  2815. xs_html_add(entry,
  2816. ch_details);
  2817. xs_html *fch_container = xs_html_tag("div",
  2818. xs_html_attr("class", "snac-thread-cont"));
  2819. xs_html_add(ch_details,
  2820. fch_container);
  2821. xs_html *ch_container = xs_html_tag("div",
  2822. xs_html_attr("class", level < 4 ? "snac-children" : "snac-children-too-deep"));
  2823. xs_html_add(ch_details,
  2824. ch_container);
  2825. xs_html *ch_older = NULL;
  2826. if (left > 3) {
  2827. xs_html_add(ch_container,
  2828. ch_older = xs_html_tag("details",
  2829. xs_html_tag("summary",
  2830. xs_html_text(L("Older...")))));
  2831. }
  2832. int ctxt = 0;
  2833. const char *cmd5;
  2834. int cnt = 0;
  2835. int o_cnt = 0;
  2836. int f_cnt = 0;
  2837. /* get the first child */
  2838. xs_list_next(children, &cmd5, &ctxt);
  2839. xs *f_chd = NULL;
  2840. if (user)
  2841. timeline_get_by_md5(user, cmd5, &f_chd);
  2842. else
  2843. object_get_by_md5(cmd5, &f_chd);
  2844. if (f_chd != NULL && xs_is_null(xs_dict_get(f_chd, "name"))) {
  2845. const char *p_author = get_atto(msg);
  2846. const char *author = get_atto(f_chd);
  2847. /* is the first child from the same author? */
  2848. if (xs_is_string(p_author) && xs_is_string(author) && strcmp(p_author, author) == 0) {
  2849. /* then, don't add it to the children container,
  2850. so that it appears unindented just before the parent
  2851. like a fucking Twitter-like thread */
  2852. xs_html_add(fch_container,
  2853. html_entry(user, f_chd, read_only, level + 1, cmd5, hide_children));
  2854. cnt++;
  2855. f_cnt++;
  2856. left--;
  2857. }
  2858. else
  2859. ctxt = 0; /* restart from the beginning */
  2860. }
  2861. while (xs_list_next(children, &cmd5, &ctxt)) {
  2862. xs *chd = NULL;
  2863. if (user)
  2864. timeline_get_by_md5(user, cmd5, &chd);
  2865. else
  2866. object_get_by_md5(cmd5, &chd);
  2867. if (chd != NULL) {
  2868. if (xs_is_null(xs_dict_get(chd, "name"))) {
  2869. xs_html *che = html_entry(user, chd, read_only,
  2870. level + 1, cmd5, hide_children);
  2871. if (che != NULL) {
  2872. if (left > 3) {
  2873. xs_html_add(ch_older,
  2874. che);
  2875. o_cnt++;
  2876. }
  2877. else
  2878. xs_html_add(ch_container,
  2879. che);
  2880. cnt++;
  2881. }
  2882. }
  2883. left--;
  2884. }
  2885. else
  2886. srv_debug(2, xs_fmt("cannot read child %s", cmd5));
  2887. }
  2888. /* if no children were finally added, hide the details */
  2889. if (cnt == 0)
  2890. xs_html_add(ch_details,
  2891. xs_html_attr("style", "display: none"));
  2892. if (o_cnt == 0 && ch_older)
  2893. xs_html_add(ch_older,
  2894. xs_html_attr("style", "display: none"));
  2895. if (f_cnt == 0)
  2896. xs_html_add(fch_container,
  2897. xs_html_attr("style", "display: none"));
  2898. }
  2899. }
  2900. /* add an invisible hr, to help differentiate between posts in text browsers */
  2901. xs_html_add(entry_top,
  2902. xs_html_sctag("hr",
  2903. xs_html_attr("hidden", NULL)));
  2904. return entry_top;
  2905. }
  2906. xs_html *html_footer(const snac *user)
  2907. {
  2908. return xs_html_tag("div",
  2909. xs_html_attr("class", "snac-footer"),
  2910. xs_html_tag("a",
  2911. xs_html_attr("href", srv_baseurl),
  2912. xs_html_text(L("about this site"))),
  2913. xs_html_text(" - "),
  2914. xs_html_text(L("powered by ")),
  2915. xs_html_tag("a",
  2916. xs_html_attr("href", WHAT_IS_SNAC_URL),
  2917. xs_html_tag("abbr",
  2918. xs_html_attr("title", "Social Networks Are Crap"),
  2919. xs_html_text(USER_AGENT))));
  2920. }
  2921. xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
  2922. int skip, int show, int show_more,
  2923. const char *title, const char *page,
  2924. int utl, const char *error, int terse)
  2925. /* returns the HTML for the timeline */
  2926. {
  2927. const char *v;
  2928. double t = ftime();
  2929. int hide_children = xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")) && read_only;
  2930. xs *desc = NULL;
  2931. xs *alternate = NULL;
  2932. if (xs_list_len(list) == 1) {
  2933. /* only one element? pick the description from the source */
  2934. const char *id = xs_list_get(list, 0);
  2935. xs *d = NULL;
  2936. object_get_by_md5(id, &d);
  2937. const char *sc = xs_dict_get(d, "sourceContent");
  2938. if (d && sc != NULL)
  2939. desc = xs_dup(sc);
  2940. alternate = xs_dup(xs_dict_get(d, "id"));
  2941. }
  2942. xs_html *head;
  2943. xs_html *body;
  2944. if (user) {
  2945. head = html_user_head(user, desc, alternate);
  2946. if (terse)
  2947. body = xs_html_tag("body", NULL);
  2948. else
  2949. body = html_user_body(user, read_only);
  2950. }
  2951. else {
  2952. head = html_instance_head();
  2953. body = html_instance_body();
  2954. }
  2955. xs_html *html = xs_html_tag("html",
  2956. head,
  2957. body);
  2958. if (user && !read_only)
  2959. xs_html_add(body,
  2960. html_top_controls(user));
  2961. if (error != NULL) {
  2962. xs_html_add(body,
  2963. xs_html_tag("dialog",
  2964. xs_html_attr("open", NULL),
  2965. xs_html_tag("p",
  2966. xs_html_text(error)),
  2967. xs_html_tag("form",
  2968. xs_html_attr("method", "dialog"),
  2969. xs_html_sctag("input",
  2970. xs_html_attr("type", "submit"),
  2971. xs_html_attr("value", L("Dismiss"))))));
  2972. }
  2973. /* show links to the available lists */
  2974. if (user && !read_only) {
  2975. xs_html *lol = xs_html_tag("ul",
  2976. xs_html_attr("class", "snac-list-of-lists"));
  2977. xs_html_add(body, lol);
  2978. xs *lists = list_maint(user, NULL, 0); /* get list of lists */
  2979. int ct = 0;
  2980. const char *v;
  2981. while (xs_list_next(lists, &v, &ct)) {
  2982. const char *lname = xs_list_get(v, 1);
  2983. xs *url = xs_fmt("%s/list/%s", user->actor, xs_list_get(v, 0));
  2984. xs *ttl = xs_fmt(L("Timeline for list '%s'"), lname);
  2985. xs_html_add(lol,
  2986. xs_html_tag("li",
  2987. xs_html_tag("a",
  2988. xs_html_attr("href", url),
  2989. xs_html_attr("class", "snac-list-link"),
  2990. xs_html_attr("title", ttl),
  2991. xs_html_text(lname))));
  2992. }
  2993. {
  2994. /* show the list of pinned posts */
  2995. xs *url = xs_fmt("%s/pinned", user->actor);
  2996. xs_html_add(lol,
  2997. xs_html_tag("li",
  2998. xs_html_tag("a",
  2999. xs_html_attr("href", url),
  3000. xs_html_attr("class", "snac-list-link"),
  3001. xs_html_attr("title", L("Pinned posts")),
  3002. xs_html_text(L("pinned")))));
  3003. }
  3004. {
  3005. /* show the list of bookmarked posts */
  3006. xs *url = xs_fmt("%s/admirations", user->actor);
  3007. xs_html_add(lol,
  3008. xs_html_tag("li",
  3009. xs_html_tag("a",
  3010. xs_html_attr("href", url),
  3011. xs_html_attr("class", "snac-list-link"),
  3012. xs_html_attr("title", L("Liked, boosted or reacted posts")),
  3013. xs_html_text(L("admirations")))));
  3014. }
  3015. {
  3016. /* show the list of bookmarked posts */
  3017. xs *url = xs_fmt("%s/bookmarks", user->actor);
  3018. xs_html_add(lol,
  3019. xs_html_tag("li",
  3020. xs_html_tag("a",
  3021. xs_html_attr("href", url),
  3022. xs_html_attr("class", "snac-list-link"),
  3023. xs_html_attr("title", L("Bookmarked posts")),
  3024. xs_html_text(L("bookmarks")))));
  3025. }
  3026. {
  3027. /* show the list of drafts */
  3028. xs *url = xs_fmt("%s/drafts", user->actor);
  3029. xs_html_add(lol,
  3030. xs_html_tag("li",
  3031. xs_html_tag("a",
  3032. xs_html_attr("href", url),
  3033. xs_html_attr("class", "snac-list-link"),
  3034. xs_html_attr("title", L("Post drafts")),
  3035. xs_html_text(L("drafts")))));
  3036. }
  3037. {
  3038. /* show the list of scheduled posts */
  3039. xs *url = xs_fmt("%s/sched", user->actor);
  3040. xs_html_add(lol,
  3041. xs_html_tag("li",
  3042. xs_html_tag("a",
  3043. xs_html_attr("href", url),
  3044. xs_html_attr("class", "snac-list-link"),
  3045. xs_html_attr("title", L("Scheduled posts")),
  3046. xs_html_text(L("scheduled posts")))));
  3047. }
  3048. /* the list of followed hashtags */
  3049. const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags");
  3050. if (xs_is_list(followed_hashtags) && xs_list_len(followed_hashtags)) {
  3051. xs_html *loht = xs_html_tag("ul",
  3052. xs_html_attr("class", "snac-list-of-lists"));
  3053. xs_html_add(body, loht);
  3054. const char *ht;
  3055. xs_list_foreach(followed_hashtags, ht) {
  3056. xs *url = NULL;
  3057. if (!xs_startswith(ht, "https:/""/"))
  3058. url = xs_fmt("%s/admin?q=%s", user->actor, ht);
  3059. else
  3060. url = xs_dup(ht);
  3061. url = xs_replace_i(url, "#", "%23");
  3062. xs_html_add(loht,
  3063. xs_html_tag("li",
  3064. xs_html_tag("a",
  3065. xs_html_attr("href", url),
  3066. xs_html_attr("class", "snac-list-link"),
  3067. xs_html_text(ht))));
  3068. }
  3069. }
  3070. }
  3071. xs_html_add(body,
  3072. xs_html_tag("a",
  3073. xs_html_attr("name", "snac-posts")));
  3074. xs_html *posts = xs_html_tag("div",
  3075. xs_html_attr("class", "snac-posts"));
  3076. if (title) {
  3077. xs_html_add(posts,
  3078. xs_html_tag("h2",
  3079. xs_html_attr("class", "snac-header"),
  3080. xs_html_text(title)));
  3081. }
  3082. xs_html_add(body, posts);
  3083. int mark_shown = 0;
  3084. int show_unlisted = user ? xs_is_true(xs_dict_get(user->config, "show_unlisted")) : 0;
  3085. xs_list_foreach(list, v) {
  3086. xs *msg = NULL;
  3087. int status;
  3088. /* "already seen" mark? */
  3089. if (strcmp(v, MD5_ALREADY_SEEN_MARK) == 0) {
  3090. if (skip == 0 && !mark_shown) {
  3091. xs *s = xs_fmt("%s/admin", user->actor);
  3092. xs_html_add(posts,
  3093. xs_html_tag("div",
  3094. xs_html_attr("class", "snac-no-more-unseen-posts"),
  3095. xs_html_text(L("No more unseen posts")),
  3096. xs_html_text(" - "),
  3097. xs_html_tag("a",
  3098. xs_html_attr("href", s),
  3099. xs_html_text(L("Back to top")))));
  3100. }
  3101. mark_shown = 1;
  3102. continue;
  3103. }
  3104. if (utl && user && !is_pinned_by_md5(user, v))
  3105. status = timeline_get_by_md5(user, v, &msg);
  3106. else
  3107. status = object_get_by_md5(v, &msg);
  3108. if (!valid_status(status))
  3109. continue;
  3110. /* if it's an instance page, discard messages from private users */
  3111. if (user == NULL && is_msg_from_private_user(msg))
  3112. continue;
  3113. const int scope = get_msg_visibility(msg);
  3114. if (user != NULL && scope != SCOPE_PUBLIC && !is_msg_mine(user, xs_dict_get(msg, "id"))) {
  3115. /* is this message a non-public reply? */
  3116. const char *irt = get_in_reply_to(msg);
  3117. /* is it a reply to something not in the storage? */
  3118. if (!xs_is_null(irt) && !object_here(irt)) {
  3119. /* is it for me? */
  3120. const xs_list *to = xs_dict_get_def(msg, "to", xs_stock(XSTYPE_LIST));
  3121. const xs_list *cc = xs_dict_get_def(msg, "cc", xs_stock(XSTYPE_LIST));
  3122. if (xs_list_in(to, user->actor) == -1 && xs_list_in(cc, user->actor) == -1) {
  3123. snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
  3124. continue;
  3125. }
  3126. }
  3127. }
  3128. /* hide non-public posts from /instance view */
  3129. if (page != NULL && strcmp(page, "/instance") == 0 && scope != SCOPE_PUBLIC)
  3130. continue;
  3131. /* hide non-public posts viewed from outside */
  3132. if (read_only) {
  3133. if (scope == SCOPE_MENTIONED || scope == SCOPE_FOLLOWERS)
  3134. continue;
  3135. if (scope == SCOPE_UNLISTED && !show_unlisted)
  3136. continue;
  3137. }
  3138. xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1);
  3139. if (entry != NULL)
  3140. xs_html_add(posts,
  3141. entry);
  3142. }
  3143. if (list && user && read_only) {
  3144. /** history **/
  3145. if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE && !terse) {
  3146. xs_html *ul = xs_html_tag("ul", NULL);
  3147. xs_html *history = xs_html_tag("div",
  3148. xs_html_attr("class", "snac-history"),
  3149. xs_html_tag("p",
  3150. xs_html_attr("class", "snac-history-title"),
  3151. xs_html_text(L("History"))),
  3152. ul);
  3153. xs *list = history_list(user);
  3154. xs_list *p = list;
  3155. const char *v;
  3156. while (xs_list_iter(&p, &v)) {
  3157. xs *fn = xs_replace(v, ".html", "");
  3158. xs *url = xs_fmt("%s/h/%s", user->actor, v);
  3159. xs_html_add(ul,
  3160. xs_html_tag("li",
  3161. xs_html_tag("a",
  3162. xs_html_attr("href", url),
  3163. xs_html_text(fn))));
  3164. }
  3165. xs_html_add(body,
  3166. history);
  3167. }
  3168. }
  3169. {
  3170. xs *s1 = xs_fmt("\n<!-- %lf seconds -->\n", ftime() - t);
  3171. xs_html_add(body,
  3172. xs_html_raw(s1));
  3173. }
  3174. if (show_more) {
  3175. xs *m = NULL;
  3176. xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
  3177. xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
  3178. if (page != NULL)
  3179. url = xs_str_cat(url, page);
  3180. if (xs_str_in(url, "?") != -1)
  3181. m = xs_fmt("%s&%s", url, ss);
  3182. else
  3183. m = xs_fmt("%s?%s", url, ss);
  3184. xs_html *more_links = xs_html_tag("p",
  3185. xs_html_tag("a",
  3186. xs_html_attr("href", url),
  3187. xs_html_attr("name", "snac-more"),
  3188. xs_html_text(L("Back to top"))),
  3189. xs_html_text(" - "),
  3190. xs_html_tag("a",
  3191. xs_html_attr("href", m),
  3192. xs_html_attr("name", "snac-more"),
  3193. xs_html_text(L("More..."))));
  3194. xs_html_add(body,
  3195. more_links);
  3196. }
  3197. xs_html_add(body,
  3198. html_footer(user));
  3199. return xs_html_render_s(html, "<!DOCTYPE html>\n");
  3200. }
  3201. xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy, int count)
  3202. {
  3203. xs_html *snac_posts;
  3204. xs *header_cnt;
  3205. if (count > -1)
  3206. header_cnt = xs_fmt("%s - %d\n", header, count);
  3207. else
  3208. header_cnt = xs_fmt("%s\n", header);
  3209. xs_html *people = xs_html_tag("div",
  3210. xs_html_tag("h2",
  3211. xs_html_attr("class", "snac-header"),
  3212. xs_html_text(header_cnt)),
  3213. snac_posts = xs_html_tag("details",
  3214. xs_html_attr("open", NULL),
  3215. xs_html_tag("summary",
  3216. xs_html_text("..."))));
  3217. xs *redir = xs_fmt("%s/people", user->actor);
  3218. const char *actor_id;
  3219. xs_list_foreach(list, actor_id) {
  3220. xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
  3221. xs *actor = NULL;
  3222. if (valid_status(actor_get(actor_id, &actor))) {
  3223. xs_html *snac_post = xs_html_tag("div",
  3224. xs_html_attr("class", "snac-post"),
  3225. xs_html_tag("a",
  3226. xs_html_attr("name", md5)),
  3227. xs_html_tag("div",
  3228. xs_html_attr("class", "snac-post-header"),
  3229. html_actor_icon(user, actor, xs_dict_get(actor, "published"),
  3230. NULL, NULL, -1, 0, proxy, NULL, NULL)));
  3231. /* content (user bio) */
  3232. const char *c = xs_dict_get(actor, "summary");
  3233. const xs_val *tag = xs_dict_get(actor, "tag");
  3234. if (!xs_is_null(c)) {
  3235. xs *sc = sanitize(c);
  3236. sc = replace_shortnames(sc, tag, 2, proxy);
  3237. xs_html *snac_content = xs_html_tag("div",
  3238. xs_html_attr("class", "snac-content"));
  3239. if (xs_startswith(sc, "<p>"))
  3240. xs_html_add(snac_content,
  3241. xs_html_raw(sc)); /* already sanitized */
  3242. else
  3243. xs_html_add(snac_content,
  3244. xs_html_tag("p",
  3245. xs_html_raw(sc))); /* already sanitized */
  3246. xs_html_add(snac_post, snac_content);
  3247. }
  3248. /* does it have account metrics in the object? */
  3249. const xs_val *fwing = xs_dict_get(actor, "following_count");
  3250. const xs_val *fwers = xs_dict_get(actor, "followers_count");
  3251. if (!xs_is_null(fwing) && !xs_is_null(fwers)) {
  3252. int i_fwing = (int) xs_number_get_l(fwing);
  3253. int i_fwers = (int) xs_number_get_l(fwers);
  3254. if (i_fwing || i_fwers) {
  3255. xs *s = xs_fmt(L("%d following, %d followers"), i_fwing, i_fwers);
  3256. xs_html_add(snac_post,
  3257. xs_html_tag("div",
  3258. xs_html_tag("p",
  3259. xs_html_text(s))));
  3260. }
  3261. }
  3262. /* does it have a location? */
  3263. const xs_dict *location = xs_dict_get(actor, "location");
  3264. if (xs_is_dict(location)) {
  3265. const xs_val *latitude = xs_dict_get(location, "latitude");
  3266. const xs_val *longitude = xs_dict_get(location, "longitude");
  3267. if (latitude && longitude) {
  3268. const char *name = xs_dict_get_def(location, "name", "Home");
  3269. const char *la = xs_is_string(latitude) ? latitude : xs_number_str(latitude);
  3270. const char *lo = xs_is_string(longitude) ? longitude : xs_number_str(longitude);
  3271. if (xs_is_string(la) && xs_is_string(lo)) {
  3272. xs *label = xs_fmt("%s,%s", la, lo);
  3273. xs *url = xs_fmt("https://openstreetmap.org/search?query=%s,%s", la, lo);
  3274. xs_html_add(snac_post,
  3275. xs_html_tag("p",
  3276. xs_html_text(name),
  3277. xs_html_text(": "),
  3278. xs_html_tag("a",
  3279. xs_html_attr("href", url),
  3280. xs_html_attr("target", "_blank"),
  3281. xs_html_text(label))));
  3282. }
  3283. }
  3284. }
  3285. /* add user metadata */
  3286. xs_html *snac_metadata = xs_html_tag("div",
  3287. xs_html_attr("class", "snac-metadata"));
  3288. int count = 0;
  3289. const xs_val *address = xs_dict_get(actor, "vcard:Address");
  3290. if (xs_is_string(address)) {
  3291. xs_html_add(snac_metadata,
  3292. xs_html_tag("span",
  3293. xs_html_attr("class", "snac-property-name"),
  3294. xs_html_raw("&#x1F4CD; Location")),
  3295. xs_html_text(":"),
  3296. xs_html_raw("&nbsp;"),
  3297. xs_html_tag("span",
  3298. xs_html_attr("class", "snac-property-value p-adr"),
  3299. xs_html_text(address)),
  3300. xs_html_sctag("br", NULL));
  3301. count++;
  3302. }
  3303. const xs_val *birthday = xs_dict_get(actor, "vcard:bday");
  3304. if (xs_is_string(birthday)) {
  3305. xs_html_add(snac_metadata,
  3306. xs_html_tag("span",
  3307. xs_html_attr("class", "snac-property-name"),
  3308. xs_html_raw("&#x1F382; Birthday")),
  3309. xs_html_text(":"),
  3310. xs_html_raw("&nbsp;"),
  3311. xs_html_tag("time",
  3312. xs_html_attr("class", "snac-property-value dt-bday"),
  3313. xs_html_text(birthday)),
  3314. xs_html_sctag("br", NULL));
  3315. count++;
  3316. }
  3317. const xs_list *attachment = xs_dict_get(actor, "attachment");
  3318. if (count > 0 && xs_list_len(attachment) > 0) {
  3319. xs_html_add(snac_metadata,
  3320. xs_html_sctag("hr",
  3321. xs_html_attr("class", "snac-property-divider")));
  3322. }
  3323. const xs_val *v;
  3324. xs_list_foreach(attachment, v) {
  3325. const char *type = xs_dict_get(v, "type");
  3326. const char *name = xs_dict_get(v, "name");
  3327. const char *value = xs_dict_get(v, "value");
  3328. if (!xs_is_null(type) && !xs_is_null(name) &&
  3329. !xs_is_null(value) && strcmp(type, "PropertyValue") == 0) {
  3330. /* both the name and the value can contain emoji */
  3331. xs *nam = sanitize(name);
  3332. nam = replace_shortnames(nam, tag, 1, proxy);
  3333. /* todo: sometimes the value is transmitted as markdown and not html ._. */
  3334. xs *val = sanitize(value);
  3335. val = replace_shortnames(val, tag, 1, proxy);
  3336. /* delete <p> tags, because some software sends them */
  3337. val = xs_replace_i(val, "<p>", "");
  3338. val = xs_replace_i(val, "</p>", "");
  3339. xs_html_add(snac_metadata,
  3340. xs_html_tag("span",
  3341. xs_html_attr("class", "snac-property-name"),
  3342. xs_html_raw(nam)),
  3343. xs_html_text(":"),
  3344. xs_html_raw("&nbsp;"),
  3345. xs_html_tag("span",
  3346. xs_html_attr("class", "snac-property-value"),
  3347. xs_html_raw(val)),
  3348. xs_html_sctag("br", NULL));
  3349. count++;
  3350. }
  3351. }
  3352. if (count > 0) {
  3353. xs_html_add(snac_post, snac_metadata);
  3354. }
  3355. else {
  3356. /* free the html, by rendering it... */
  3357. xs_free(xs_html_render(snac_metadata));
  3358. }
  3359. /* buttons */
  3360. xs *btn_form_action = xs_fmt("%s/admin/action", user->actor);
  3361. xs_html *snac_controls = xs_html_tag("div",
  3362. xs_html_attr("class", "snac-controls"));
  3363. xs_html *form = xs_html_tag("form",
  3364. xs_html_attr("autocomplete", "off"),
  3365. xs_html_attr("method", "post"),
  3366. xs_html_attr("action", btn_form_action),
  3367. xs_html_sctag("input",
  3368. xs_html_attr("type", "hidden"),
  3369. xs_html_attr("name", "actor"),
  3370. xs_html_attr("value", actor_id)),
  3371. xs_html_sctag("input",
  3372. xs_html_attr("type", "hidden"),
  3373. xs_html_attr("name", "hard-redir"),
  3374. xs_html_attr("value", redir)),
  3375. xs_html_sctag("input",
  3376. xs_html_attr("type", "hidden"),
  3377. xs_html_attr("name", "actor-form"),
  3378. xs_html_attr("value", "yes")));
  3379. xs_html_add(snac_controls, form);
  3380. if (following_check(user, actor_id)) {
  3381. xs_html_add(form,
  3382. html_button("unfollow", L("Unfollow"),
  3383. L("Stop following this user's activity")));
  3384. if (is_limited(user, actor_id))
  3385. xs_html_add(form,
  3386. html_button("unlimit", L("Unlimit"),
  3387. L("Allow announces (boosts) from this user")));
  3388. else
  3389. xs_html_add(form,
  3390. html_button("limit", L("Limit"),
  3391. L("Block announces (boosts) from this user")));
  3392. }
  3393. else {
  3394. xs_html_add(form,
  3395. html_button("follow", L("Follow"),
  3396. L("Start following this user's activity")));
  3397. if (follower_check(user, actor_id))
  3398. xs_html_add(form,
  3399. html_button("delete", L("Delete"), L("Delete this user")));
  3400. }
  3401. if (pending_check(user, actor_id)) {
  3402. xs_html_add(form,
  3403. html_button("approve", L("Approve"),
  3404. L("Approve this follow request")));
  3405. xs_html_add(form,
  3406. html_button("discard", L("Discard"), L("Discard this follow request")));
  3407. }
  3408. if (is_muted(user, actor_id))
  3409. xs_html_add(form,
  3410. html_button("unmute", L("Unmute"),
  3411. L("Stop blocking activities from this user")));
  3412. else
  3413. xs_html_add(form,
  3414. html_button("mute", L("MUTE"),
  3415. L("Block any activity from this user")));
  3416. /* the post textarea */
  3417. xs *dm_div_id = xs_fmt("%s_%s_dm", md5, t);
  3418. xs *dm_form_id = xs_fmt("%s_reply_form", md5);
  3419. xs_html_add(snac_controls,
  3420. xs_html_tag("p", NULL),
  3421. html_note(user, L("Direct Message..."),
  3422. dm_div_id, dm_form_id,
  3423. "", "",
  3424. NULL, actor_id,
  3425. xs_stock(XSTYPE_FALSE), "",
  3426. SCOPE_MENTIONED, NULL,
  3427. NULL, 0, NULL, NULL, 0, NULL, NULL),
  3428. xs_html_tag("p", NULL));
  3429. xs_html_add(snac_post, snac_controls);
  3430. xs_html_add(snac_posts, snac_post);
  3431. }
  3432. }
  3433. return people;
  3434. }
  3435. xs_str *html_people(snac *user)
  3436. {
  3437. const char *proxy = NULL;
  3438. if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
  3439. proxy = user->actor;
  3440. xs *wing = following_list(user);
  3441. xs *wers = follower_list(user);
  3442. xs *pending = pending_list(user);
  3443. xs_html *lists = xs_html_tag("div",
  3444. xs_html_attr("class", "snac-posts"));
  3445. if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) {
  3446. xs_html_add(lists,
  3447. html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy, xs_list_len(pending)));
  3448. }
  3449. xs_html_add(lists,
  3450. html_people_list(user, wing, L("People you follow"), "i", proxy, following_list_len(user)),
  3451. html_people_list(user, wers, L("People that follow you"), "e", proxy, xs_list_len(wers)));
  3452. xs_html *html = xs_html_tag("html",
  3453. html_user_head(user, NULL, NULL),
  3454. xs_html_add(html_user_body(user, 0),
  3455. lists,
  3456. html_footer(user)));
  3457. return xs_html_render_s(html, "<!DOCTYPE html>\n");
  3458. }
  3459. /* Filter list to display only posts by actor. We'll probably show
  3460. fewer than show posts. Should we try harder to find some? */
  3461. xs_str *html_people_one(snac *user, const char *actor, const xs_list *list,
  3462. int skip, int show, int show_more, const char *page)
  3463. {
  3464. const char *proxy = NULL;
  3465. xs_list *p = (xs_list *)list;
  3466. const char *v;
  3467. enqueue_actor_refresh(user, actor, 0);
  3468. if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
  3469. proxy = user->actor;
  3470. xs_html *body = html_user_body(user, 0);
  3471. xs_html *lists = xs_html_tag("div",
  3472. xs_html_attr("class", "snac-posts"));
  3473. xs *foll = xs_list_append(xs_list_new(), actor);
  3474. xs_html_add(lists,
  3475. html_people_list(user, foll, L("Contact's posts"), "p", proxy, -1));
  3476. xs_html_add(body, lists);
  3477. while (xs_list_iter(&p, &v)) {
  3478. xs *msg = NULL;
  3479. int status;
  3480. status = timeline_get_by_md5(user, v, &msg);
  3481. if (!valid_status(status))
  3482. continue;
  3483. const char *id = xs_dict_get(msg, "id");
  3484. const char *by = get_atto(msg);
  3485. xs *actor_md5 = NULL;
  3486. xs_list *boosts = NULL;
  3487. xs_list *likes = NULL;
  3488. xs_list *reacts = NULL;
  3489. /* Besides actor's posts, also show actor's boosts, and also
  3490. posts by user with likes or reacts by actor. I.e., any
  3491. actor's actions that user could have seen in the timeline
  3492. or in notifications. */
  3493. if (!(by && strcmp(actor, by) == 0) &&
  3494. xs_list_in((boosts = object_announces(id)),
  3495. (actor_md5 = xs_md5_hex(actor, strlen(actor)))) == -1 &&
  3496. (!(by && strcmp(user->actor, by) == 0) ||
  3497. (xs_list_in((likes = object_likes(id)), actor_md5) == -1 &&
  3498. xs_list_in((reacts = object_get_emoji_reacts(id)), actor_md5) == -1)))
  3499. continue;
  3500. xs_html *entry = html_entry(user, msg, 0, 0, v, 1);
  3501. if (entry != NULL)
  3502. xs_html_add(lists,
  3503. entry);
  3504. }
  3505. if (show_more) {
  3506. xs *m = NULL;
  3507. xs *m10 = NULL;
  3508. xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
  3509. xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
  3510. if (page != NULL)
  3511. url = xs_str_cat(url, page);
  3512. if (xs_str_in(url, "?") != -1)
  3513. m = xs_fmt("%s&%s", url, ss);
  3514. else
  3515. m = xs_fmt("%s?%s", url, ss);
  3516. m10 = xs_fmt("%s0", m);
  3517. xs_html *more_links = xs_html_tag("p",
  3518. xs_html_tag("a",
  3519. xs_html_attr("href", url),
  3520. xs_html_attr("name", "snac-more"),
  3521. xs_html_text(L("Back to top"))),
  3522. xs_html_text(" - "),
  3523. xs_html_tag("a",
  3524. xs_html_attr("href", m),
  3525. xs_html_attr("name", "snac-more"),
  3526. xs_html_text(L("More..."))),
  3527. xs_html_text(" - "),
  3528. xs_html_tag("a",
  3529. xs_html_attr("href", m10),
  3530. xs_html_attr("name", "snac-more"),
  3531. xs_html_text(L("More (x 10)..."))));
  3532. xs_html_add(body,
  3533. more_links);
  3534. }
  3535. xs_html *html = xs_html_tag("html",
  3536. html_user_head(user, NULL, NULL),
  3537. xs_html_add(body,
  3538. html_footer(user)));
  3539. return xs_html_render_s(html, "<!DOCTYPE html>\n");
  3540. }
  3541. void notify_filter(snac *user, const xs_dict *p_vars)
  3542. /* sets filter for notifications */
  3543. {
  3544. const char *v;
  3545. int likes_on = (v = xs_dict_get(p_vars, "likes_on")) ? strcmp(v, "on") == 0 : 0;
  3546. int reacts_on = (v = xs_dict_get(p_vars, "reacts_on")) ? strcmp(v, "on") == 0 : 0;
  3547. int ments_on = (v = xs_dict_get(p_vars, "mentions_on")) ? strcmp(v, "on") == 0 : 0;
  3548. int ann_on = (v = xs_dict_get(p_vars, "announces_on")) ? strcmp(v, "on") == 0 : 0;
  3549. int foll_on = (v = xs_dict_get(p_vars, "follows_on")) ? strcmp(v, "on") == 0 : 0;
  3550. int unfoll_on = (v = xs_dict_get(p_vars, "unfollows_on")) ? strcmp(v, "on") == 0 : 0;
  3551. int folreq_on = (v = xs_dict_get(p_vars, "folreqs_on")) ? strcmp(v, "on") == 0 : 0;
  3552. int blocks_on = (v = xs_dict_get(p_vars, "blocks_on")) ? strcmp(v, "on") == 0 : 0;
  3553. int polls_on = (v = xs_dict_get(p_vars, "polls_on")) ? strcmp(v, "on") == 0 : 0;
  3554. xs *filter = xs_dict_new();
  3555. filter = xs_dict_set(filter, "likes", xs_stock(likes_on ? XSTYPE_TRUE : XSTYPE_FALSE));
  3556. filter = xs_dict_set(filter, "reacts", xs_stock(reacts_on ? XSTYPE_TRUE : XSTYPE_FALSE));
  3557. filter = xs_dict_set(filter, "mentions", xs_stock(ments_on ? XSTYPE_TRUE : XSTYPE_FALSE));
  3558. filter = xs_dict_set(filter, "announces", xs_stock(ann_on ? XSTYPE_TRUE : XSTYPE_FALSE));
  3559. filter = xs_dict_set(filter, "follows", xs_stock(foll_on ? XSTYPE_TRUE : XSTYPE_FALSE));
  3560. filter = xs_dict_set(filter, "unfollows", xs_stock(unfoll_on ? XSTYPE_TRUE : XSTYPE_FALSE));
  3561. filter = xs_dict_set(filter, "folreqs", xs_stock(folreq_on ? XSTYPE_TRUE : XSTYPE_FALSE));
  3562. filter = xs_dict_set(filter, "blocks", xs_stock(blocks_on ? XSTYPE_TRUE : XSTYPE_FALSE));
  3563. filter = xs_dict_set(filter, "polls", xs_stock(polls_on ? XSTYPE_TRUE : XSTYPE_FALSE));
  3564. user->config = xs_dict_set(user->config, "notify_filter", filter);
  3565. user->tz = xs_dict_get_def(user->config, "tz", "UTC"); // previous line invalidates user->tz
  3566. }
  3567. xs_str *html_notifications(snac *user, int skip, int show)
  3568. {
  3569. const char *proxy = NULL;
  3570. if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
  3571. proxy = user->actor;
  3572. xs *n_list_unfilt = notify_list(user, skip, show);
  3573. xs *n_time = notify_check_time(user, 0);
  3574. xs_html *body = html_user_body(user, 0);
  3575. const xs_dict *n_filter = xs_dict_get(user->config, "notify_filter");
  3576. if (!n_filter) {
  3577. xs *filter = xs_dict_new();
  3578. user->config = xs_dict_set(user->config, "notify_filter", filter);
  3579. user->tz = xs_dict_get_def(user->config, "tz", "UTC"); // previous line invalidates user->tz
  3580. n_filter = xs_dict_get(user->config, "notify_filter");
  3581. }
  3582. xs *n_list = notify_filter_list(user, n_list_unfilt);
  3583. /* all filters are true by default */
  3584. const xs_val *n_def = xs_stock( XSTYPE_TRUE );
  3585. int n_likes_on = xs_is_true(xs_dict_get_def(n_filter, "likes", n_def));
  3586. int n_reacts_on = xs_is_true(xs_dict_get_def(n_filter, "reacts", n_def));
  3587. int n_ments_on = xs_is_true(xs_dict_get_def(n_filter, "mentions", n_def));
  3588. int n_ann_on = xs_is_true(xs_dict_get_def(n_filter, "announces", n_def));
  3589. int n_fol_on = xs_is_true(xs_dict_get_def(n_filter, "follows", n_def));
  3590. int n_unfol_on = xs_is_true(xs_dict_get_def(n_filter, "unfollows", n_def));
  3591. int n_folreq_on = xs_is_true(xs_dict_get_def(n_filter, "folreqs", n_def));
  3592. int n_blocks_on = xs_is_true(xs_dict_get_def(n_filter, "blocks", n_def));
  3593. int n_polls_on = xs_is_true(xs_dict_get_def(n_filter, "polls", n_def));
  3594. xs_html *html = xs_html_tag("html",
  3595. html_user_head(user, NULL, NULL),
  3596. body);
  3597. xs *filter_notifs_action = xs_fmt("%s/admin/filter-notifications", user->actor);
  3598. xs *notifs_action = xs_fmt("%s/notifications", user->actor);
  3599. xs_html *notifs_form = xs_html_tag("form",
  3600. xs_html_attr("autocomplete", "off"),
  3601. xs_html_attr("method", "post"),
  3602. xs_html_attr("action", filter_notifs_action),
  3603. xs_html_attr("enctype", "multipart/form-data"),
  3604. xs_html_attr("id", "filter"),
  3605. xs_html_sctag("input",
  3606. xs_html_attr("type", "hidden"),
  3607. xs_html_attr("name", "hard-redir"),
  3608. xs_html_attr("value", notifs_action)),
  3609. html_checkbox("likes_on", L("Likes"), n_likes_on),
  3610. html_checkbox("reacts_on", L("Emoji reacts"), n_reacts_on),
  3611. html_checkbox("mentions_on", L("Mentions"), n_ments_on),
  3612. html_checkbox("announces_on", L("Announces"), n_ann_on),
  3613. html_checkbox("follows_on", L("Follows"), n_fol_on),
  3614. html_checkbox("unfollows_on", L("Unfollows"), n_unfol_on),
  3615. html_checkbox("folreqs_on", L("Follow requests"), n_folreq_on),
  3616. html_checkbox("blocks_on", L("Blocks"), n_blocks_on),
  3617. html_checkbox("polls_on", L("Polls"), n_polls_on),
  3618. xs_html_sctag("input",
  3619. xs_html_attr("type", "submit"),
  3620. xs_html_attr("class", "button"),
  3621. xs_html_attr("value", L("Save"))));
  3622. xs_html_add(body,
  3623. xs_html_tag("p",
  3624. xs_html_tag("div",
  3625. xs_html_attr("class", "snac-notify-filter"),
  3626. xs_html_tag("details",
  3627. xs_html_tag("summary",
  3628. xs_html_text(L("Notifications filter..."))),
  3629. notifs_form))));
  3630. xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor);
  3631. xs_html_add(body,
  3632. xs_html_tag("form",
  3633. xs_html_attr("autocomplete", "off"),
  3634. xs_html_attr("method", "post"),
  3635. xs_html_attr("action", clear_all_action),
  3636. xs_html_attr("id", "clear"),
  3637. xs_html_sctag("input",
  3638. xs_html_attr("type", "submit"),
  3639. xs_html_attr("class", "snac-btn-clear-all"),
  3640. xs_html_attr("value", L("Clear all")))));
  3641. xs_html *noti_new = NULL;
  3642. xs_html *noti_seen = NULL;
  3643. xs_html *posts = xs_html_tag("div",
  3644. xs_html_attr("class", "snac-posts"));
  3645. xs_html_add(body, posts);
  3646. xs_set rep;
  3647. xs_set_init(&rep);
  3648. /* dict to store previous notification labels */
  3649. xs *admiration_labels = xs_dict_new();
  3650. const xs_str *v;
  3651. xs_list_foreach(n_list, v) {
  3652. xs *noti = notify_get(user, v);
  3653. if (noti == NULL)
  3654. continue;
  3655. xs *obj = NULL;
  3656. const char *type = xs_dict_get(noti, "type");
  3657. const char *utype = xs_dict_get(noti, "utype");
  3658. const char *id = xs_dict_get(noti, "objid");
  3659. const char *date = xs_dict_get(noti, "date");
  3660. const char *id2 = xs_dict_get_path(noti, "msg.id");
  3661. xs *wrk = NULL;
  3662. if (xs_is_null(id))
  3663. continue;
  3664. if (is_hidden(user, id))
  3665. continue;
  3666. if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1)
  3667. continue;
  3668. if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact")))
  3669. continue;
  3670. object_get(id, &obj);
  3671. const char *msg_id = NULL;
  3672. if (xs_is_dict(obj))
  3673. msg_id = xs_dict_get(obj, "id");
  3674. const char *actor_id = xs_dict_get(noti, "actor");
  3675. xs *actor = NULL;
  3676. xs *a_name = NULL;
  3677. if (valid_status(actor_get(actor_id, &actor)))
  3678. a_name = actor_name(actor, proxy);
  3679. else {
  3680. a_name = xs_dup(actor_id);
  3681. /* actor not here: request it */
  3682. enqueue_actor_refresh(user, actor_id, 0);
  3683. }
  3684. const char *html_url = xs_dict_get_def(actor, "url", actor_id);
  3685. if (xs_is_list(html_url))
  3686. html_url = xs_list_get(html_url, 0);
  3687. if (!xs_is_string(html_url))
  3688. html_url = actor_id;
  3689. xs *label_sanitized = sanitize(type);
  3690. const char *label = label_sanitized;
  3691. if (strcmp(type, "Create") == 0)
  3692. label = L("Mention");
  3693. else
  3694. if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
  3695. label = L("Finished poll");
  3696. else
  3697. if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") == 0)
  3698. label = L("Unfollow");
  3699. else
  3700. if (strcmp(type, "EmojiReact") == 0 || strcmp(type, "Like") == 0) {
  3701. const char *content = xs_dict_get_path(noti, "msg.content");
  3702. xs *cd = xs_dup(content);
  3703. const char *sna = cd;
  3704. const xs_dict *tag = xs_dict_get_path(noti, "msg.tag");
  3705. unsigned int utf = xs_utf8_dec((const char **)&sna);
  3706. int isEmoji = 0;
  3707. if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
  3708. isEmoji = 1;
  3709. if (xs_type(content) == XSTYPE_STRING) {
  3710. if (!isEmoji && !n_likes_on)
  3711. continue;
  3712. xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy);
  3713. wrk = xs_fmt("%s (%s&#xFE0F;)", isEmoji ? "EmojiReact" : "Like", emoji);
  3714. label = wrk;
  3715. }
  3716. }
  3717. else
  3718. if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id))
  3719. label = L("Follow Request");
  3720. xs *s_date = html_date_label(user, date);
  3721. xs_html *this_html_label = xs_html_container(
  3722. xs_html_tag("b",
  3723. xs_html_raw(label),
  3724. xs_html_text(" by "),
  3725. xs_html_tag("a",
  3726. xs_html_attr("href", html_url),
  3727. xs_html_raw(a_name))), /* a_name is already sanitized */
  3728. xs_html_text(" "),
  3729. xs_html_tag("time",
  3730. xs_html_attr("class", "dt-published snac-pubdate"),
  3731. xs_html_attr("title", date),
  3732. xs_html_text(s_date)));
  3733. xs_html *html_label = NULL;
  3734. if (xs_is_string(msg_id)) {
  3735. const xs_val *prev_label = xs_dict_get(admiration_labels, msg_id);
  3736. if (xs_type(prev_label) == XSTYPE_DATA) {
  3737. /* there is a previous list of admiration labels! */
  3738. xs_data_get(&html_label, prev_label);
  3739. xs_html_add(html_label,
  3740. xs_html_sctag("br", NULL),
  3741. this_html_label);
  3742. continue;
  3743. }
  3744. }
  3745. xs_html *entry = NULL;
  3746. html_label = xs_html_tag("p",
  3747. this_html_label);
  3748. /* store in the admiration labels dict */
  3749. xs *pl = xs_data_new(&html_label, sizeof(html_label));
  3750. if (xs_is_string(msg_id))
  3751. admiration_labels = xs_dict_set(admiration_labels, msg_id, pl);
  3752. entry = xs_html_tag("div",
  3753. xs_html_attr("class", "snac-post-with-desc"),
  3754. html_label);
  3755. if (strcmp(type, "Block") == 0) {
  3756. if (actor)
  3757. xs_html_add(entry,
  3758. xs_html_tag("div",
  3759. xs_html_attr("class", "snac-post"),
  3760. html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
  3761. }
  3762. else
  3763. if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0) {
  3764. if (actor) {
  3765. xs *action = xs_fmt("%s/admin/action", user->actor);
  3766. xs_html *button = NULL;
  3767. if (following_check(user, actor_id))
  3768. button = html_button("unfollow", L("Unfollow"), L("Stop following this user's activity"));
  3769. else
  3770. button = html_button("follow", L("Follow"), L("Start following this user's activity"));
  3771. xs_html_add(entry,
  3772. xs_html_tag("div",
  3773. xs_html_attr("class", "snac-post"),
  3774. html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL),
  3775. xs_html_tag("form",
  3776. xs_html_attr("method", "post"),
  3777. xs_html_attr("action", action),
  3778. xs_html_sctag("input",
  3779. xs_html_attr("type", "hidden"),
  3780. xs_html_attr("name", "actor"),
  3781. xs_html_attr("value", actor_id)),
  3782. button,
  3783. xs_html_sctag("br", NULL))));
  3784. }
  3785. }
  3786. else
  3787. if (strcmp(type, "Move") == 0) {
  3788. const xs_dict *o_msg = xs_dict_get(noti, "msg");
  3789. const char *target;
  3790. if (xs_type(o_msg) == XSTYPE_DICT && (target = xs_dict_get(o_msg, "target"))) {
  3791. xs *old_actor = NULL;
  3792. if (valid_status(actor_get(target, &old_actor))) {
  3793. xs_html_add(entry,
  3794. xs_html_tag("div",
  3795. xs_html_attr("class", "snac-post"),
  3796. html_actor_icon(user, old_actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
  3797. }
  3798. }
  3799. }
  3800. else
  3801. if (obj != NULL) {
  3802. xs *md5 = xs_md5_hex(id, strlen(id));
  3803. xs *ctxt = xs_fmt("%s/admin/p/%s#%s_entry", user->actor, md5, md5);
  3804. xs_html *h = html_entry(user, obj, 0, 0, md5, 1);
  3805. if (h != NULL) {
  3806. xs_html_add(entry,
  3807. xs_html_tag("p",
  3808. xs_html_tag("a",
  3809. xs_html_attr("href", ctxt),
  3810. xs_html_text(L("Context")))),
  3811. h);
  3812. }
  3813. else
  3814. xs_html_add(entry,
  3815. xs_html_tag("p",
  3816. xs_html_text(L("Location: ")),
  3817. xs_html_tag("a",
  3818. xs_html_attr("href", id),
  3819. xs_html_text(id))));
  3820. }
  3821. if (strcmp(v, n_time) > 0) {
  3822. /* unseen notification */
  3823. if (noti_new == NULL) {
  3824. noti_new = xs_html_tag("div",
  3825. xs_html_tag("h2",
  3826. xs_html_attr("class", "snac-header"),
  3827. xs_html_text(L("New"))));
  3828. xs_html_add(posts,
  3829. noti_new);
  3830. }
  3831. xs_html_add(noti_new,
  3832. entry);
  3833. }
  3834. else {
  3835. /* already seen notification */
  3836. if (noti_seen == NULL) {
  3837. noti_seen = xs_html_tag("div",
  3838. xs_html_tag("h2",
  3839. xs_html_attr("class", "snac-header"),
  3840. xs_html_text(L("Already seen"))));
  3841. xs_html_add(posts,
  3842. noti_seen);
  3843. }
  3844. xs_html_add(noti_seen,
  3845. entry);
  3846. }
  3847. }
  3848. if (noti_new == NULL && noti_seen == NULL)
  3849. xs_html_add(body,
  3850. xs_html_tag("h2",
  3851. xs_html_attr("class", "snac-header"),
  3852. xs_html_text(L("None"))));
  3853. /* add the navigation footer */
  3854. xs *next_p = notify_list(user, skip + show, 1);
  3855. if (xs_list_len(next_p)) {
  3856. xs *url = xs_fmt("%s/notifications?skip=%d&show=%d",
  3857. user->actor, skip + show, show);
  3858. xs_html_add(body,
  3859. xs_html_tag("p",
  3860. xs_html_tag("a",
  3861. xs_html_attr("href", url),
  3862. xs_html_text(L("More...")))));
  3863. }
  3864. xs_set_free(&rep);
  3865. xs_html_add(body,
  3866. html_footer(user));
  3867. /* set the check time to now */
  3868. xs *dummy = notify_check_time(user, 1);
  3869. dummy = xs_free(dummy);
  3870. timeline_touch(user);
  3871. return xs_html_render_s(html, "<!DOCTYPE html>\n");
  3872. }
  3873. void set_user_lang(snac *user)
  3874. /* sets the language dict according to user configuration */
  3875. {
  3876. user->lang = NULL;
  3877. const char *lang = xs_dict_get(user->config, "lang");
  3878. if (xs_is_string(lang))
  3879. user->lang = xs_dict_get(srv_langs, lang);
  3880. }
  3881. int html_get_handler(const xs_dict *req, const char *q_path,
  3882. char **body, int *b_size, char **ctype,
  3883. xs_str **etag, xs_str **last_modified)
  3884. {
  3885. const char *accept = xs_dict_get(req, "accept");
  3886. int status = HTTP_STATUS_NOT_FOUND;
  3887. const snac *user = NULL;
  3888. snac snac;
  3889. xs *uid = NULL;
  3890. const char *p_path;
  3891. int cache = 1;
  3892. int save = 1;
  3893. int proxy = 0;
  3894. int terse = 0;
  3895. const char *v;
  3896. const xs_dict *q_vars = xs_dict_get(req, "q_vars");
  3897. xs *l = xs_split_n(q_path, "/", 2);
  3898. v = xs_list_get(l, 1);
  3899. if (xs_is_null(v)) {
  3900. srv_log(xs_fmt("html_get_handler bad query '%s'", q_path));
  3901. return HTTP_STATUS_NOT_FOUND;
  3902. }
  3903. if (!xs_is_null(xs_dict_get(q_vars, "terse")))
  3904. terse = 1;
  3905. if (strcmp(v, "share-bridge") == 0) {
  3906. /* temporary redirect for a post */
  3907. const char *login = xs_dict_get(q_vars, "login");
  3908. const char *content = xs_dict_get(q_vars, "content");
  3909. if (xs_type(login) == XSTYPE_STRING && xs_type(content) == XSTYPE_STRING) {
  3910. xs *b64 = xs_base64_enc(content, strlen(content));
  3911. srv_log(xs_fmt("share-bridge for user '%s'", login));
  3912. *body = xs_fmt("%s/%s/share?content=%s", srv_baseurl, login, b64);
  3913. return HTTP_STATUS_SEE_OTHER;
  3914. }
  3915. else
  3916. return HTTP_STATUS_NOT_FOUND;
  3917. }
  3918. else
  3919. if (strcmp(v, "auth-int-bridge") == 0) {
  3920. const char *login = xs_dict_get(q_vars, "login");
  3921. const char *id = xs_dict_get(q_vars, "id");
  3922. const char *action = xs_dict_get(q_vars, "action");
  3923. if (xs_is_string(login) && xs_is_string(id) && xs_is_string(action)) {
  3924. *body = xs_fmt("%s/%s/authorize_interaction?action=%s&id=%s",
  3925. srv_baseurl, login, action, id);
  3926. return HTTP_STATUS_SEE_OTHER;
  3927. }
  3928. else
  3929. return HTTP_STATUS_NOT_FOUND;
  3930. }
  3931. uid = xs_dup(v);
  3932. /* rss extension? */
  3933. if (xs_endswith(uid, ".rss")) {
  3934. uid = xs_crop_i(uid, 0, -4);
  3935. p_path = ".rss";
  3936. }
  3937. else
  3938. p_path = xs_list_get(l, 2);
  3939. if (!uid || !user_open(&snac, uid)) {
  3940. /* invalid user */
  3941. status = grave(uid, 0) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND;
  3942. srv_debug(2, xs_fmt("html_get_handler bad user '%s' %d", xs_or(uid, "(null)"), status));
  3943. return status;
  3944. }
  3945. user = &snac; /* for L() */
  3946. set_user_lang(&snac);
  3947. if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
  3948. proxy = 1;
  3949. /* return the RSS if requested by Accept header */
  3950. if (accept != NULL) {
  3951. if (xs_str_in(accept, "text/xml") != -1 ||
  3952. xs_str_in(accept, "application/rss+xml") != -1)
  3953. p_path = ".rss";
  3954. }
  3955. /* check if server config variable 'disable_cache' is set */
  3956. if ((v = xs_dict_get(srv_config, "disable_cache")) && xs_type(v) == XSTYPE_TRUE)
  3957. cache = 0;
  3958. int skip = 0;
  3959. const char *max_show_default = "50";
  3960. int max_show = xs_number_get(xs_dict_get_def(srv_config, "max_timeline_entries",
  3961. max_show_default));
  3962. int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries",
  3963. xs_dict_get_def(srv_config, "max_timeline_entries",
  3964. max_show_default)));
  3965. int show = def_show;
  3966. if ((v = xs_dict_get(q_vars, "skip")) != NULL)
  3967. skip = atoi(v), cache = 0, save = 0;
  3968. if ((v = xs_dict_get(q_vars, "show")) != NULL)
  3969. show = atoi(v), cache = 0, save = 0;
  3970. if ((v = xs_dict_get(q_vars, "da")) != NULL) {
  3971. /* user dismissed an announcement */
  3972. if (login(&snac, req)) {
  3973. double ts = atof(v);
  3974. xs *timestamp = xs_number_new(ts);
  3975. srv_log(xs_fmt("user dismissed announcements until %d", ts));
  3976. snac.config = xs_dict_set(snac.config, "last_announcement", timestamp);
  3977. user_persist(&snac, 0);
  3978. }
  3979. }
  3980. /* get a possible error message */
  3981. const char *error = xs_dict_get(q_vars, "error");
  3982. if (error != NULL)
  3983. cache = 0;
  3984. /* a show of 0 has no sense */
  3985. if (show == 0)
  3986. show = def_show;
  3987. if (show > max_show)
  3988. show = max_show;
  3989. if (p_path == NULL) { /** public timeline **/
  3990. xs *h = xs_str_localtime(0, "%Y-%m.html");
  3991. if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) {
  3992. /** empty public timeline for private users **/
  3993. *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error, terse);
  3994. *b_size = strlen(*body);
  3995. status = HTTP_STATUS_OK;
  3996. }
  3997. else
  3998. if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) {
  3999. snac_debug(&snac, 1, xs_fmt("serving cached local timeline"));
  4000. status = history_get(&snac, h, body, b_size,
  4001. xs_dict_get(req, "if-none-match"), etag);
  4002. }
  4003. else {
  4004. xs *list = NULL;
  4005. int more = 0;
  4006. if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")))
  4007. list = timeline_simple_list(&snac, "public", skip, show, &more);
  4008. else
  4009. list = timeline_list(&snac, "public", skip, show, &more);
  4010. xs *pins = pinned_list(&snac);
  4011. pins = xs_list_cat(pins, list);
  4012. *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error, terse);
  4013. *b_size = strlen(*body);
  4014. status = HTTP_STATUS_OK;
  4015. if (save)
  4016. history_add(&snac, h, *body, *b_size, etag);
  4017. }
  4018. }
  4019. else
  4020. if (strcmp(p_path, "admin") == 0) { /** private timeline **/
  4021. if (!login(&snac, req)) {
  4022. *body = xs_dup(uid);
  4023. status = HTTP_STATUS_UNAUTHORIZED;
  4024. }
  4025. else {
  4026. const char *q = NULL;
  4027. xs *cq = xs_dup(xs_dict_get(q_vars, "q"));
  4028. xs *url_acct = NULL;
  4029. if (xs_is_string(cq)) {
  4030. cq = xs_strip_i(cq);
  4031. q = cq;
  4032. }
  4033. /* searching for an URL? */
  4034. if (q && xs_match(q, "https://*|http://*")) {
  4035. /* bring it to the user's timeline */
  4036. xs *actor_obj = NULL;
  4037. xs *object = NULL;
  4038. int status;
  4039. status = activitypub_request(&snac, q, &object);
  4040. snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status));
  4041. if (valid_status(status) && xs_is_dict(object)) {
  4042. /* got it; also request the actor */
  4043. const char *attr_to = get_atto(object);
  4044. if (!xs_is_null(attr_to)) {
  4045. status = actor_request(&snac, attr_to, &actor_obj);
  4046. if (valid_status(status)) {
  4047. /* reset the query string to be the real id */
  4048. url_acct = xs_dup(xs_dict_get(object, "id"));
  4049. q = url_acct;
  4050. /* add the post to the timeline */
  4051. if (!timeline_here(&snac, q))
  4052. timeline_add(&snac, q, object);
  4053. }
  4054. }
  4055. else {
  4056. /* retry webfinger, this time with the 'official' id */
  4057. const char *id = xs_dict_get(object, "id");
  4058. if (xs_is_string(id) && valid_status(webfinger_request_fake(id, &actor_obj, &url_acct)) &&
  4059. xs_is_string(url_acct))
  4060. q = url_acct;
  4061. }
  4062. }
  4063. else
  4064. /* may by an actor; try a webfinger */
  4065. if (valid_status(webfinger_request(q, &actor_obj, &url_acct)) && xs_is_string(url_acct)) {
  4066. /* it's an actor; do the dirty trick of changing q to the account name */
  4067. q = url_acct;
  4068. }
  4069. /* fall through */
  4070. }
  4071. if (q && *q) {
  4072. if (xs_regex_match(q, "^@?[^@]+@[a-zA-Z0-9-]+\\.")) {
  4073. /** search account **/
  4074. xs *actor = NULL;
  4075. xs *acct = NULL;
  4076. xs *l = xs_list_new();
  4077. xs_html *page = NULL;
  4078. if (valid_status(webfinger_request(q, &actor, &acct))) {
  4079. xs *actor_obj = NULL;
  4080. if (valid_status(actor_request(&snac, actor, &actor_obj))) {
  4081. actor_add(actor, actor_obj);
  4082. /* create a people list with only one element */
  4083. l = xs_list_append(l, actor);
  4084. xs *title = xs_fmt(L("Search results for account %s"), q);
  4085. page = html_people_list(&snac, l, title, "wf", NULL, 1);
  4086. }
  4087. }
  4088. if (page == NULL) {
  4089. xs *title = xs_fmt(L("Account %s not found"), q);
  4090. page = xs_html_tag("div",
  4091. xs_html_tag("h2",
  4092. xs_html_attr("class", "snac-header"),
  4093. xs_html_text(title)));
  4094. }
  4095. xs_html *html = xs_html_tag("html",
  4096. html_user_head(&snac, NULL, NULL),
  4097. xs_html_add(html_user_body(&snac, 0),
  4098. page,
  4099. html_footer(user)));
  4100. *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
  4101. *b_size = strlen(*body);
  4102. status = HTTP_STATUS_OK;
  4103. }
  4104. else
  4105. if (*q == '#') {
  4106. /** search by tag **/
  4107. xs *tl = tag_search(q, skip, show + 1);
  4108. int more = 0;
  4109. if (xs_list_len(tl) >= show + 1) {
  4110. /* drop the last one */
  4111. tl = xs_list_del(tl, -1);
  4112. more = 1;
  4113. }
  4114. xs *page = xs_fmt("/admin?q=%%23%s", q + 1);
  4115. xs *title = xs_fmt(xs_list_len(tl) ?
  4116. L("Search results for tag %s") : L("Nothing found for tag %s"), q);
  4117. *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error, terse);
  4118. *b_size = strlen(*body);
  4119. status = HTTP_STATUS_OK;
  4120. }
  4121. else {
  4122. /** search by content **/
  4123. int to = 0;
  4124. int msecs = atoi(xs_dict_get_def(q_vars, "msecs", "0"));
  4125. xs *tl = content_search(&snac, q, 1, skip, show, msecs, &to);
  4126. xs *title = NULL;
  4127. xs *page = xs_fmt("/admin?q=%s&msecs=%d", q, msecs + 10);
  4128. int tl_len = xs_list_len(tl);
  4129. if (to)
  4130. title = xs_fmt(L("Search results for '%s' (may be more)"), q);
  4131. else
  4132. if (tl_len)
  4133. title = xs_fmt(L("Search results for '%s'"), q);
  4134. else
  4135. if (skip)
  4136. title = xs_fmt(L("No more matches for '%s'"), q);
  4137. else
  4138. title = xs_fmt(L("Nothing found for '%s'"), q);
  4139. *body = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show,
  4140. title, page, 0, error, terse);
  4141. *b_size = strlen(*body);
  4142. status = HTTP_STATUS_OK;
  4143. }
  4144. }
  4145. else {
  4146. /** the private timeline **/
  4147. double t = history_mtime(&snac, "timeline.html_");
  4148. /* if enabled by admin, return a cached page if its timestamp is:
  4149. a) newer than the timeline timestamp
  4150. b) newer than the start time of the server
  4151. */
  4152. if (cache && t > timeline_mtime(&snac) && t > p_state->srv_start_time) {
  4153. snac_debug(&snac, 1, xs_fmt("serving cached timeline"));
  4154. status = history_get(&snac, "timeline.html_", body, b_size,
  4155. xs_dict_get(req, "if-none-match"), etag);
  4156. }
  4157. else {
  4158. int more = 0;
  4159. snac_debug(&snac, 1, xs_fmt("building timeline"));
  4160. xs *list = timeline_list(&snac, "private", skip, show, &more);
  4161. *body = html_timeline(&snac, list, 0, skip, show,
  4162. more, NULL, "/admin", 1, error, terse);
  4163. *b_size = strlen(*body);
  4164. status = HTTP_STATUS_OK;
  4165. if (save)
  4166. history_add(&snac, "timeline.html_", *body, *b_size, etag);
  4167. timeline_add_mark(&snac);
  4168. }
  4169. }
  4170. }
  4171. }
  4172. else
  4173. if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/
  4174. if (!login(&snac, req)) {
  4175. *body = xs_dup(uid);
  4176. status = HTTP_STATUS_UNAUTHORIZED;
  4177. }
  4178. else {
  4179. xs *l = xs_split(p_path, "/");
  4180. const char *md5 = xs_list_get(l, -1);
  4181. if (md5 && *md5 && timeline_here_by_md5(&snac, md5)) {
  4182. xs *list0 = xs_list_append(xs_list_new(), md5);
  4183. xs *list = timeline_top_level(&snac, list0);
  4184. *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error, terse);
  4185. *b_size = strlen(*body);
  4186. status = HTTP_STATUS_OK;
  4187. }
  4188. }
  4189. }
  4190. else
  4191. if (strcmp(p_path, "people") == 0) { /** the list of people **/
  4192. if (!login(&snac, req)) {
  4193. *body = xs_dup(uid);
  4194. status = HTTP_STATUS_UNAUTHORIZED;
  4195. }
  4196. else {
  4197. *body = html_people(&snac);
  4198. *b_size = strlen(*body);
  4199. status = HTTP_STATUS_OK;
  4200. }
  4201. }
  4202. else
  4203. if (xs_startswith(p_path, "people/")) { /** a single actor **/
  4204. if (!login(&snac, req)) {
  4205. *body = xs_dup(uid);
  4206. status = HTTP_STATUS_UNAUTHORIZED;
  4207. }
  4208. else {
  4209. xs *actor_dict = NULL;
  4210. const char *actor_id = NULL;
  4211. xs *actor = NULL;
  4212. xs_list *page_lst = xs_split_n(p_path, "?", 2);
  4213. xs *page = xs_str_cat(xs_str_new("/"), xs_list_get(page_lst, 0));
  4214. xs_list *l = xs_split_n(page, "/", 3);
  4215. const char *actor_md5 = xs_list_get(l, 2);
  4216. if (valid_status(object_get_by_md5(actor_md5, &actor_dict)) &&
  4217. (actor_id = xs_dict_get(actor_dict, "id")) != NULL &&
  4218. valid_status(actor_get(actor_id, &actor))) {
  4219. int more = 0;
  4220. xs *list = timeline_simple_list(&snac, "private", skip, show, &more);
  4221. *body = html_people_one(&snac, actor_id, list, skip, show, more, page);
  4222. *b_size = strlen(*body);
  4223. status = HTTP_STATUS_OK;
  4224. }
  4225. else {
  4226. *body = xs_dup(uid);
  4227. status = HTTP_STATUS_NOT_FOUND;
  4228. }
  4229. }
  4230. }
  4231. else
  4232. if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
  4233. if (!login(&snac, req)) {
  4234. *body = xs_dup(uid);
  4235. status = HTTP_STATUS_UNAUTHORIZED;
  4236. }
  4237. else {
  4238. *body = html_notifications(&snac, skip, show);
  4239. *b_size = strlen(*body);
  4240. status = HTTP_STATUS_OK;
  4241. }
  4242. }
  4243. else
  4244. if (strcmp(p_path, "instance") == 0) { /** instance timeline **/
  4245. if (!login(&snac, req)) {
  4246. *body = xs_dup(uid);
  4247. status = HTTP_STATUS_UNAUTHORIZED;
  4248. }
  4249. else {
  4250. xs *list = timeline_instance_list(skip, show);
  4251. xs *next = timeline_instance_list(skip + show, 1);
  4252. *body = html_timeline(&snac, list, 0, skip, show,
  4253. xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error, terse);
  4254. *b_size = strlen(*body);
  4255. status = HTTP_STATUS_OK;
  4256. }
  4257. }
  4258. else
  4259. if (strcmp(p_path, "pinned") == 0) { /** list of pinned posts **/
  4260. if (!login(&snac, req)) {
  4261. *body = xs_dup(uid);
  4262. status = HTTP_STATUS_UNAUTHORIZED;
  4263. }
  4264. else {
  4265. xs *list = pinned_list(&snac);
  4266. *body = html_timeline(&snac, list, 0, skip, show,
  4267. 0, L("Pinned posts"), "", 0, error, terse);
  4268. *b_size = strlen(*body);
  4269. status = HTTP_STATUS_OK;
  4270. }
  4271. }
  4272. else
  4273. if (strcmp(p_path, "admirations") == 0) { /** list of admired posts **/
  4274. if (!login(&snac, req)) {
  4275. *body = xs_dup(uid);
  4276. status = HTTP_STATUS_UNAUTHORIZED;
  4277. }
  4278. else {
  4279. int more = 0;
  4280. xs *list = timeline_list(&snac, "admire", skip, show, &more);
  4281. *body = html_timeline(&snac, list, 0, skip, show,
  4282. more, L("Liked, boosted or reacted posts"), "/admirations", 0, error, terse);
  4283. *b_size = strlen(*body);
  4284. status = HTTP_STATUS_OK;
  4285. }
  4286. }
  4287. else
  4288. if (strcmp(p_path, "bookmarks") == 0) { /** list of bookmarked posts **/
  4289. if (!login(&snac, req)) {
  4290. *body = xs_dup(uid);
  4291. status = HTTP_STATUS_UNAUTHORIZED;
  4292. }
  4293. else {
  4294. xs *list = bookmark_list(&snac);
  4295. *body = html_timeline(&snac, list, 0, skip, show,
  4296. 0, L("Bookmarked posts"), "", 0, error, terse);
  4297. *b_size = strlen(*body);
  4298. status = HTTP_STATUS_OK;
  4299. }
  4300. }
  4301. else
  4302. if (strcmp(p_path, "drafts") == 0) { /** list of drafts **/
  4303. if (!login(&snac, req)) {
  4304. *body = xs_dup(uid);
  4305. status = HTTP_STATUS_UNAUTHORIZED;
  4306. }
  4307. else {
  4308. xs *list = draft_list(&snac);
  4309. *body = html_timeline(&snac, list, 0, skip, show,
  4310. 0, L("Post drafts"), "", 0, error, terse);
  4311. *b_size = strlen(*body);
  4312. status = HTTP_STATUS_OK;
  4313. }
  4314. }
  4315. else
  4316. if (strcmp(p_path, "sched") == 0) { /** list of scheduled posts **/
  4317. if (!login(&snac, req)) {
  4318. *body = xs_dup(uid);
  4319. status = HTTP_STATUS_UNAUTHORIZED;
  4320. }
  4321. else {
  4322. xs *list = scheduled_list(&snac);
  4323. *body = html_timeline(&snac, list, 0, skip, show,
  4324. 0, L("Scheduled posts"), "", 0, error, terse);
  4325. *b_size = strlen(*body);
  4326. status = HTTP_STATUS_OK;
  4327. }
  4328. }
  4329. else
  4330. if (xs_startswith(p_path, "list/")) { /** list timelines **/
  4331. if (!login(&snac, req)) {
  4332. *body = xs_dup(uid);
  4333. status = HTTP_STATUS_UNAUTHORIZED;
  4334. }
  4335. else {
  4336. xs *l = xs_split(p_path, "/");
  4337. const char *lid = xs_list_get(l, -1);
  4338. xs *list = list_timeline(&snac, lid, skip, show);
  4339. xs *next = list_timeline(&snac, lid, skip + show, 1);
  4340. if (list != NULL) {
  4341. xs *ttl = timeline_top_level(&snac, list);
  4342. xs *base = xs_fmt("/list/%s", lid);
  4343. xs *name = list_maint(&snac, lid, 3);
  4344. xs *title = xs_fmt(L("Showing timeline for list '%s'"), name);
  4345. *body = html_timeline(&snac, ttl, 0, skip, show,
  4346. xs_list_len(next), title, base, 1, error, terse);
  4347. *b_size = strlen(*body);
  4348. status = HTTP_STATUS_OK;
  4349. }
  4350. }
  4351. }
  4352. else
  4353. if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
  4354. if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
  4355. return HTTP_STATUS_FORBIDDEN;
  4356. xs *id = xs_fmt("%s/%s", snac.actor, p_path);
  4357. xs *msg = NULL;
  4358. if (valid_status(object_get(id, &msg)) && is_msg_public(msg)) {
  4359. xs *md5 = xs_md5_hex(id, strlen(id));
  4360. xs *list = xs_list_new();
  4361. list = xs_list_append(list, md5);
  4362. *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error, terse);
  4363. *b_size = strlen(*body);
  4364. status = HTTP_STATUS_OK;
  4365. }
  4366. }
  4367. else
  4368. if (xs_startswith(p_path, "s/")) { /** a static file **/
  4369. xs *l = xs_split_n(p_path, "/", 1);
  4370. const char *id = xs_list_get(l, 1);
  4371. int sz;
  4372. if (id && *id) {
  4373. status = static_get(&snac, id, body, &sz,
  4374. xs_dict_get(req, "if-none-match"), etag);
  4375. if (valid_status(status)) {
  4376. *b_size = sz;
  4377. *ctype = (char *)xs_mime_by_ext(id);
  4378. }
  4379. }
  4380. }
  4381. else
  4382. if (xs_startswith(p_path, "h/")) { /** an entry from the history **/
  4383. if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
  4384. return HTTP_STATUS_FORBIDDEN;
  4385. if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
  4386. return HTTP_STATUS_FORBIDDEN;
  4387. xs *l = xs_split(p_path, "/");
  4388. const char *id = xs_list_get(l, 1);
  4389. if (id && *id) {
  4390. if (xs_endswith(id, "timeline.html_")) {
  4391. /* Don't let them in */
  4392. *b_size = 0;
  4393. status = HTTP_STATUS_NOT_FOUND;
  4394. }
  4395. else
  4396. status = history_get(&snac, id, body, b_size,
  4397. xs_dict_get(req, "if-none-match"), etag);
  4398. }
  4399. }
  4400. else
  4401. if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/
  4402. if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
  4403. return HTTP_STATUS_FORBIDDEN;
  4404. int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
  4405. xs *elems = timeline_simple_list(&snac, "public", 0, cnt, NULL);
  4406. xs *bio = xs_dup(xs_dict_get(snac.config, "bio"));
  4407. xs *rss_title = xs_fmt("%s (@%s@%s)",
  4408. xs_dict_get(snac.config, "name"),
  4409. snac.uid,
  4410. xs_dict_get(srv_config, "host"));
  4411. xs *rss_link = xs_fmt("%s.rss", snac.actor);
  4412. *body = rss_from_timeline(&snac, elems, rss_title, rss_link, bio);
  4413. *b_size = strlen(*body);
  4414. *ctype = "application/rss+xml; charset=utf-8";
  4415. status = HTTP_STATUS_OK;
  4416. snac_debug(&snac, 1, xs_fmt("serving RSS"));
  4417. }
  4418. else
  4419. if (proxy && (xs_startswith(p_path, "x/") || xs_startswith(p_path, "y/"))) { /** remote media by proxy **/
  4420. xs *proxy_prefix = NULL;
  4421. if (xs_startswith(p_path, "x/")) {
  4422. /* proxy usage authorized by http basic auth */
  4423. if (login(&snac, req))
  4424. proxy_prefix = xs_str_new("x/");
  4425. else {
  4426. *body = xs_dup(uid);
  4427. status = HTTP_STATUS_UNAUTHORIZED;
  4428. }
  4429. }
  4430. else {
  4431. /* proxy usage authorized by proxy_token */
  4432. xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, snac.actor);
  4433. xs *tk = xs_md5_hex(tks, strlen(tks));
  4434. xs *p = xs_fmt("y/%s/", tk);
  4435. if (xs_startswith(p_path, p))
  4436. proxy_prefix = xs_dup(p);
  4437. }
  4438. if (proxy_prefix) {
  4439. /* pick the raw path (including optional ? arguments) */
  4440. const char *raw_path = xs_dict_get(req, "raw_path");
  4441. /* skip to where the proxy/ string starts */
  4442. raw_path += xs_str_in(raw_path, proxy_prefix);
  4443. xs *url = xs_replace_n(raw_path, proxy_prefix, "https:/" "/", 1);
  4444. xs *hdrs = xs_dict_new();
  4445. hdrs = xs_dict_append(hdrs, "user-agent", USER_AGENT);
  4446. const char *ims = xs_dict_get(req, "if-modified-since");
  4447. const char *inm = xs_dict_get(req, "if-none-match");
  4448. if (ims) hdrs = xs_dict_append(hdrs, "if-modified-since", ims);
  4449. if (inm) hdrs = xs_dict_append(hdrs, "if-none-match", inm);
  4450. xs *rsp = xs_http_request("GET", url, hdrs,
  4451. NULL, 0, &status, body, b_size, 0);
  4452. if (valid_status(status)) {
  4453. const char *ct = xs_or(xs_dict_get(rsp, "content-type"), "");
  4454. const char *lm = xs_dict_get(rsp, "last-modified");
  4455. const char *et = xs_dict_get(rsp, "etag");
  4456. if (lm) *last_modified = xs_dup(lm);
  4457. if (et) *etag = xs_dup(et);
  4458. /* find the content-type in the static mime types,
  4459. and return that value instead of ct, which will
  4460. be destroyed when out of scope */
  4461. for (int n = 0; xs_mime_types[n]; n += 2) {
  4462. if (strcmp(ct, xs_mime_types[n + 1]) == 0) {
  4463. *ctype = (char *)xs_mime_types[n + 1];
  4464. break;
  4465. }
  4466. }
  4467. }
  4468. snac_debug(&snac, 1, xs_fmt("Proxy for %s %d", url, status));
  4469. }
  4470. }
  4471. else
  4472. if (strcmp(p_path, "share") == 0) { /** direct post **/
  4473. if (!login(&snac, req)) {
  4474. *body = xs_dup(uid);
  4475. status = HTTP_STATUS_UNAUTHORIZED;
  4476. }
  4477. else {
  4478. const char *b64 = xs_dict_get(q_vars, "content");
  4479. xs *b64_2 = xs_replace(b64, " ", "+");
  4480. int sz;
  4481. xs *content = xs_base64_dec(b64_2, &sz);
  4482. xs *msg = msg_note(&snac, content, NULL, NULL, NULL, 0, NULL, NULL);
  4483. xs *c_msg = msg_create(&snac, msg);
  4484. timeline_add(&snac, xs_dict_get(msg, "id"), msg);
  4485. enqueue_message(&snac, c_msg);
  4486. snac_debug(&snac, 1, xs_fmt("web action 'share' received"));
  4487. *body = xs_fmt("%s/admin", snac.actor);
  4488. *b_size = strlen(*body);
  4489. status = HTTP_STATUS_SEE_OTHER;
  4490. }
  4491. }
  4492. else
  4493. if (strcmp(p_path, "authorize_interaction") == 0) { /** follow, like or boost from Mastodon **/
  4494. if (!login(&snac, req)) {
  4495. *body = xs_dup(uid);
  4496. status = HTTP_STATUS_UNAUTHORIZED;
  4497. }
  4498. else {
  4499. status = HTTP_STATUS_NOT_FOUND;
  4500. const char *id = xs_dict_get(q_vars, "id");
  4501. const char *action = xs_dict_get(q_vars, "action");
  4502. if (xs_is_string(id) && xs_is_string(action)) {
  4503. if (strcmp(action, "Follow") == 0) {
  4504. xs *msg = msg_follow(&snac, id);
  4505. if (msg != NULL) {
  4506. const char *actor = xs_dict_get(msg, "object");
  4507. following_add(&snac, actor, msg);
  4508. enqueue_output_by_actor(&snac, msg, actor, 0);
  4509. status = HTTP_STATUS_SEE_OTHER;
  4510. }
  4511. }
  4512. else
  4513. if (xs_match(action, "Like|Boost|Announce")) {
  4514. /* bring the post */
  4515. xs *msg = msg_admiration(&snac, id, *action == 'L' ? "Like" : "Announce");
  4516. if (msg != NULL) {
  4517. timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0, msg);
  4518. enqueue_message(&snac, msg);
  4519. status = HTTP_STATUS_SEE_OTHER;
  4520. }
  4521. }
  4522. }
  4523. if (status == HTTP_STATUS_SEE_OTHER) {
  4524. *body = xs_fmt("%s/admin", snac.actor);
  4525. *b_size = strlen(*body);
  4526. }
  4527. }
  4528. }
  4529. else
  4530. status = HTTP_STATUS_NOT_FOUND;
  4531. user_free(&snac);
  4532. if (valid_status(status) && *ctype == NULL) {
  4533. *ctype = "text/html; charset=utf-8";
  4534. }
  4535. return status;
  4536. }
  4537. int html_post_handler(const xs_dict *req, const char *q_path,
  4538. char *payload, int p_size,
  4539. char **body, int *b_size, char **ctype)
  4540. {
  4541. (void)p_size;
  4542. (void)ctype;
  4543. int status = 0;
  4544. const snac *user = NULL;
  4545. snac snac;
  4546. const char *uid;
  4547. const char *p_path;
  4548. const xs_dict *p_vars;
  4549. xs *l = xs_split_n(q_path, "/", 2);
  4550. uid = xs_list_get(l, 1);
  4551. if (!uid || !user_open(&snac, uid)) {
  4552. /* invalid user */
  4553. srv_debug(1, xs_fmt("html_post_handler bad user %s", xs_or(uid, "(null)")));
  4554. return HTTP_STATUS_NOT_FOUND;
  4555. }
  4556. p_path = xs_list_get(l, 2);
  4557. /* all posts must be authenticated */
  4558. if (!login(&snac, req)) {
  4559. user_free(&snac);
  4560. *body = xs_dup(uid);
  4561. return HTTP_STATUS_UNAUTHORIZED;
  4562. }
  4563. user = &snac; /* for L() */
  4564. set_user_lang(&snac);
  4565. p_vars = xs_dict_get(req, "p_vars");
  4566. if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/
  4567. snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path));
  4568. /* post note */
  4569. const char *content = xs_dict_get(p_vars, "content");
  4570. const char *in_reply_to = xs_dict_get(p_vars, "in_reply_to");
  4571. const char *to = xs_dict_get(p_vars, "to");
  4572. const char *sensitive = xs_dict_get(p_vars, "sensitive");
  4573. const char *summary = xs_dict_get(p_vars, "summary");
  4574. const char *edit_id = xs_dict_get(p_vars, "edit_id");
  4575. const char *post_date = xs_dict_get_def(p_vars, "post_date", "");
  4576. const char *post_time = xs_dict_get_def(p_vars, "post_time", "");
  4577. const char *post_lang = xs_dict_get(p_vars, "post_lang");
  4578. const char *visibility = xs_dict_get(p_vars, "visibility");
  4579. int scope = SCOPE_PUBLIC; /* default to public */
  4580. if (!xs_is_null(visibility)) {
  4581. if (strcmp(visibility, "unlisted") == 0)
  4582. scope = SCOPE_UNLISTED;
  4583. else
  4584. if (strcmp(visibility, "followers") == 0)
  4585. scope = SCOPE_FOLLOWERS;
  4586. else
  4587. if (strcmp(visibility, "mentioned") == 0)
  4588. scope = SCOPE_MENTIONED;
  4589. }
  4590. int store_as_draft = !xs_is_null(xs_dict_get(p_vars, "is_draft"));
  4591. xs *attach_list = xs_list_new();
  4592. /* iterate the attachments */
  4593. int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4"));
  4594. for (int att_n = 0; att_n < max_attachments; att_n++) {
  4595. xs *url_lbl = xs_fmt("attach_url_%d", att_n);
  4596. xs *att_lbl = xs_fmt("attach_%d", att_n);
  4597. xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
  4598. const char *attach_url = xs_dict_get(p_vars, url_lbl);
  4599. const xs_list *attach_file = xs_dict_get(p_vars, att_lbl);
  4600. const char *alt_text = xs_dict_get_def(p_vars, alt_lbl, "");
  4601. if (xs_is_string(attach_url) && *attach_url != '\0') {
  4602. xs *l = xs_list_new();
  4603. l = xs_list_append(l, attach_url);
  4604. l = xs_list_append(l, alt_text);
  4605. attach_list = xs_list_append(attach_list, l);
  4606. }
  4607. else
  4608. if (xs_is_list(attach_file)) {
  4609. const char *fn = xs_list_get(attach_file, 0);
  4610. if (xs_is_string(fn) && *fn != '\0') {
  4611. char rnd[32];
  4612. xs_rnd_buf(rnd, sizeof(rnd));
  4613. const char *ext = strrchr(fn, '.');
  4614. xs *hash = xs_md5_hex(rnd, sizeof(rnd));
  4615. xs *id = xs_fmt("post-%s%s", hash, ext ? ext : "");
  4616. xs *url = xs_fmt("%s/s/%s", snac.actor, id);
  4617. int fo = xs_number_get(xs_list_get(attach_file, 1));
  4618. int fs = xs_number_get(xs_list_get(attach_file, 2));
  4619. /* store */
  4620. static_put(&snac, id, payload + fo, fs);
  4621. xs *l = xs_list_new();
  4622. l = xs_list_append(l, url);
  4623. l = xs_list_append(l, alt_text);
  4624. attach_list = xs_list_append(attach_list, l);
  4625. }
  4626. }
  4627. }
  4628. if (content != NULL) {
  4629. xs *msg = NULL;
  4630. xs *c_msg = NULL;
  4631. xs *content_2 = xs_replace(content, "\r", "");
  4632. xs *poll_opts = NULL;
  4633. /* is there a valid set of poll options? */
  4634. const char *v = xs_dict_get(p_vars, "poll_options");
  4635. if (!xs_is_null(v) && *v) {
  4636. xs *v2 = xs_strip_i(xs_replace(v, "\r", ""));
  4637. poll_opts = xs_split(v2, "\n");
  4638. }
  4639. if (!xs_is_null(poll_opts) && xs_list_len(poll_opts)) {
  4640. /* get the rest of poll configuration */
  4641. const char *p_multiple = xs_dict_get(p_vars, "poll_multiple");
  4642. const char *p_end_secs = xs_dict_get(p_vars, "poll_end_secs");
  4643. int multiple = 0;
  4644. int end_secs = atoi(!xs_is_null(p_end_secs) ? p_end_secs : "60");
  4645. if (!xs_is_null(p_multiple) && strcmp(p_multiple, "on") == 0)
  4646. multiple = 1;
  4647. msg = msg_question(&snac, content_2, attach_list,
  4648. poll_opts, multiple, end_secs);
  4649. enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
  4650. }
  4651. else
  4652. msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, scope, post_lang, NULL);
  4653. if (sensitive != NULL) {
  4654. msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
  4655. msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary);
  4656. }
  4657. if (xs_is_string(post_date) && *post_date) {
  4658. xs *post_pubdate = xs_fmt("%sT%s", post_date,
  4659. xs_is_string(post_time) && *post_time ? post_time : "00:00:00");
  4660. time_t t = xs_parse_iso_date(post_pubdate, 0);
  4661. if (t != 0) {
  4662. t -= xs_tz_offset(snac.tz);
  4663. xs *iso_date = xs_str_iso_date(t);
  4664. msg = xs_dict_set(msg, "published", iso_date);
  4665. snac_debug(&snac, 1, xs_fmt("Published date: [%s]", iso_date));
  4666. }
  4667. else
  4668. snac_log(&snac, xs_fmt("Invalid post date: [%s]", post_pubdate));
  4669. }
  4670. /* is the published date from the future? */
  4671. int future_post = 0;
  4672. xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
  4673. if (strcmp(xs_dict_get(msg, "published"), right_now) > 0)
  4674. future_post = 1;
  4675. if (xs_is_null(edit_id)) {
  4676. /* new message */
  4677. const char *id = xs_dict_get(msg, "id");
  4678. if (store_as_draft) {
  4679. draft_add(&snac, id, msg);
  4680. }
  4681. else
  4682. if (future_post) {
  4683. schedule_add(&snac, id, msg);
  4684. }
  4685. else {
  4686. c_msg = msg_create(&snac, msg);
  4687. timeline_add(&snac, id, msg);
  4688. }
  4689. }
  4690. else {
  4691. /* an edition of a previous message */
  4692. xs *p_msg = NULL;
  4693. if (valid_status(object_get(edit_id, &p_msg))) {
  4694. /* copy relevant fields from previous version */
  4695. char *fields[] = { "id", "context", "url",
  4696. "cc", "attributedTo",
  4697. "to", "inReplyTo", NULL };
  4698. int n;
  4699. for (n = 0; fields[n]; n++) {
  4700. const char *v = xs_dict_get(p_msg, fields[n]);
  4701. msg = xs_dict_set(msg, fields[n], v);
  4702. }
  4703. if (store_as_draft) {
  4704. draft_add(&snac, edit_id, msg);
  4705. }
  4706. else
  4707. if (is_draft(&snac, edit_id)) {
  4708. /* message was previously a draft; it's a create activity */
  4709. /* if the date is from the past, overwrite it with right_now */
  4710. if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
  4711. snac_debug(&snac, 1, xs_fmt("setting draft ancient date to %s", right_now));
  4712. msg = xs_dict_set(msg, "published", right_now);
  4713. }
  4714. /* overwrite object */
  4715. object_add_ow(edit_id, msg);
  4716. if (future_post) {
  4717. schedule_add(&snac, edit_id, msg);
  4718. }
  4719. else {
  4720. c_msg = msg_create(&snac, msg);
  4721. timeline_add(&snac, edit_id, msg);
  4722. }
  4723. draft_del(&snac, edit_id);
  4724. }
  4725. else
  4726. if (is_scheduled(&snac, edit_id)) {
  4727. /* editing an scheduled post; just update it */
  4728. schedule_add(&snac, edit_id, msg);
  4729. }
  4730. else {
  4731. /* ignore the (possibly changed) published date */
  4732. msg = xs_dict_set(msg, "published", xs_dict_get(p_msg, "published"));
  4733. /* set the updated field */
  4734. xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
  4735. msg = xs_dict_set(msg, "updated", updated);
  4736. /* overwrite object, not updating the indexes */
  4737. object_add_ow(edit_id, msg);
  4738. /* index tags */
  4739. tag_index(edit_id, msg);
  4740. /* update message */
  4741. c_msg = msg_update(&snac, msg);
  4742. }
  4743. }
  4744. else
  4745. snac_log(&snac, xs_fmt("cannot get object '%s' for editing", edit_id));
  4746. }
  4747. if (c_msg != NULL) {
  4748. enqueue_message(&snac, c_msg);
  4749. enqueue_webmention(msg);
  4750. }
  4751. history_del(&snac, "timeline.html_");
  4752. }
  4753. status = HTTP_STATUS_SEE_OTHER;
  4754. }
  4755. else
  4756. if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
  4757. /* action on an entry */
  4758. const char *id = xs_dict_get(p_vars, "id");
  4759. const char *actor = xs_dict_get(p_vars, "actor");
  4760. const char *action = xs_dict_get(p_vars, "action");
  4761. const char *group = xs_dict_get(p_vars, "group");
  4762. if (action == NULL)
  4763. return HTTP_STATUS_NOT_FOUND;
  4764. snac_debug(&snac, 1, xs_fmt("web action '%s' received", action));
  4765. status = HTTP_STATUS_SEE_OTHER;
  4766. if (strcmp(action, L("EmojiUnreact")) == 0) { /** **/
  4767. const char *eid = xs_dict_get(p_vars, "eid");
  4768. if (eid != NULL) {
  4769. object_user_cache_del(&snac, id, "admire");
  4770. xs *n_msg = msg_emoji_unreact(&snac, id, eid);
  4771. if (n_msg != NULL)
  4772. enqueue_message(&snac, n_msg);
  4773. }
  4774. }
  4775. else
  4776. if (strcmp(action, L("EmojiReact")) == 0) { /** **/
  4777. xs *eid = xs_dup(xs_dict_get(p_vars, "eid"));
  4778. eid = xs_strip_chars_i(eid, ":");
  4779. xs *ret = msg_emoji_init(&snac, id, eid);
  4780. /* fails if either invalid or already reacted */
  4781. if (!ret) {
  4782. object_user_cache_del(&snac, id, "admire");
  4783. ret = msg_emoji_unreact(&snac, id, eid);
  4784. }
  4785. if (!ret)
  4786. status = HTTP_STATUS_NOT_FOUND;
  4787. }
  4788. else
  4789. if (strcmp(action, L("Like")) == 0) { /** **/
  4790. xs *msg = msg_admiration(&snac, id, "Like");
  4791. if (msg != NULL) {
  4792. timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1, msg);
  4793. enqueue_message(&snac, msg);
  4794. }
  4795. }
  4796. else
  4797. if (strcmp(action, L("Boost")) == 0) { /** **/
  4798. xs *msg = msg_admiration(&snac, id, "Announce");
  4799. if (msg != NULL) {
  4800. timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, msg);
  4801. enqueue_message(&snac, msg);
  4802. }
  4803. }
  4804. else
  4805. if (strcmp(action, L("Unlike")) == 0) { /** **/
  4806. xs *msg = msg_repulsion(&snac, id, "Like");
  4807. if (msg != NULL) {
  4808. object_user_cache_del(&snac, id, "admire");
  4809. enqueue_message(&snac, msg);
  4810. }
  4811. }
  4812. else
  4813. if (strcmp(action, L("Unboost")) == 0) { /** **/
  4814. xs *msg = msg_repulsion(&snac, id, "Announce");
  4815. if (msg != NULL) {
  4816. object_user_cache_del(&snac, id, "admire");
  4817. enqueue_message(&snac, msg);
  4818. }
  4819. }
  4820. else
  4821. if (strcmp(action, L("MUTE")) == 0) { /** **/
  4822. mute(&snac, actor);
  4823. }
  4824. else
  4825. if (strcmp(action, L("Unmute")) == 0) { /** **/
  4826. unmute(&snac, actor);
  4827. }
  4828. else
  4829. if (strcmp(action, L("Hide")) == 0) { /** **/
  4830. if (is_draft(&snac, id))
  4831. draft_del(&snac, id);
  4832. else
  4833. if (is_scheduled(&snac, id))
  4834. schedule_del(&snac, id);
  4835. else
  4836. hide(&snac, id);
  4837. }
  4838. else
  4839. if (strcmp(action, L("Limit")) == 0) { /** **/
  4840. limit(&snac, actor);
  4841. }
  4842. else
  4843. if (strcmp(action, L("Unlimit")) == 0) { /** **/
  4844. unlimit(&snac, actor);
  4845. }
  4846. else
  4847. if (strcmp(action, L("Follow")) == 0) { /** **/
  4848. xs *msg = msg_follow(&snac, actor);
  4849. if (msg != NULL) {
  4850. /* reload the actor from the message, in may be different */
  4851. actor = xs_dict_get(msg, "object");
  4852. following_add(&snac, actor, msg);
  4853. enqueue_output_by_actor(&snac, msg, actor, 0);
  4854. }
  4855. }
  4856. else
  4857. if (strcmp(action, L("Unfollow")) == 0) { /** **/
  4858. /* get the following object */
  4859. xs *object = NULL;
  4860. if (valid_status(following_get(&snac, actor, &object))) {
  4861. xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
  4862. following_del(&snac, actor);
  4863. enqueue_output_by_actor(&snac, msg, actor, 0);
  4864. snac_log(&snac, xs_fmt("unfollowed actor %s", actor));
  4865. }
  4866. else
  4867. snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
  4868. }
  4869. else
  4870. if (strcmp(action, L("Follow Group")) == 0) { /** **/
  4871. xs *msg = msg_follow(&snac, group);
  4872. if (msg != NULL) {
  4873. /* reload the group from the message, in may be different */
  4874. group = xs_dict_get(msg, "object");
  4875. following_add(&snac, group, msg);
  4876. enqueue_output_by_actor(&snac, msg, group, 0);
  4877. }
  4878. }
  4879. else
  4880. if (strcmp(action, L("Unfollow Group")) == 0) { /** **/
  4881. /* get the following object */
  4882. xs *object = NULL;
  4883. if (valid_status(following_get(&snac, group, &object))) {
  4884. xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
  4885. following_del(&snac, group);
  4886. enqueue_output_by_actor(&snac, msg, group, 0);
  4887. snac_log(&snac, xs_fmt("unfollowed group %s", group));
  4888. }
  4889. else
  4890. snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
  4891. }
  4892. else
  4893. if (strcmp(action, L("Delete")) == 0) { /** **/
  4894. const char *actor_form = xs_dict_get(p_vars, "actor-form");
  4895. if (actor_form != NULL) {
  4896. /* delete follower */
  4897. if (valid_status(follower_del(&snac, actor)))
  4898. snac_log(&snac, xs_fmt("deleted follower %s", actor));
  4899. else
  4900. snac_log(&snac, xs_fmt("error deleting follower %s", actor));
  4901. }
  4902. else {
  4903. /* delete an entry */
  4904. if (is_msg_mine(&snac, id) && !is_draft(&snac, id)) {
  4905. /* it's a post by us: generate a delete */
  4906. xs *msg = msg_delete(&snac, id);
  4907. enqueue_message(&snac, msg);
  4908. snac_log(&snac, xs_fmt("posted tombstone for %s", id));
  4909. }
  4910. timeline_del(&snac, id);
  4911. draft_del(&snac, id);
  4912. schedule_del(&snac, id);
  4913. snac_log(&snac, xs_fmt("deleted entry %s", id));
  4914. }
  4915. }
  4916. else
  4917. if (strcmp(action, L("Pin")) == 0) { /** **/
  4918. pin(&snac, id);
  4919. timeline_touch(&snac);
  4920. }
  4921. else
  4922. if (strcmp(action, L("Unpin")) == 0) { /** **/
  4923. unpin(&snac, id);
  4924. timeline_touch(&snac);
  4925. }
  4926. else
  4927. if (strcmp(action, L("Bookmark")) == 0) { /** **/
  4928. bookmark(&snac, id);
  4929. timeline_touch(&snac);
  4930. }
  4931. else
  4932. if (strcmp(action, L("Unbookmark")) == 0) { /** **/
  4933. unbookmark(&snac, id);
  4934. timeline_touch(&snac);
  4935. }
  4936. else
  4937. if (strcmp(action, L("Approve")) == 0) { /** **/
  4938. xs *fwreq = pending_get(&snac, actor);
  4939. if (fwreq != NULL) {
  4940. xs *reply = msg_accept(&snac, fwreq, actor);
  4941. enqueue_message(&snac, reply);
  4942. if (xs_is_null(xs_dict_get(fwreq, "published"))) {
  4943. /* add a date if it doesn't include one (Mastodon) */
  4944. xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
  4945. fwreq = xs_dict_set(fwreq, "published", date);
  4946. }
  4947. timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
  4948. follower_add(&snac, actor);
  4949. pending_del(&snac, actor);
  4950. snac_log(&snac, xs_fmt("new follower %s", actor));
  4951. }
  4952. }
  4953. else
  4954. if (strcmp(action, L("Discard")) == 0) { /** **/
  4955. pending_del(&snac, actor);
  4956. }
  4957. else
  4958. status = HTTP_STATUS_NOT_FOUND;
  4959. /* delete the cached timeline */
  4960. if (status == HTTP_STATUS_SEE_OTHER)
  4961. history_del(&snac, "timeline.html_");
  4962. }
  4963. else
  4964. if (p_path && strcmp(p_path, "admin/user-setup") == 0) { /** **/
  4965. /* change of user data */
  4966. const char *v;
  4967. const char *p1, *p2;
  4968. if ((v = xs_dict_get(p_vars, "name")) != NULL)
  4969. snac.config = xs_dict_set(snac.config, "name", v);
  4970. if ((v = xs_dict_get(p_vars, "avatar")) != NULL)
  4971. snac.config = xs_dict_set(snac.config, "avatar", v);
  4972. if ((v = xs_dict_get(p_vars, "bio")) != NULL)
  4973. snac.config = xs_dict_set(snac.config, "bio", v);
  4974. if ((v = xs_dict_get(p_vars, "cw")) != NULL &&
  4975. strcmp(v, "on") == 0) {
  4976. snac.config = xs_dict_set(snac.config, "cw", "open");
  4977. } else { /* if the checkbox is not set, the parameter is missing */
  4978. snac.config = xs_dict_set(snac.config, "cw", "");
  4979. }
  4980. if ((v = xs_dict_get(p_vars, "email")) != NULL)
  4981. snac.config = xs_dict_set(snac.config, "email", v);
  4982. if ((v = xs_dict_get(p_vars, "telegram_bot")) != NULL)
  4983. snac.config = xs_dict_set(snac.config, "telegram_bot", v);
  4984. if ((v = xs_dict_get(p_vars, "telegram_chat_id")) != NULL)
  4985. snac.config = xs_dict_set(snac.config, "telegram_chat_id", v);
  4986. if ((v = xs_dict_get(p_vars, "ntfy_server")) != NULL)
  4987. snac.config = xs_dict_set(snac.config, "ntfy_server", v);
  4988. if ((v = xs_dict_get(p_vars, "ntfy_token")) != NULL)
  4989. snac.config = xs_dict_set(snac.config, "ntfy_token", v);
  4990. if ((v = xs_dict_get(p_vars, "purge_days")) != NULL) {
  4991. xs *days = xs_number_new(atof(v));
  4992. snac.config = xs_dict_set(snac.config, "purge_days", days);
  4993. }
  4994. if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0)
  4995. snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_TRUE));
  4996. else
  4997. snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_FALSE));
  4998. if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0)
  4999. snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_TRUE));
  5000. else
  5001. snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_FALSE));
  5002. if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0)
  5003. snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_TRUE));
  5004. else
  5005. snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_FALSE));
  5006. if ((v = xs_dict_get(p_vars, "auto_boost")) != NULL && strcmp(v, "on") == 0)
  5007. snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_TRUE));
  5008. else
  5009. snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_FALSE));
  5010. if ((v = xs_dict_get(p_vars, "collapse_threads")) != NULL && strcmp(v, "on") == 0)
  5011. snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_TRUE));
  5012. else
  5013. snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_FALSE));
  5014. if ((v = xs_dict_get(p_vars, "approve_followers")) != NULL && strcmp(v, "on") == 0)
  5015. snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_TRUE));
  5016. else
  5017. snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_FALSE));
  5018. if ((v = xs_dict_get(p_vars, "show_contact_metrics")) != NULL && strcmp(v, "on") == 0)
  5019. snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE));
  5020. else
  5021. snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
  5022. if ((v = xs_dict_get(p_vars, "show_unlisted")) != NULL && strcmp(v, "on") == 0)
  5023. snac.config = xs_dict_set(snac.config, "show_unlisted", xs_stock(XSTYPE_TRUE));
  5024. else
  5025. snac.config = xs_dict_set(snac.config, "show_unlisted", xs_stock(XSTYPE_FALSE));
  5026. if ((v = xs_dict_get(p_vars, "web_ui_lang")) != NULL)
  5027. snac.config = xs_dict_set(snac.config, "lang", v);
  5028. if ((v = xs_dict_get(p_vars, "tz")) != NULL)
  5029. snac.config = xs_dict_set(snac.config, "tz", v);
  5030. if ((v = xs_dict_get(p_vars, "post_langs")) != NULL)
  5031. snac.config = xs_dict_set(snac.config, "post_langs", v);
  5032. if ((v = xs_dict_get(p_vars, "excluded_langs")) != NULL)
  5033. snac.config = xs_dict_set(snac.config, "excluded_langs", v);
  5034. else
  5035. snac.config = xs_dict_del(snac.config, "excluded_langs");
  5036. snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", ""));
  5037. snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", ""));
  5038. snac.config = xs_dict_set(snac.config, "notify_webhook", xs_dict_get_def(p_vars, "notify_webhook", ""));
  5039. if ((v = xs_dict_get(p_vars, "metadata")) != NULL)
  5040. snac.config = xs_dict_set(snac.config, "metadata", v);
  5041. /* uploads */
  5042. const char *uploads[] = { "avatar", "header", NULL };
  5043. int n;
  5044. for (n = 0; uploads[n]; n++) {
  5045. xs *var_name = xs_fmt("%s_file", uploads[n]);
  5046. const xs_list *uploaded_file = xs_dict_get(p_vars, var_name);
  5047. if (xs_type(uploaded_file) == XSTYPE_LIST) {
  5048. const char *fn = xs_list_get(uploaded_file, 0);
  5049. if (fn && *fn) {
  5050. const char *mimetype = xs_mime_by_ext(fn);
  5051. if (xs_startswith(mimetype, "image/")) {
  5052. const char *ext = strrchr(fn, '.');
  5053. xs *hash = xs_md5_hex(fn, strlen(fn));
  5054. xs *id = xs_fmt("%s-%s%s", uploads[n], hash, ext ? ext : "");
  5055. xs *url = xs_fmt("%s/s/%s", snac.actor, id);
  5056. int fo = xs_number_get(xs_list_get(uploaded_file, 1));
  5057. int fs = xs_number_get(xs_list_get(uploaded_file, 2));
  5058. /* store */
  5059. static_put(&snac, id, payload + fo, fs);
  5060. snac.config = xs_dict_set(snac.config, uploads[n], url);
  5061. }
  5062. }
  5063. }
  5064. }
  5065. /* delete images by removing url from user.json */
  5066. for (n = 0; uploads[n]; n++) {
  5067. xs *var_name = xs_fmt("%s_delete", uploads[n]);
  5068. const char *delete_var = xs_dict_get(p_vars, var_name);
  5069. if (delete_var != NULL && strcmp(delete_var, "on") == 0) {
  5070. snac.config = xs_dict_set(snac.config, uploads[n], "");
  5071. }
  5072. }
  5073. /* password change? */
  5074. if ((p1 = xs_dict_get(p_vars, "passwd1")) != NULL &&
  5075. (p2 = xs_dict_get(p_vars, "passwd2")) != NULL &&
  5076. *p1 && strcmp(p1, p2) == 0) {
  5077. xs *pw = hash_password(snac.uid, p1, NULL);
  5078. snac.config = xs_dict_set(snac.config, "passwd", pw);
  5079. }
  5080. user_persist(&snac, 1);
  5081. status = HTTP_STATUS_SEE_OTHER;
  5082. }
  5083. else
  5084. if (p_path && strcmp(p_path, "admin/filter-notifications") == 0) { /** **/
  5085. notify_filter(&snac, p_vars);
  5086. user_persist(&snac, 0);
  5087. status = HTTP_STATUS_SEE_OTHER;
  5088. }
  5089. else
  5090. if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
  5091. notify_clear(&snac);
  5092. timeline_touch(&snac);
  5093. status = HTTP_STATUS_SEE_OTHER;
  5094. }
  5095. else
  5096. if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
  5097. const char *irt = xs_dict_get(p_vars, "irt");
  5098. const char *opt = xs_dict_get(p_vars, "question");
  5099. const char *actor = xs_dict_get(p_vars, "actor");
  5100. xs *ls = NULL;
  5101. /* multiple choices? */
  5102. if (xs_type(opt) == XSTYPE_LIST)
  5103. ls = xs_dup(opt);
  5104. else
  5105. if (xs_type(opt) == XSTYPE_STRING) {
  5106. ls = xs_list_new();
  5107. ls = xs_list_append(ls, opt);
  5108. }
  5109. const xs_str *v;
  5110. int c = 0;
  5111. while (xs_list_next(ls, &v, &c)) {
  5112. xs *msg = msg_note(&snac, "", actor, irt, NULL, 1, NULL, NULL);
  5113. /* set the option */
  5114. msg = xs_dict_append(msg, "name", v);
  5115. /* delete the content */
  5116. msg = xs_dict_del(msg, "content");
  5117. xs *c_msg = msg_create(&snac, msg);
  5118. enqueue_message(&snac, c_msg);
  5119. timeline_add(&snac, xs_dict_get(msg, "id"), msg);
  5120. }
  5121. if (ls != NULL) {
  5122. /* get the poll object */
  5123. xs *poll = NULL;
  5124. if (valid_status(object_get(irt, &poll))) {
  5125. const char *date = xs_dict_get(poll, "endTime");
  5126. if (xs_is_null(date))
  5127. date = xs_dict_get(poll, "closed");
  5128. if (!xs_is_null(date)) {
  5129. time_t t = xs_parse_iso_date(date, 0) - time(NULL);
  5130. /* request the poll when it's closed;
  5131. Pleroma does not send and update when the poll closes */
  5132. enqueue_object_request(&snac, irt, t + 2);
  5133. }
  5134. }
  5135. }
  5136. status = HTTP_STATUS_SEE_OTHER;
  5137. }
  5138. else
  5139. if (p_path && strcmp(p_path, "admin/followed-hashtags") == 0) { /** **/
  5140. const char *followed_hashtags = xs_dict_get(p_vars, "followed_hashtags");
  5141. if (xs_is_null(followed_hashtags))
  5142. followed_hashtags = "";
  5143. if (xs_is_string(followed_hashtags)) {
  5144. xs *new_hashtags = xs_list_new();
  5145. xs *l = xs_split(followed_hashtags, "\n");
  5146. const char *v;
  5147. xs_list_foreach(l, v) {
  5148. xs *s1 = xs_strip_i(xs_dup(v));
  5149. s1 = xs_replace_i(s1, " ", "");
  5150. if (*s1 == '\0')
  5151. continue;
  5152. xs *s2 = NULL;
  5153. if (xs_startswith(s1, "https:/"))
  5154. s2 = xs_dup(s1);
  5155. else {
  5156. s2 = xs_utf8_to_lower(s1);
  5157. if (*s2 != '#')
  5158. s2 = xs_str_prepend_i(s2, "#");
  5159. }
  5160. new_hashtags = xs_list_append(new_hashtags, s2);
  5161. }
  5162. snac.config = xs_dict_set(snac.config, "followed_hashtags", new_hashtags);
  5163. user_persist(&snac, 0);
  5164. }
  5165. status = HTTP_STATUS_SEE_OTHER;
  5166. }
  5167. else
  5168. if (p_path && strcmp(p_path, "admin/blocked-hashtags") == 0) { /** **/
  5169. const char *hashtags = xs_dict_get(p_vars, "blocked_hashtags");
  5170. if (xs_is_null(hashtags))
  5171. hashtags = "";
  5172. if (xs_is_string(hashtags)) {
  5173. xs *new_hashtags = xs_list_new();
  5174. xs *l = xs_split(hashtags, "\n");
  5175. const char *v;
  5176. xs_list_foreach(l, v) {
  5177. xs *s1 = xs_strip_i(xs_dup(v));
  5178. s1 = xs_replace_i(s1, " ", "");
  5179. if (*s1 == '\0')
  5180. continue;
  5181. xs *s2 = xs_utf8_to_lower(s1);
  5182. if (*s2 != '#')
  5183. s2 = xs_str_prepend_i(s2, "#");
  5184. new_hashtags = xs_list_append(new_hashtags, s2);
  5185. }
  5186. snac.config = xs_dict_set(snac.config, "blocked_hashtags", new_hashtags);
  5187. user_persist(&snac, 0);
  5188. }
  5189. status = HTTP_STATUS_SEE_OTHER;
  5190. }
  5191. else
  5192. if (p_path && strcmp(p_path, "admin/muted-words") == 0) {
  5193. const char *words = xs_dict_get(p_vars, "muted_words");
  5194. if (xs_is_string(words)) {
  5195. xs *new_words = xs_list_new();
  5196. xs *l = xs_split(words, "\n");
  5197. const char *v;
  5198. xs_list_foreach(l, v) {
  5199. xs *s1 = xs_strip_i(xs_dup(v));
  5200. s1 = xs_replace_i(s1, " ", "");
  5201. if (*s1 == '\0')
  5202. continue;
  5203. xs *s2 = xs_utf8_to_lower(s1);
  5204. new_words = xs_list_insert_sorted(new_words, s2);
  5205. }
  5206. snac.config = xs_dict_set(snac.config, "muted_words", new_words);
  5207. user_persist(&snac, 0);
  5208. }
  5209. status = HTTP_STATUS_SEE_OTHER;
  5210. }
  5211. if (status == HTTP_STATUS_SEE_OTHER) {
  5212. const char *hard_redir = xs_dict_get(p_vars, "hard-redir");
  5213. if (xs_is_string(hard_redir))
  5214. *body = xs_dup(hard_redir);
  5215. else {
  5216. const char *redir = xs_dict_get(p_vars, "redir");
  5217. if (xs_is_null(redir))
  5218. redir = "top";
  5219. *body = xs_fmt("%s/admin#%s", snac.actor, redir);
  5220. }
  5221. *b_size = strlen(*body);
  5222. }
  5223. user_free(&snac);
  5224. return status;
  5225. }