| Comment: | add lots more shit |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
d4ecea913fb950b3a9a4686743283906 |
| User & Date: | lexi on 2020-12-31 00:15:53 |
| Other Links: | manifest | tags |
|
2020-12-31
| ||
| 02:18 | start work on user mgmt check-in: db4c5fd644 user: lexi tags: trunk | |
| 00:15 | add lots more shit check-in: d4ecea913f user: lexi tags: trunk | |
|
2020-12-30
| ||
| 02:44 | enable profile editing check-in: ac4a630ad5 user: lexi tags: trunk | |
Modified acl.t from [3939510d0a] to [7cc6c4467d].
1 1 -- vim: ft=terra 2 +local m = { 3 + agentkind = lib.enum { 4 + 'user', 'circle' 5 + }; 6 +} 7 + 8 +struct m.agent { 9 + kind: m.agentkind.t 10 + id: uint64 11 +} 12 + 13 +terra m.eval(expr: lib.str.t, agent: m.agent) 14 + 15 +end 16 + 17 +terra lib.store.post:save(ctupdate: bool) 18 +-- this post handles the messy details of registering a post's 19 +-- circles and actors, and increments the edit-count if ctupdate 20 +-- is true, which is should be in almost all cases. 21 + if ctupdate then 22 + self.chgcount = self.chgcount + 1 23 + self.edited = lib.osclock.time(nil) 24 + end 25 + -- TODO extract mentions from body, circles from acl 26 + self.source:post_save(self) 27 +end 28 + 29 +return m
Modified backend/pgsql.t from [d54604496b] to [af6b4187ca].
167 167 168 168 values ( 169 169 (select count(tweets.*)::bigint from tweets), 170 170 (select count(follows.*)::bigint from follows), 171 171 (select count(followers.*)::bigint from followers), 172 172 (select count(mutuals.*)::bigint from mutuals) 173 173 ) 174 - ]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation[r]) end) 174 + ]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation.idvmap[r]) end) 175 175 }; 176 176 177 177 actor_auth_how = { 178 178 params = {rawstring, lib.store.inet}, sql = [[ 179 179 with mts as (select a.kind from parsav_auth as a 180 180 left join parsav_actors as u on u.id = a.uid 181 181 where (a.uid is null or u.handle = $1::text or ( ................................................................................ 189 189 (select count(*) from mts where kind like 'otp-%') > 0, 190 190 (select count(*) from mts where kind like 'challenge-%') > 0, 191 191 (select count(*) from mts where kind = 'trust') > 0 192 192 ]]; -- cheat 193 193 }; 194 194 195 195 actor_session_fetch = { 196 - params = {uint64, lib.store.inet}, sql = [[ 196 + params = {uint64, lib.store.inet, int64}, sql = [[ 197 197 select a.id, a.nym, a.handle, a.origin, a.bio, 198 198 a.avataruri, a.rank, a.quota, a.key, a.epithet, 199 199 extract(epoch from a.knownsince)::bigint, 200 200 coalesce(a.handle || '@' || s.domain, 201 201 '@' || a.handle) as xid, 202 202 203 203 au.restrict, ................................................................................ 209 209 array['admin' ] <@ au.restrict as can_admin 210 210 211 211 from parsav_auth au 212 212 left join parsav_actors a on au.uid = a.id 213 213 left join parsav_servers s on a.origin = s.id 214 214 215 215 where au.aid = $1::bigint and au.blacklist = false and 216 - (au.netmask is null or au.netmask >> $2::inet) 216 + (au.netmask is null or au.netmask >> $2::inet) and 217 + ($3::bigint = 0 or --slightly abusing the epoch time fmt here, but 218 + ((a.authtime is null or a.authtime <= to_timestamp($3::bigint)) and 219 + (au.valperiod is null or au.valperiod <= to_timestamp($3::bigint)))) 217 220 ]]; 218 221 }; 219 222 220 223 actor_powers_fetch = { 221 224 params = {uint64}, sql = [[ 222 225 select key, allow from parsav_rights where actor = $1::bigint 223 226 ]] ................................................................................ 234 237 actor_power_delete = { 235 238 params = {uint64,lib.mem.ptr(int8)}, cmd = true, sql = [[ 236 239 delete from parsav_rights where 237 240 actor = $1::bigint and 238 241 key = $2::text 239 242 ]] 240 243 }; 244 + 245 + auth_sigtime_user_fetch = { 246 + params = {uint64}, sql = [[ 247 + select extract(epoch from authtime)::bigint 248 + from parsav_actors where id = $1::bigint 249 + ]]; 250 + }; 251 + 252 + auth_sigtime_user_alter = { 253 + params = {uint64,int64}, cmd = true, sql = [[ 254 + update parsav_actors set 255 + authtime = to_timestamp($2::bigint) 256 + where id = $1::bigint 257 + ]]; 258 + }; 241 259 242 260 auth_create_pw = { 243 261 params = {uint64, lib.mem.ptr(uint8)}, cmd = true, sql = [[ 244 262 insert into parsav_auth (uid, name, kind, cred) values ( 245 263 $1::bigint, 246 264 (select handle from parsav_actors where id = $1::bigint), 247 265 'pw-sha256', $2::bytea ................................................................................ 252 270 auth_purge_type = { 253 271 params = {rawstring, uint64, rawstring}, cmd = true, sql = [[ 254 272 delete from parsav_auth where 255 273 ((uid = 0 and name = $1::text) or uid = $2::bigint) and 256 274 kind like $3::text 257 275 ]] 258 276 }; 277 + 278 + post_save = { 279 + params = { 280 + uint64, uint32, int64; 281 + rawstring, rawstring, rawstring; 282 + }, cmd = true, sql = [[ 283 + update parsav_posts set 284 + subject = $4::text, 285 + acl = $5::text, 286 + body = $6::text, 287 + chgcount = $2::integer, 288 + edited = to_timestamp($3::bigint) 289 + where id = $1::bigint 290 + ]] 291 + }; 259 292 260 293 post_create = { 261 294 params = {uint64, rawstring, rawstring, rawstring}, sql = [[ 262 295 insert into parsav_posts ( 263 296 author, subject, acl, body, 264 297 posted, discovered, 265 298 circles, mentions ................................................................................ 266 299 ) values ( 267 300 $1::bigint, case when $2::text = '' then null else $2::text end, 268 301 $3::text, $4::text, 269 302 now(), now(), array[]::bigint[], array[]::bigint[] 270 303 ) returning id 271 304 ]]; -- TODO array handling 272 305 }; 306 + 307 + post_fetch = { 308 + params = {uint64}, sql = [[ 309 + select a.origin is null, 310 + p.id, p.author, p.subject, p.acl, p.body, 311 + extract(epoch from p.posted )::bigint, 312 + extract(epoch from p.discovered)::bigint, 313 + extract(epoch from p.edited )::bigint, 314 + p.parent, p.convoheaduri, p.chgcount 315 + from parsav_posts as p 316 + inner join parsav_actors as a on p.author = a.id 317 + where p.id = $1::bigint 318 + ]]; 319 + }; 273 320 274 321 post_enum_author_uid = { 275 322 params = {uint64,uint64,uint64,uint64, uint64}, sql = [[ 276 323 select a.origin is null, 277 324 p.id, p.author, p.subject, p.acl, p.body, 278 325 extract(epoch from p.posted )::bigint, 279 326 extract(epoch from p.discovered)::bigint, 280 - p.parent, p.convoheaduri 327 + extract(epoch from p.edited )::bigint, 328 + p.parent, p.convoheaduri, p.chgcount 281 329 from parsav_posts as p 282 330 inner join parsav_actors as a on p.author = a.id 283 331 where p.author = $5::bigint and 284 332 ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and 285 333 ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) 286 334 order by (p.posted, p.discovered) desc 287 335 limit case when $3::bigint = 0 then null ................................................................................ 294 342 295 343 timeline_instance_fetch = { 296 344 params = {uint64, uint64, uint64, uint64}, sql = [[ 297 345 select true, 298 346 p.id, p.author, p.subject, p.acl, p.body, 299 347 extract(epoch from p.posted )::bigint, 300 348 extract(epoch from p.discovered)::bigint, 301 - p.parent, null::text 349 + extract(epoch from p.edited )::bigint, 350 + p.parent, null::text, p.chgcount 302 351 from parsav_posts as p 303 352 inner join parsav_actors as a on p.author = a.id 304 353 where 305 354 ($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and 306 355 ($2::bigint = 0 or to_timestamp($2::bigint) < p.posted) and 307 356 (a.origin is null) 308 357 order by (p.posted, p.discovered) desc ................................................................................ 565 614 local terra row_to_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post) 566 615 var subj: rawstring, sblen: intptr 567 616 var cvhu: rawstring, cvhlen: intptr 568 617 if r:null(row,3) 569 618 then subj = nil sblen = 0 570 619 else subj = r:string(row,3) sblen = r:len(row,3)+1 571 620 end 572 - if r:null(row,9) 621 + if r:null(row,10) 573 622 then cvhu = nil cvhlen = 0 574 - else cvhu = r:string(row,9) cvhlen = r:len(row,9)+1 623 + else cvhu = r:string(row,10) cvhlen = r:len(row,10)+1 575 624 end 576 625 var p = [ lib.str.encapsulate(lib.store.post, { 577 626 subject = { `subj, `sblen }; 578 627 acl = {`r:string(row,4), `r:len(row,4)+1}; 579 628 body = {`r:string(row,5), `r:len(row,5)+1}; 580 629 convoheaduri = { `cvhu, `cvhlen }; --FIXME 581 630 }) ] 582 631 p.ptr.id = r:int(uint64,row,1) 583 632 p.ptr.author = r:int(uint64,row,2) 584 633 p.ptr.posted = r:int(uint64,row,6) 585 634 p.ptr.discovered = r:int(uint64,row,7) 586 - if r:null(row,8) 635 + p.ptr.edited = r:int(uint64,row,8) 636 + if r:null(row,9) 587 637 then p.ptr.parent = 0 588 - else p.ptr.parent = r:int(uint64,row,8) 638 + else p.ptr.parent = r:int(uint64,row,9) 639 + end 640 + if r:null(row,11) 641 + then p.ptr.chgcount = 0 642 + else p.ptr.chgcount = r:int(uint32,row,11) 589 643 end 590 644 p.ptr.localpost = r:bool(row,0) 591 645 592 646 return p 593 647 end 594 648 local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor) 595 649 var a: lib.mem.ptr(lib.store.actor) ................................................................................ 918 972 s.mutuals = r:int(uint64, 0, 3) 919 973 return s 920 974 end]; 921 975 922 976 actor_session_fetch = [terra( 923 977 src: &lib.store.source, 924 978 aid: uint64, 925 - ip : lib.store.inet 979 + ip : lib.store.inet, 980 + issuetime: lib.store.timepoint 926 981 ): { lib.stat(lib.store.auth), lib.mem.ptr(lib.store.actor) } 927 - var r = queries.actor_session_fetch.exec(src, aid, ip) 982 + var r = queries.actor_session_fetch.exec(src, aid, ip, issuetime) 928 983 if r.sz == 0 then goto fail end 929 984 do defer r:free() 930 985 931 986 if r:null(0,0) then goto fail end 932 987 933 988 var a = row_to_actor(&r, 0) 934 989 a.ptr.source = src ................................................................................ 959 1014 ): uint64 960 1015 var r = queries.post_create.exec(src,post.author,post.subject,post.acl,post.body) 961 1016 if r.sz == 0 then return 0 end 962 1017 defer r:free() 963 1018 var id = r:int(uint64,0,0) 964 1019 return id 965 1020 end]; 1021 + 1022 + post_fetch = [terra( 1023 + src: &lib.store.source, 1024 + post: uint64 1025 + ): lib.mem.ptr(lib.store.post) 1026 + var r = queries.post_fetch.exec(src, post) 1027 + if r.sz == 0 then return [lib.mem.ptr(lib.store.post)].null() end 1028 + var p = row_to_post(&r, 0) 1029 + p.ptr.source = src 1030 + return p 1031 + end]; 966 1032 967 1033 timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range) 968 1034 var r = pqr { sz = 0 } 969 1035 var A,B,C,D = rg:matrix() -- :/ 970 1036 r = queries.timeline_instance_fetch.exec(src,A,B,C,D) 971 1037 972 1038 var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz) ................................................................................ 1106 1172 detach: bool 1107 1173 ): {} 1108 1174 if detach 1109 1175 then queries.post_attach_ctl_del.exec(src,post,artifact) 1110 1176 else queries.post_attach_ctl_ins.exec(src,post,artifact) 1111 1177 end 1112 1178 end]; 1179 + 1180 + post_save = [terra( 1181 + src: &lib.store.source, 1182 + post: &lib.store.post 1183 + ): {} 1184 + queries.post_save.exec(src, 1185 + post.id, post.chgcount, post.edited, 1186 + post.subject, post.acl, post.body) 1187 + end]; 1188 + 1189 + auth_sigtime_user_fetch = [terra( 1190 + src: &lib.store.source, 1191 + uid: uint64 1192 + ): lib.store.timepoint 1193 + var r = queries.auth_sigtime_user_fetch.exec(src, uid) 1194 + if r.sz > 0 then defer r:free() 1195 + var t = r:int(int64,0,0) 1196 + return t 1197 + else return 0 end 1198 + end]; 1199 + 1200 + auth_sigtime_user_alter = [terra( 1201 + src: &lib.store.source, 1202 + uid: uint64, 1203 + time: lib.store.timepoint 1204 + ): {} queries.auth_sigtime_user_alter.exec(src, uid, time) end]; 1113 1205 1114 1206 actor_auth_register_uid = nil; -- TODO better support non-view based auth 1115 1207 } 1116 1208 1117 1209 return b
Modified backend/schema/pgsql-auth.sql from [1170b3857b] to [8bbbf24f23].
42 42 -- if the credential matches, access will be denied, even if 43 43 -- non-blacklisted credentials match. most useful with 44 44 -- uid = null, kind = trust, cidr = (untrusted IP range) 45 45 46 46 valperiod timestamp default now(), 47 47 -- cookies bearing timestamps earlier than this point in time 48 48 -- will be considered invalid and will not grant access 49 + 50 + comment text, 51 + -- a field the user can use to identify the specific credential, 52 + -- in order to aid credential management 49 53 50 54 unique(name,kind,cred) 51 55 );
Modified backend/schema/pgsql.sql from [cb277a93b1] to [135f2b367a].
24 24 25 25 create table parsav_actors ( 26 26 id bigint primary key default (1+random()*(2^63-1))::bigint, 27 27 nym text, 28 28 handle text not null, -- nym [@handle@origin] 29 29 origin bigint references parsav_servers(id) 30 30 on delete cascade, -- null origin = local actor 31 - knownsince timestamp, 31 + knownsince timestamp not null default now(), 32 32 bio text, 33 33 avatarid bigint, -- artifact id, null if remote 34 34 avataruri text, -- null if local 35 35 rank smallint not null default 0, 36 36 quota integer not null default 1000, 37 37 key bytea, -- private if localactor; public if remote 38 38 epithet text, ................................................................................ 56 56 author bigint references parsav_actors(id) 57 57 on delete cascade, 58 58 subject text, 59 59 acl text not null default 'all', -- just store the script raw 🤷 60 60 body text, 61 61 posted timestamp not null, 62 62 discovered timestamp not null, 63 + chgcount integer not null default 0, 64 + edited timestamp, 63 65 parent bigint not null default 0, -- if post: part of conversation; if chatroom: top-level post 64 66 circles bigint[], -- TODO at edit or creation, iterate through each circle 65 67 mentions bigint[], -- a user has, check if it can see her post, and if so add 66 68 artifacts bigint[], 67 69 68 70 convoheaduri text 69 71 -- only used for tracking foreign conversations and tying them to post heads;
Modified cmdparse.t from [49c267b075] to [dec7841af5].
21 21 for o,desc in pairs(tbl) do 22 22 local consume = desc.consume or 0 23 23 local incr = desc.inc or 0 24 24 options.entries[#options.entries + 1] = { 25 25 field = o, type = (consume > 0) and &rawstring or 26 26 (incr > 0) and uint or bool 27 27 } 28 - helpstr = helpstr .. string.format(' -%s --%s: %s\n', 29 - desc[1], sanitize(o), desc[2]) 28 + if desc[1] then 29 + helpstr = helpstr .. string.format(' -%s --%s: %s\n', 30 + desc[1], sanitize(o), desc[2]) 31 + else 32 + helpstr = helpstr .. string.format(' --%s: %s\n', 33 + sanitize(o), desc[2]) 34 + end 30 35 end 31 36 for o,desc in pairs(tbl) do 32 37 local flag = desc[1] 33 38 local consume = desc.consume or 0 34 39 local incr = desc.inc or 0 35 40 init[#init + 1] = quote [self].[o] = [ 36 41 (consume > 0 and `nil) or ................................................................................ 48 53 end 49 54 end 50 55 elseif incr > 0 then 51 56 ch = quote [self].[o] = [self].[o] + incr end 52 57 else ch = quote 53 58 [self].[o] = true 54 59 end end 55 - shortcases[#shortcases + 1] = quote 56 - case [int8]([string.byte(flag)]) then [ch] end 60 + if flag ~= nil then 61 + shortcases[#shortcases + 1] = quote 62 + case [int8]([string.byte(flag)]) then [ch] end 63 + end 57 64 end 58 65 longcases[#longcases + 1] = quote 59 66 if lib.str.cmp([arg]+2, [sanitize(o)]) == 0 then [ch] goto [skip] end 60 67 end 61 68 end 62 69 terra options:free() self.arglist:free() end 63 70 options.methods.parse = terra([self], [argc], [argv])
Modified config.lua from [931922a3e1] to [3d0e5432a4].
44 44 when = os.date(); 45 45 }; 46 46 feat = {}; 47 47 debug = u.tobool(default('parsav_enable_debug',true)); 48 48 backends = defaultlist('parsav_backends', 'pgsql'); 49 49 braingeniousmode = false; 50 50 embeds = { 51 + -- TODO with gzip compression, svg is dramatically superior to webp 52 + -- we should have a build-time option to serve svg so instances 53 + -- proxied behind nginx can serve svgz, or possibly just straight-up 54 + -- add support for content-encoding headers and pre-compress the 55 + -- damn things before compiling 51 56 {'style.css', 'text/css'}; 52 57 {'default-avatar.webp', 'image/webp'}; 53 58 {'padlock.webp', 'image/webp'}; 54 59 {'warn.webp', 'image/webp'}; 60 + {'query.webp', 'image/webp'}; 55 61 }; 56 62 } 57 63 if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then 58 64 conf.braingeniousmode = true -- SOUND GENERAL QUARTERS 59 65 end 60 66 if u.ping '.fslckout' or u.ping '_FOSSIL_' then 61 67 if u.ping '_FOSSIL_' then default_os = 'windows' end
Modified math.t from [573a13128c] to [bc01716315].
1 1 -- vim: ft=terra 2 2 local m = { 3 3 shorthand = {maxlen = 14} 4 4 } 5 + 6 +local pstring = lib.mem.ptr(int8) 5 7 6 8 -- swap in place -- faster on little endian 7 9 m.netswap_ip = macro(function(ty, src, dest) 8 10 if ty:astype().type ~= 'integer' then error('bad type') end 9 11 local bytes = ty:astype().bytes 10 12 src = `[&uint8](src) 11 13 dest = `[&uint8](dest) ................................................................................ 182 184 else dgtct = dgtct + 1 end 183 185 end else 184 186 buf = buf - 1 185 187 @buf = 0x30 186 188 end 187 189 return buf 188 190 end 191 + 192 +terra m.ndigits(n: intptr, base: intptr): intptr 193 + var c = base 194 + var i = 1 195 + while true do 196 + if n < c then return i end 197 + c = c * base 198 + i = i + 1 199 + end 200 +end 201 + 202 +terra m.fsz_parse(f: pstring): {intptr, bool} 203 +-- take a string representing a file size and return {nbytes, true} 204 +-- or {0, false} if the parse fails 205 + if f.ct == 0 then f.ct = lib.str.sz(f.ptr) end 206 + var sz: intptr = 0 207 + for i = 0, f.ct do 208 + if f(i) == @',' then goto skip end 209 + if f(i) >= 0x30 and f(i) <= 0x39 then 210 + sz = sz * 10 211 + sz = sz + f(i) - 0x30 212 + else 213 + if i+1 == f.ct or f(i) == 0 then return sz, true end 214 + if i+2 == f.ct or f(i+1) == 0 then 215 + if f(i) == @'b' then return sz/8, true end -- bits 216 + else 217 + var s: intptr = 0 218 + if i+3 == f.ct or f(i+2) == 0 then 219 + s = i + 1 220 + elseif (i+4 == f.ct or f(i+3) == 0) and f(i+1) == @'i' then 221 + -- grudgingly tolerate ~mebibits~ and its ilk, without 222 + -- affecting the result in any way 223 + s = i + 2 224 + else return 0, false end 225 + 226 + if f(s) == @'b' then sz = sz/8 -- bits 227 + elseif f(s) ~= @'B' then return 0, false end -- wth 228 + end 229 + var c = f(i) 230 + if c >= @'A' and c <= @'Z' then c = c - 0x20 end 231 + switch c do -- normal char literal syntax doesn't work here, leads to llvm error (!!) 232 + case [uint8]([string.byte('k')]) then return sz * [1024ULL ^ 1], true end 233 + case [uint8]([string.byte('m')]) then return sz * [1024ULL ^ 2], true end 234 + case [uint8]([string.byte('g')]) then return sz * [1024ULL ^ 3], true end 235 + case [uint8]([string.byte('t')]) then return sz * [1024ULL ^ 4], true end 236 + case [uint8]([string.byte('e')]) then return sz * [1024ULL ^ 5], true end 237 + case [uint8]([string.byte('y')]) then return sz * [1024ULL ^ 6], true end 238 + else return sz, true 239 + end 240 + end 241 + ::skip::end 242 + return sz, true 243 +end 189 244 190 245 return m
Modified mgtool.t from [03b0c72484] to [faf7450a82].
297 297 298 298 if dbmode.arglist.ct == 1 then 299 299 lib.bail('you are attempting to completely obliterate all data! make sure you have selected your target correctly. if you really want to do this, pass the confirmation string ', &cfmstr[0]) 300 300 elseif dbmode.arglist.ct == 2 then 301 301 if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then 302 302 lib.warn('completely obliterating all data!') 303 303 dlg:obliterate_everything() 304 + elseif lib.str.cmp(dbmode.arglist(1), 'print-confirmation-string') == 0 then 305 + lib.io.send(1, cfmstr, lib.str.sz(cfmstr)) 304 306 else 305 307 lib.bail('you passed an incorrect confirmation string; pass ', &cfmstr[0], ' if you really want to destroy everything') 306 308 end 307 309 else goto cmderr end 308 310 else goto cmderr end 309 311 elseif lib.str.cmp(mode.arglist(0),'be') == 0 then 310 312 srv:setup(cnf) ................................................................................ 329 331 if cfmode.arglist.ct < 1 then goto cmderr end 330 332 331 333 if cfmode.arglist.ct == 1 then 332 334 if lib.str.cmp(cfmode.arglist(0),'chsec') == 0 then 333 335 var sec: int8[65] gensec(&sec[0]) 334 336 dlg:conf_set('server-secret', &sec[0]) 335 337 lib.report('server secret reset') 336 - -- FIXME notify server to reload its config 337 338 elseif lib.str.cmp(cfmode.arglist(0),'refresh') == 0 then 338 - -- TODO notify server to reload config 339 + cfmode.no_notify = false -- duh 339 340 else goto cmderr end 340 341 elseif cfmode.arglist.ct == 3 and 341 342 lib.str.cmp(cfmode.arglist(0),'set') == 0 then 342 343 dlg:conf_set(cfmode.arglist(1),cfmode.arglist(2)) 343 344 lib.report('parameter set') 344 345 else goto cmderr end 346 + 347 + -- successful commands fall through 348 + if not cfmode.no_notify then 349 + dlg:ipc_send(lib.ipc.cmd.cfgrefresh,0) 350 + end 345 351 else 346 352 srv:setup(cnf) 347 353 srv:conprep(lib.store.prepmode.full) 348 354 if lib.str.cmp(mode.arglist(0),'mkroot') == 0 then 349 355 var cfmode: pbasic cfmode:parse(mode.arglist.ct, &mode.arglist(0)) 350 356 if cfmode.help then 351 357 [ lib.emit(false, 1, 'usage: ', `argv[0], ' mkroot ', cfmode.type.helptxt.flags, ' <handle>', cfmode.type.helptxt.opts) ] ................................................................................ 364 370 var root = lib.store.actor.mk(&kbuf[0]) 365 371 root.handle = cfmode.arglist(0) 366 372 var epithets = array( 367 373 'root', 'god', 'regional jehovah', 'titan king', 368 374 'king of olympus', 'cyberpharaoh', 'electric ellimist', 369 375 "rampaging c'tan", 'deathless tweetlord', 'postmaster', 370 376 'faerie queene', 'lord of the posts', 'ruthless cybercrat', 371 - 'general secretary', 'commissar', 'kwisatz haderach' 377 + 'general secretary', 'commissar', 'kwisatz haderach', 378 + 'dedicated hyperturing' 372 379 -- feel free to add more 373 380 ) 374 381 root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])] 375 382 root.rights.powers:fill() -- grant omnipotence 376 383 root.rights.rank = 1 377 384 var ruid = dlg:actor_create(&root) 378 385 dlg:conf_set('master',root.handle)
Modified parsav.t from [dc97a3f269] to [022b1bf037].
125 125 var [val] 126 126 [exp] 127 127 in val end 128 128 return q 129 129 end); 130 130 proc = { 131 131 fork = terralib.externfunction('fork', {} -> int); 132 - daemonize = terralib.externfunction('daemon', {int,int} -> {}); 133 132 exit = terralib.externfunction('exit', int -> {}); 134 133 getenv = terralib.externfunction('getenv', rawstring -> rawstring); 135 134 exec = terralib.externfunction('execv', {rawstring,&rawstring} -> int); 136 135 execp = terralib.externfunction('execvp', {rawstring,&rawstring} -> int); 137 136 }; 138 137 io = { 139 138 send = terralib.externfunction('write', {int, rawstring, intptr} -> ptrdiff); 140 139 recv = terralib.externfunction('read', {int, rawstring, intptr} -> ptrdiff); 141 140 close = terralib.externfunction('close', {int} -> int); 142 141 say = macro(function(msg) return `lib.io.send(2, msg, [#(msg:asvalue())]) end); 143 142 fmt = terralib.externfunction('printf', 144 143 terralib.types.funcpointer({rawstring},{int},true)); 144 + ttyp = terralib.externfunction('isatty', int -> int); 145 145 }; 146 146 str = { sz = terralib.externfunction('strlen', rawstring -> intptr) }; 147 147 copy = function(tbl) 148 148 local new = {} 149 149 for k,v in pairs(tbl) do new[k] = v end 150 150 setmetatable(new, getmetatable(tbl)) 151 151 return new ................................................................................ 273 273 then lib.io.say([' - ' .. v .. ': true\n']) 274 274 else lib.io.say([' - ' .. v .. ': false\n']) 275 275 end 276 276 end 277 277 end 278 278 return q 279 279 end) 280 + terra set:setbit(i: intptr, val: bool) 281 + if val then 282 + self._store[i/8] = self._store[i/8] or (1 << (i % 8)) 283 + else 284 + self._store[i/8] = self._store[i/8] and not (1 << (i % 8)) 285 + end 286 + end 287 + set.bits = {} 288 + set.idvmap = {} 289 + for i,v in ipairs(tbl) do 290 + set.idvmap[v] = i 291 + set.bits[v] = quote var b: set b:clear() b:setbit(i, true) in b end 292 + end 280 293 set.metamethods.__add = macro(function(self,other) 281 294 local new = symbol(set) 282 295 local q = quote var [new] new:clear() end 283 296 for i = 0, bytes - 1 do 284 297 q = quote [q] 285 298 new._store[i] = self._store[i] or other._store[i] 286 299 end ................................................................................ 292 305 local q = quote var [new] new:clear() end 293 306 for i = 0, bytes - 1 do 294 307 q = quote [q] 295 308 new._store[i] = self._store[i] and other._store[i] 296 309 end 297 310 end 298 311 return quote [q] in new end 312 + end) 313 + set.metamethods.__eq = macro(function(self,other) 314 + local rt = symbol(bool) 315 + local fb if #tbl % 8 == 0 then fb = bytes - 1 else fb = bytes - 2 end 316 + local q = quote rt = true end 317 + for i = 0, fb do 318 + q = quote 319 + if self._store[i] ~= other._store[i] then rt = false else [q] end 320 + end 321 + end 322 + -- we need to mask out any extraneous bits the values might have, as we 323 + -- don't want the kind of noise introduced by :fill() to affect comparison 324 + if #tbl % 8 ~= 0 then 325 + local last = #tbl-1 326 + local msk = (2 ^ (#tbl % 8)) - 1 327 + q = quote 328 + if (self._store [last] and [uint8](msk)) ~= 329 + (other._store[last] and [uint8](msk)) then rt = false else [q] end 330 + end 331 + end 332 + return quote var [rt]; [q] in rt end 299 333 end) 300 334 set.metamethods.__not = macro(function(self) 301 335 local new = symbol(set) 302 336 local q = quote var [new] new:clear() end 303 337 for i = 0, bytes - 1 do 304 338 q = quote [q] 305 339 new._store[i] = not self._store[i] ................................................................................ 345 379 lib.md = lib.loadlib('mbedtls','mbedtls/md.h') 346 380 lib.b64 = lib.loadlib('mbedtls','mbedtls/base64.h') 347 381 lib.net = lib.loadlib('mongoose','mongoose.h') 348 382 lib.pq = lib.loadlib('libpq','libpq-fe.h') 349 383 350 384 lib.load { 351 385 'mem', 'math', 'str', 'file', 'crypt', 'ipc'; 352 - 'http', 'html', 'session', 'tpl', 'store'; 386 + 'http', 'html', 'session', 'tpl', 'store', 'acl'; 353 387 354 388 'smackdown'; -- md-alike parser 355 389 } 356 390 357 391 local be = {} 358 392 for _, b in pairs(config.backends) do 359 393 be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')() ................................................................................ 387 421 388 422 lib.load { 389 423 'srv'; 390 424 'render:nav'; 391 425 'render:nym'; 392 426 'render:login'; 393 427 'render:profile'; 394 - 395 428 'render:compose'; 396 429 'render:tweet'; 397 - 'render:userpage'; 430 + 'render:tweet-page'; 431 + 'render:user-page'; 398 432 'render:timeline'; 399 433 400 434 'render:docpage'; 401 435 402 436 'render:conf:profile'; 437 + 'render:conf:sec'; 403 438 'render:conf'; 404 439 'route'; 405 440 } 406 441 407 442 do 408 443 local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when) 409 444 terra version() lib.io.send(1, p, [#p]) end
Modified render/compose.t from [cb3a66bab9] to [13509724e6].
1 1 -- vim: ft=terra 2 2 local terra 3 -render_compose(co: &lib.srv.convo, edit: &lib.store.post) 3 +render_compose(co: &lib.srv.convo, edit: &lib.store.post, acc: &lib.str.acc) 4 4 var target, tgtlen = co:getv('to') 5 5 var form: data.view.compose 6 + form = data.view.compose { 7 + handle = co.who.handle; 8 + circles = ''; -- TODO: list user's circles, rooms, and saved aclexps 9 + } 6 10 if edit == nil then 7 - form = data.view.compose { 8 - content = lib.coalesce(target, ''); 9 - acl = lib.trn(target == nil, 'all', 'mentioned'); -- TODO default acl setting? 10 - handle = co.who.handle; 11 - circles = ''; -- TODO: list user's circles, rooms, and saved aclexps 12 - } 11 + form.content = lib.coalesce(target, '') 12 + form.acl = lib.trn(target == nil, 'all', 'mentioned') -- TODO default acl setting? 13 + else 14 + form.content = lib.coalesce(edit.body, '') 15 + form.acl = edit.acl 13 16 end 17 + if acc ~= nil then form:append(acc) return end 18 + 14 19 var cotxt = form:tostr() defer cotxt:free() 15 20 16 - var doc = data.view.docskel { 17 - instance = co.srv.cfg.instance; 21 + var doc = [lib.srv.convo.page] { 18 22 title = lib.str.plit 'compose'; 19 23 body = cotxt; 20 24 class = lib.str.plit 'compose'; 21 - navlinks = co.navbar; 25 + cache = true; 22 26 } 23 27 24 - var hdrs = array( 25 - lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' } 26 - ) 27 - doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]}) 28 + co:stdpage(doc) 28 29 end 29 30 30 31 return render_compose
Modified render/conf/profile.t from [248ab207d4] to [7f970c2f4a].
4 4 5 5 local terra cs(s: rawstring) 6 6 return pstr { ptr = s, ct = lib.str.sz(s) } 7 7 end 8 8 9 9 local terra 10 10 render_conf_profile(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr 11 - 12 11 var c = data.view.conf_profile { 13 12 handle = cs(co.who.handle); 14 13 nym = cs(lib.coalesce(co.who.nym,'')); 15 14 bio = cs(lib.coalesce(co.who.bio,'')); 16 15 } 17 16 return c:tostr() 18 17 end 19 18 20 19 return render_conf_profile
Added render/conf/sec.t version [2ed8642241].
1 +-- vim: ft=terra 2 +local pstr = lib.mem.ptr(int8) 3 +local pref = lib.mem.ref(int8) 4 +local terra 5 +render_conf_sec(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr 6 + var time: lib.store.timepoint = co.who.source:auth_sigtime_user_fetch(co.who.id) 7 + var tstr: int8[26] 8 + lib.osclock.ctime_r(&time, &tstr[0]) 9 + var body = data.view.conf_sec { 10 + lastreset = pstr { 11 + ptr = &tstr[0], ct = lib.str.sz(&tstr[0]) 12 + } 13 + } 14 + 15 + if co.srv.cfg.credmgd then 16 + var a: lib.str.acc a:init(768) 17 + body:append(&a) 18 + var credmgr = data.view.conf_sec_credmg { 19 + credlist = '<option>your password</option>' 20 + } 21 + credmgr:append(&a) 22 + return a:finalize() 23 + else return body:tostr() end 24 +end 25 +return render_conf_sec
Modified render/docpage.t from [3eda6110a6] to [148acf7303].
67 67 68 68 local terra 69 69 pushbranches(list: &lib.str.acc, idx: intptr, ps: lib.store.powerset): {} 70 70 var [pages] = array([allpages]) 71 71 var started = false 72 72 for i=0,[pages.type.N] do 73 73 if pages[i].parent == idx+1 and (pages[i].priv:sz() == 0 or 74 - (ps and pages[i].priv):sz() > 0) then 74 + (ps and pages[i].priv) == pages[i].priv) then 75 75 if not started then 76 76 started = true 77 77 list:lpush('<ul>') 78 78 end 79 79 list:lpush('<li><a href="/doc/'):rpush(pages[i].name):lpush('">') 80 80 :rpush(pages[i].title):lpush('</a>') 81 81 pushbranches(list, i, ps)
Modified render/profile.t from [19457b4b7c] to [ae13f6f2b7].
3 3 local terra cs(s: rawstring) 4 4 return pstr { ptr = s, ct = lib.str.sz(s) } 5 5 end 6 6 7 7 local terra 8 8 render_profile(co: &lib.srv.convo, actor: &lib.store.actor) 9 9 var aux: lib.str.acc 10 + var followed = true -- FIXME 10 11 if co.aid ~= 0 and co.who.id == actor.id then 11 - aux:compose('<a href="/conf/profile?go=/',actor.xid,'">alter</a>') 12 + aux:compose('<a class="button" href="/conf/profile?go=/',actor.xid,'">alter</a>') 12 13 elseif co.aid ~= 0 then 13 - aux:compose('<a href="/', actor.xid, '/follow">follow</a><a href="/', 14 - actor.xid, '/chat">chat</a>') 14 + if not followed then 15 + aux:compose('<button method="post" name="act" value="follow">follow</a>') 16 + elseif not followed then 17 + aux:compose('<button method="post" name="act" value="unfollow">unfollow</a>') 18 + end 19 + aux:lpush('<a href="/'):push(actor.xid,0):lpush('/chat">chat</a>') 15 20 if co.who.rights.powers:affect_users() then 16 - aux:lpush('<a href="/'):push(actor.xid,0):lpush('/ctl">control</a>') 21 + aux:lpush('<a class="button" href="/'):push(actor.xid,0):lpush('/ctl">control</a>') 17 22 end 18 23 else 19 - aux:compose('<a href="/', actor.xid, '/follow">remote follow</a>') 24 + aux:compose('<a class="button" href="/', actor.xid, '/follow">remote follow</a>') 20 25 end 21 26 var auxp = aux:finalize() 22 27 var avistr: lib.str.acc if actor.origin == 0 then 23 28 avistr:compose('/avi/',actor.handle) 24 29 end 25 30 var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, ×tr[0]) 26 31
Added render/tweet-page.t version [ad592d16bd].
1 +-- vim: ft=terra 2 +local pstr = lib.mem.ptr(int8) 3 +local pref = lib.mem.ref(int8) 4 +local terra cs(s: rawstring) 5 + return pstr { ptr = s, ct = lib.str.sz(s) } 6 +end 7 + 8 +local terra 9 +render_tweet_page( 10 + co: &lib.srv.convo, 11 + path: lib.mem.ptr(pref), 12 + p: &lib.store.post 13 +): {} 14 + var pg: lib.str.acc pg:init(256) 15 + lib.render.tweet(co, p, &pg) 16 + pg:lpush('<form class="action-bar" method="post">') 17 + 18 + if co.aid ~= 0 then 19 + var liked = false -- FIXME 20 + var rtd = false 21 + if not liked 22 + then pg:lpush('<button class="pos" name="act" value="like">like</button>') 23 + else pg:lpush('<button class="neg" name="act" value="dislike">dislike</button>') 24 + end 25 + if not rtd 26 + then pg:lpush('<button class="pos" name="act" value="rt">retweet</button>') 27 + else pg:lpush('<button class="neg" name="act" value="unrt">detweet</button>') 28 + end 29 + if p.author == co.who.id then 30 + pg:lpush('<a class="button" href="/post/'):rpush(path(1)):lpush('/edit">edit</a><a class="neg button" href="/post/'):rpush(path(1)):lpush('/del">delete</a>') 31 + end 32 + -- TODO list user's chosen reaction emoji 33 + pg:lpush('</form>') 34 + 35 + if co.who.rights.powers.post() then 36 + lib.render.compose(co, nil, &pg) 37 + end 38 + end 39 + 40 + var ppg = pg:finalize() defer ppg:free() 41 + co:stdpage([lib.srv.convo.page] { 42 + title = lib.str.plit 'post'; cache = false; 43 + class = lib.str.plit 'post'; body = ppg; 44 + }) 45 + 46 + -- TODO display conversation 47 + -- perhaps display descendant nodes here, and have a link to the top of the whole tree? 48 +end 49 + 50 +return render_tweet_page
Name change from render/userpage.t to render/user-page.t.
Modified route.t from [ae96c17fe6] to [2469fad253].
10 10 var handle = [lib.mem.ptr(int8)] { ptr = &uri.ptr[2], ct = 0 } 11 11 for i=2,uri.ct do 12 12 if uri.ptr[i] == @'/' or uri.ptr[i] == 0 then handle.ct = i - 2 break end 13 13 end 14 14 if handle.ct == 0 then 15 15 handle.ct = uri.ct - 2 16 16 uri:advance(uri.ct) 17 - else 18 - if handle.ct + 2 < uri.ct then 19 - uri:advance(handle.ct + 2) 20 - --uri.ptr = uri.ptr + (handle.ct + 2) 21 - --uri.ct = uri.ct - (handle.ct + 2) 22 - end 23 - end 17 + elseif handle.ct + 2 < uri.ct then uri:advance(handle.ct + 2) end 24 18 25 19 lib.dbg('looking up user by xid "', {handle.ptr,handle.ct} ,'", path: ', {uri.ptr,uri.ct}) 26 20 27 21 var path = lib.http.hier(uri) defer path:free() 28 22 for i=0,path.ct do 29 23 lib.dbg('got path component ', {path.ptr[i].ptr, path.ptr[i].ct}) 30 24 end ................................................................................ 32 26 var actor = co.srv:actor_fetch_xid(handle) 33 27 if actor.ptr == nil then 34 28 co:complain(404,'no such user','no such user known to this server') 35 29 return 36 30 end 37 31 defer actor:free() 38 32 39 - lib.render.userpage(co, actor.ptr) 33 + lib.render.user_page(co, actor.ptr) 40 34 end 41 35 42 -terra http.actor_profile_uid(co: &lib.srv.convo, path: lib.mem.ptr(lib.mem.ref(int8)), meth: method.t) 36 +terra http.actor_profile_uid ( 37 + co: &lib.srv.convo, 38 + path: lib.mem.ptr(lib.mem.ref(int8)), 39 + meth: method.t 40 +) 43 41 if path.ct < 2 then 44 42 co:complain(404,'bad url','invalid user url') 45 43 return 46 44 end 47 45 48 46 var uid, ok = lib.math.shorthand.parse(path.ptr[1].ptr, path.ptr[1].ct) 49 47 if not ok then ................................................................................ 54 52 var actor = co.srv:actor_fetch_uid(uid) 55 53 if actor.ptr == nil then 56 54 co:complain(404, 'no such user', 'no user by that ID is known to this instance') 57 55 return 58 56 end 59 57 defer actor:free() 60 58 61 - lib.render.userpage(co, actor.ptr) 59 + lib.render.user_page(co, actor.ptr) 62 60 end 63 61 64 62 terra http.login_form(co: &lib.srv.convo, meth: method.t) 65 63 if meth == method.get then 66 64 -- request a username 67 65 lib.render.login(co, nil, nil, lib.str.plit(nil)) 68 66 elseif meth == method.post then ................................................................................ 103 101 if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then 104 102 aid = co.srv:actor_auth_pw(co.peer, 105 103 [lib.mem.ptr(int8)]{ptr=usn,ct=usnl}, 106 104 [lib.mem.ptr(int8)]{ptr=chrs,ct=chrsl}) 107 105 elseif lib.str.ncmp('otp', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then 108 106 lib.dbg('using otp auth') 109 107 -- ··· -- 110 - else 111 - lib.dbg('invalid auth method') 112 - end 108 + else lib.dbg('invalid auth method') end 113 109 114 110 -- error out 115 111 if aid == 0 then 116 112 lib.render.login(co, nil, nil, lib.str.plit 'authentication failure') 117 113 else 118 - var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1] 119 - do var p = &sesskey[0] 120 - p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1]) 121 - p = p + lib.session.cookie_gen(co.srv.cfg.secret, aid, lib.osclock.time(nil), p) 122 - lib.dbg('sending cookie ',{&sesskey[0],15}) 123 - p = lib.str.ncpy(p, '; Path=/', 9) 124 - end 125 - co:reroute_cookie('/', &sesskey[0]) 114 + co:installkey('/',aid) 126 115 end 127 116 end 128 117 if act.ptr ~= nil and fakeact == false then act:free() end 129 118 else 130 119 ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end 131 120 end 132 121 return ................................................................................ 134 123 135 124 terra http.post_compose(co: &lib.srv.convo, meth: method.t) 136 125 if not co:assertpow('post') then return end 137 126 --if co.who.rights.powers.post() == false then 138 127 --co:complain(403,'insufficient privileges','you lack the <strong>post</strong> power and cannot perform this action') 139 128 140 129 if meth == method.get then 141 - lib.render.compose(co, nil) 130 + lib.render.compose(co, nil, nil) 142 131 elseif meth == method.post then 143 132 var text, textlen = co:postv("post") 144 133 var acl, acllen = co:postv("acl") 145 134 var subj, subjlen = co:postv("subject") 146 135 if text == nil or acl == nil then 147 136 co:complain(405, 'invalid post', 'every post must have at least body text and an ACL') 148 137 return ................................................................................ 171 160 lib.render.docpage(co,path(1)) 172 161 elseif path.ct == 1 then 173 162 lib.render.docpage(co, rstring.null()) 174 163 else 175 164 co:complain(404, 'no such documentation', 'invalid documentation URL') 176 165 end 177 166 end 167 + 168 +terra http.tweet_page(co: &lib.srv.convo, path: hpath, meth: method.t) 169 + var pid, ok = lib.math.shorthand.parse(path(1).ptr, path(1).ct) 170 + if not ok then 171 + co:complain(400, 'bad post ID', 'that post ID is not valid') 172 + return 173 + end 174 + var post = co.srv:post_fetch(pid) 175 + if not post then 176 + co:complain(404, 'post not found', 'no such post is known to this server') 177 + return 178 + end 179 + defer post:free() 180 + 181 + if path.ct == 3 then 182 + if path(2):cmp(lib.str.lit 'edit') then 183 + if post(0).author ~= co.who.id then 184 + co:complain(403, 'forbidden', 'you cannot edit other people\'s posts') 185 + return 186 + end 187 + 188 + if meth == method.get then 189 + lib.render.compose(co, post.ptr, nil) 190 + return 191 + elseif meth == method.post then 192 + var newbody = co:postv('post')._0 193 + var newacl = co:postv('acl')._0 194 + var newsubj = co:postv('subject')._0 195 + if newbody ~= nil then post(0).body = newbody end 196 + if newacl ~= nil then post(0).acl = newacl end 197 + if newsubj ~= nil then post(0).subject = newsubj end 198 + post(0):save(true) 199 + 200 + var lnk: lib.str.acc lnk:compose('/post/', path(1)) 201 + co:reroute(lnk.buf) 202 + lnk:free() 203 + end 204 + return 205 + else goto badurl end 206 + end 207 + 208 + if meth == method.post then 209 + co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') 210 + return 211 + end 212 + 213 + lib.render.tweet_page(co, path, post.ptr) 214 + do return end 215 + 216 + ::badurl:: co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') 217 +end 178 218 179 219 terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t) 180 220 var msg = pstring.null() 181 221 if meth == method.post and path.ct >= 1 then 182 222 var user_refresh = false var fail = false 183 223 if path(1):cmp(lib.str.lit 'profile') then 224 + lib.dbg('updating profile') 184 225 co.who.bio = co:postv('bio')._0 185 226 co.who.nym = co:postv('nym')._0 186 227 if co.who.bio ~= nil and @co.who.bio == 0 then co.who.bio = nil end 187 228 if co.who.nym ~= nil and @co.who.nym == 0 then co.who.nym = nil end 188 229 co.who.source:actor_save(co.who) 189 230 msg = lib.str.plit 'profile changes saved' 190 231 --user_refresh = true -- not really necessary here, actually 191 232 elseif path(1):cmp(lib.str.lit 'srv') then 192 233 elseif path(1):cmp(lib.str.lit 'users') then 193 - 234 + elseif path(1):cmp(lib.str.lit 'sec') then 235 + var act = co:ppostv('act') 236 + if act:cmp(lib.str.plit 'invalidate') then 237 + lib.dbg('setting user\'s cookie validation time to now') 238 + co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil)) 239 + -- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again 240 + co:installkey('/conf/sec',co.aid) 241 + return 242 + end 194 243 end 195 244 196 245 if user_refresh then -- refresh the user info for the renderer 197 246 var usr = co.srv:actor_fetch_uid(co.who.id) 198 247 lib.mem.heapf(co.who) 199 248 co.who = usr.ptr 200 249 end ................................................................................ 222 271 ct = storage[([i-1])].ct; 223 272 } 224 273 goto [send] 225 274 end 226 275 end 227 276 end 228 277 terra http.static_content(co: &lib.srv.convo, [filename], [flen]) 229 - var hdrs = array(lib.http.header{'Content-Type',nil}) 278 + var hdrs = array( 279 + lib.http.header{'Content-Type',nil}) 230 280 var [page] = lib.http.page { 231 281 respcode = 200; 232 282 headers = [lib.mem.ptr(lib.http.header)] { 233 283 ptr = &hdrs[0], ct = 1 234 284 } 235 285 } 236 286 [branches] ................................................................................ 288 338 if co.aid == 0 289 339 then goto notfound 290 340 else co:reroute_cookie('/','auth=; Path=/') 291 341 end 292 342 return 293 343 else -- hierarchical routes 294 344 var path = lib.http.hier(uri) defer path:free() 295 - if path.ptr[0]:cmp(lib.str.lit('user')) then 345 + if path.ct > 1 and path(0):cmp(lib.str.lit('user')) then 296 346 http.actor_profile_uid(co, path, meth) 297 - elseif path.ptr[0]:cmp(lib.str.lit('tl')) then 347 + elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then 348 + http.tweet_page(co, path, meth) 349 + elseif path(0):cmp(lib.str.lit('tl')) then 298 350 http.timeline(co, path) 299 - elseif path.ptr[0]:cmp(lib.str.lit('doc')) then 351 + elseif path(0):cmp(lib.str.lit('doc')) then 300 352 if meth ~= method.get and meth ~= method.head then goto wrongmeth end 301 353 http.documentation(co, path) 302 - elseif path.ptr[0]:cmp(lib.str.lit('conf')) then 354 + elseif path(0):cmp(lib.str.lit('conf')) then 303 355 if co.aid == 0 then goto unauth end 304 356 http.configure(co,path,meth) 305 357 else goto notfound end 306 358 return 307 359 end 308 360 309 361 ::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end 310 362 ::notfound:: co:complain(404, 'not found', 'no such resource available') do return end 311 363 ::unauth:: co:complain(401, 'unauthorized', 'this content is not available at your clearance level') do return end 312 364 end
Modified session.t from [e8a79576f0] to [78b2aad470].
22 22 [lib.mem.ptr(uint8)] {ptr = [&uint8](secret.ptr), ct = secret.ct}, 23 23 [lib.mem.ptr( int8)] {ptr = out, ct = len}, 24 24 &hash[0]) 25 25 ptr = ptr + lib.math.shorthand.gen(lib.math.truncate64(hash, [hash.type.N]), ptr) 26 26 return ptr - out 27 27 end 28 28 29 -terra m.cookie_interpret(secret: lib.mem.ptr(int8), c: lib.mem.ptr(int8), now: uint64): uint64 -- returns either 0 or a valid authid 29 +terra m.cookie_interpret(secret: lib.mem.ptr(int8), c: lib.mem.ptr(int8), now: uint64) -- returns either 0,0 or a valid {authid, timepoint} 30 30 var authid_sz = lib.str.cspan(c.ptr, lib.str.lit '.', c.ct) 31 - if authid_sz == 0 then return 0 end 32 - if authid_sz + 1 > c.ct then return 0 end 31 + if authid_sz == 0 then return 0,0 end 32 + if authid_sz + 1 > c.ct then return 0,0 end 33 33 var time_sz = lib.str.cspan(c.ptr+authid_sz+1, lib.str.lit '.', c.ct - (authid_sz+1)) 34 - if time_sz == 0 then return 0 end 35 - if (authid_sz + time_sz + 2) > c.ct then return 0 end 34 + if time_sz == 0 then return 0,0 end 35 + if (authid_sz + time_sz + 2) > c.ct then return 0,0 end 36 36 var hash_sz = c.ct - (authid_sz + time_sz + 2) 37 37 38 38 var knownhash: uint8[lib.crypt.algsz.sha256] 39 39 lib.crypt.hmac(lib.crypt.alg.sha256, 40 40 [lib.mem.ptr(uint8)] {ptr = [&uint8](secret.ptr), ct = secret.ct}, 41 41 [lib.mem.ptr( int8)] {ptr = c.ptr, ct = c.ct - hash_sz}, 42 42 &knownhash[0]) 43 43 44 44 var authid, authok = lib.math.shorthand.parse(c.ptr, authid_sz) 45 45 var time, timeok = lib.math.shorthand.parse(c.ptr + authid_sz + 1, time_sz) 46 46 var hash, hashok = lib.math.shorthand.parse(c.ptr + c.ct - hash_sz, hash_sz) 47 - if not (timeok and authok and hashok) then return 0 end 48 - if lib.math.truncate64(knownhash, [knownhash.type.N]) ~= hash then return 0 end 49 - if now - time > m.maxage then return 0 end 47 + if not (timeok and authok and hashok) then return 0,0 end 48 + if lib.math.truncate64(knownhash, [knownhash.type.N]) ~= hash then return 0,0 end 49 + if now - time > m.maxage then return 0,0 end 50 50 51 - return authid 51 + return authid, time 52 52 end 53 53 54 54 return m
Modified srv.t from [4721bcd333] to [9e0d1b7489].
1 1 -- vim: ft=terra 2 2 local util = lib.util 3 3 local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' } 4 4 local pstring = lib.mem.ptr(int8) 5 5 local struct srv 6 6 local struct cfgcache { 7 7 secret: lib.mem.ptr(int8) 8 - instance: lib.mem.ptr(int8) 9 - overlord: &srv 10 8 pol_sec: secmode.t 11 9 pol_reg: bool 10 + credmgd: bool 11 + maxupsz: intptr 12 + instance: lib.mem.ptr(int8) 13 + overlord: &srv 12 14 } 13 15 local struct srv { 14 16 sources: lib.mem.ptr(lib.store.source) 15 17 webmgr: lib.net.mg_mgr 16 18 webcon: &lib.net.mg_connection 17 19 cfg: cfgcache 18 20 id: rawstring ................................................................................ 108 110 end) 109 111 110 112 local struct convo { 111 113 srv: &srv 112 114 con: &lib.net.mg_connection 113 115 msg: &lib.net.mg_http_message 114 116 aid: uint64 -- 0 if logged out 117 + aid_issue: lib.store.timepoint 115 118 who: &lib.store.actor -- who we're logged in as, if aid ~= 0 116 119 peer: lib.store.inet 117 120 reqtype: lib.http.mime.t -- negotiated content type 118 121 -- cache 119 122 navbar: lib.mem.ptr(int8) 120 123 actorcache: lib.mem.cache(lib.mem.ptr(lib.store.actor),32) -- naive cache to avoid unnecessary queries 121 124 -- private ................................................................................ 154 157 155 158 body:send(self.con, 303, [lib.mem.ptr(lib.http.header)] { 156 159 ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0) 157 160 }) 158 161 end 159 162 160 163 terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end 164 + 165 +terra convo:installkey(dest: rawstring, aid: uint64) 166 + var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1] 167 + do var p = &sesskey[0] 168 + p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1]) 169 + p = p + lib.session.cookie_gen(self.srv.cfg.secret, aid, lib.osclock.time(nil), p) 170 + lib.dbg('sending cookie ',{&sesskey[0],15}) 171 + p = lib.str.ncpy(p, '; Path=/', 9) 172 + end 173 + self:reroute_cookie(dest, &sesskey[0]) 174 +end 161 175 162 176 terra convo:complain(code: uint16, title: rawstring, msg: rawstring) 163 177 var hdrs = array( 164 178 lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' }, 165 179 lib.http.header { key = 'Cache-Control', value = 'no-store' } 166 180 ) 167 181 ................................................................................ 232 246 var r = self.vbofs 233 247 self.vbofs = self.vbofs + o + 1 234 248 @(self.vbofs - 1) = 0 235 249 var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o}) 236 250 return norm.ptr, norm.ct 237 251 else return nil, 0 end 238 252 end 253 +terra convo:ppostv(name: rawstring) 254 + var s,l = self:postv(name) 255 + return pstring { ptr = s, ct = l } 256 +end 239 257 240 258 terra convo:getv(name: rawstring) 241 259 if self.varbuf.ptr == nil then 242 260 self.varbuf = lib.mem.heapa(int8, self.msg.query.len + self.msg.body.len) 243 261 self.vbofs = self.varbuf.ptr 244 262 end 245 263 var o = lib.net.mg_http_get_var(&self.msg.query, name, self.vbofs, self.varbuf.ct - (self.vbofs - self.varbuf.ptr)) ................................................................................ 247 265 var r = self.vbofs 248 266 self.vbofs = self.vbofs + o + 1 249 267 @(self.vbofs - 1) = 0 250 268 var norm = lib.str.normalize([lib.mem.ptr(int8)]{ptr = r, ct = o}) 251 269 return norm.ptr, norm.ct 252 270 else return nil, 0 end 253 271 end 272 +terra convo:pgetv(name: rawstring) 273 + var s,l = self:getv(name) 274 + return pstring { ptr = s, ct = l } 275 +end 254 276 255 277 local urimatch = macro(function(uri, ptn) 256 278 return `lib.net.mg_globmatch(ptn, [#ptn], uri.ptr, uri.ct+1) 257 279 end) 258 280 259 281 local route = {} -- these are defined in route.t, as they need access to renderers 260 282 terra route.dispatch_http :: {&convo, lib.mem.ptr(int8), lib.http.method.t} -> {} ................................................................................ 303 325 304 326 switch event do 305 327 case lib.net.MG_EV_HTTP_MSG then 306 328 lib.dbg('routing HTTP request') 307 329 var msg = [&lib.net.mg_http_message](p) 308 330 var co = convo { 309 331 con = con, srv = server, msg = msg; 310 - aid = 0, who = nil, peer = peer; 332 + aid = 0, aid_issue = 0, who = nil; 311 333 reqtype = lib.http.mime.none; 334 + peer = peer; 312 335 } co.varbuf.ptr = nil 313 336 co.navbar.ptr = nil 314 337 co.actorcache.top = 0 315 338 co.actorcache.cur = 0 316 339 317 340 -- first, check for an accept header. if it's there, we need to 318 341 -- iterate over the values and pick the highest-priority one ................................................................................ 388 411 end 389 412 if val.ptr == nil then goto nocookie end 390 413 val.ct = (cookies + i) - val.ptr 391 414 if lib.str.ncmp(key.ptr, lib.session.cookiename, lib.math.biggest([#lib.session.cookiename], key.ct)) ~= 0 then 392 415 goto nocookie 393 416 end 394 417 ::foundcookie:: do 395 - var aid = lib.session.cookie_interpret(server.cfg.secret, 418 + var aid, tp = lib.session.cookie_interpret(server.cfg.secret, 396 419 [lib.mem.ptr(int8)]{ptr=val.ptr,ct=val.ct}, 397 420 lib.osclock.time(nil)) 398 - if aid ~= 0 then co.aid = aid end 421 + if aid ~= 0 then co.aid = aid co.aid_issue = tp end 399 422 end ::nocookie::; 400 423 end 401 424 402 425 if co.aid ~= 0 then 403 - var sess, usr = co.srv:actor_session_fetch(co.aid, peer) 404 - if sess.ok == false then co.aid = 0 else 426 + var sess, usr = co.srv:actor_session_fetch(co.aid, peer, co.aid_issue) 427 + if sess.ok == false then co.aid = 0 co.aid_issue = 0 else 405 428 co.who = usr.ptr 406 429 co.who.rights.powers = server:actor_powers_fetch(co.who.id) 407 430 end 408 431 end 409 432 410 433 var uridec = lib.mem.heapa(int8, msg.uri.len) defer uridec:free() 411 434 var urideclen = lib.net.mg_url_decode(msg.uri.ptr, msg.uri.len, uridec.ptr, uridec.ct, 1) ................................................................................ 640 663 self.sources:free() 641 664 end 642 665 643 666 terra cfgcache:load() 644 667 self.instance = self.overlord:conf_get('instance-name') 645 668 self.secret = self.overlord:conf_get('server-secret') 646 669 647 - self.pol_reg = false 670 + do self.pol_reg = false 648 671 var sreg = self.overlord:conf_get('policy-self-register') 649 - if sreg.ptr ~= nil then 672 + if sreg:ref() then 650 673 if lib.str.cmp(sreg.ptr, 'on') == 0 651 674 then self.pol_reg = true 652 675 else self.pol_reg = false 653 676 end 654 677 end 655 - sreg:free() 678 + sreg:free() end 679 + 680 + do self.credmgd = false 681 + var sreg = self.overlord:conf_get('credential-store') 682 + if sreg:ref() then 683 + if lib.str.cmp(sreg.ptr, 'managed') == 0 684 + then self.credmgd = true 685 + else self.credmgd = false 686 + end 687 + end 688 + sreg:free() end 689 + 690 + do self.maxupsz = [1024 * 100] -- 100 kilobyte default 691 + var sreg = self.overlord:conf_get('maximum-artifact-size') 692 + if sreg:ref() then 693 + var sz, ok = lib.math.fsz_parse(sreg) 694 + if ok then self.maxupsz = sz else 695 + lib.warn('invalid configuration value for maximum-artifact-size; keeping default 100K upload limit') 696 + end 697 + end 698 + sreg:free() end 656 699 657 700 self.pol_sec = secmode.lockdown 658 701 var smode = self.overlord:conf_get('policy-security') 659 702 if smode.ptr ~= nil then 660 703 if lib.str.cmp(smode.ptr, 'public') == 0 then 661 704 self.pol_sec = secmode.public 662 705 elseif lib.str.cmp(smode.ptr, 'private') == 0 then
Added static/query.svg version [eb8b842615].
1 +<?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 +<!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 +<svg 5 + xmlns:dc="http://purl.org/dc/elements/1.1/" 6 + xmlns:cc="http://creativecommons.org/ns#" 7 + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 8 + xmlns:svg="http://www.w3.org/2000/svg" 9 + xmlns="http://www.w3.org/2000/svg" 10 + xmlns:xlink="http://www.w3.org/1999/xlink" 11 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 12 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 13 + width="1in" 14 + height="1in" 15 + viewBox="0 0 25.4 25.400001" 16 + version="1.1" 17 + id="svg8" 18 + inkscape:version="0.92.4 (5da689c313, 2019-01-14)" 19 + sodipodi:docname="query.svg" 20 + inkscape:export-filename="/home/lexi/dev/parsav/static/warn.png" 21 + inkscape:export-xdpi="200" 22 + inkscape:export-ydpi="200"> 23 + <defs 24 + id="defs2"> 25 + <linearGradient 26 + id="linearGradient1362" 27 + inkscape:collect="always"> 28 + <stop 29 + id="stop1358" 30 + offset="0" 31 + style="stop-color:#b64bc7;stop-opacity:1" /> 32 + <stop 33 + id="stop1360" 34 + offset="1" 35 + style="stop-color:#7932a4;stop-opacity:1" /> 36 + </linearGradient> 37 + <linearGradient 38 + inkscape:collect="always" 39 + id="linearGradient973"> 40 + <stop 41 + style="stop-color:#9b3bd7;stop-opacity:1;" 42 + offset="0" 43 + id="stop969" /> 44 + <stop 45 + style="stop-color:#5d267e;stop-opacity:1" 46 + offset="1" 47 + id="stop971" /> 48 + </linearGradient> 49 + <linearGradient 50 + inkscape:collect="always" 51 + id="linearGradient2322"> 52 + <stop 53 + style="stop-color:#ca0050;stop-opacity:1;" 54 + offset="0" 55 + id="stop2318" /> 56 + <stop 57 + style="stop-color:#7b0031;stop-opacity:1" 58 + offset="1" 59 + id="stop2320" /> 60 + </linearGradient> 61 + <linearGradient 62 + inkscape:collect="always" 63 + id="linearGradient2302"> 64 + <stop 65 + style="stop-color:#ffffff;stop-opacity:1;" 66 + offset="0" 67 + id="stop2298" /> 68 + <stop 69 + style="stop-color:#ffffff;stop-opacity:0;" 70 + offset="1" 71 + id="stop2300" /> 72 + </linearGradient> 73 + <linearGradient 74 + inkscape:collect="always" 75 + id="linearGradient851"> 76 + <stop 77 + style="stop-color:#ffffff;stop-opacity:0" 78 + offset="0" 79 + id="stop847" /> 80 + <stop 81 + style="stop-color:#ffa9c6;stop-opacity:1" 82 + offset="1" 83 + id="stop849" /> 84 + </linearGradient> 85 + <radialGradient 86 + inkscape:collect="always" 87 + xlink:href="#linearGradient851" 88 + id="radialGradient853" 89 + cx="12.699999" 90 + cy="285.82184" 91 + fx="12.699999" 92 + fy="285.82184" 93 + r="1.7905753" 94 + gradientTransform="matrix(2.2137788,-2.5697531e-5,6.7771541e-5,5.8383507,-15.43436,-1382.906)" 95 + gradientUnits="userSpaceOnUse" /> 96 + <radialGradient 97 + inkscape:collect="always" 98 + xlink:href="#linearGradient2302" 99 + id="radialGradient2304" 100 + cx="7.2493401" 101 + cy="278.65524" 102 + fx="7.2493401" 103 + fy="278.65524" 104 + r="10.573204" 105 + gradientTransform="matrix(1.1874875,-1.9679213e-8,1.9699479e-8,1.1887104,-2.3813503,-53.649456)" 106 + gradientUnits="userSpaceOnUse" /> 107 + <linearGradient 108 + inkscape:collect="always" 109 + xlink:href="#linearGradient2322" 110 + id="linearGradient2324" 111 + x1="20.298828" 112 + y1="97.196846" 113 + x2="20.298828" 114 + y2="12.911131" 115 + gradientUnits="userSpaceOnUse" 116 + gradientTransform="translate(1.3858268e-6)" /> 117 + <radialGradient 118 + inkscape:collect="always" 119 + xlink:href="#linearGradient2302" 120 + id="radialGradient2304-8" 121 + cx="12.58085" 122 + cy="285.25314" 123 + fx="12.58085" 124 + fy="285.25314" 125 + r="10.573204" 126 + gradientTransform="matrix(4.4881418,-7.4378129e-8,7.4454725e-8,4.4927638,-17.038663,-1237.2864)" 127 + gradientUnits="userSpaceOnUse" /> 128 + <radialGradient 129 + inkscape:collect="always" 130 + xlink:href="#linearGradient2302" 131 + id="radialGradient960" 132 + gradientUnits="userSpaceOnUse" 133 + gradientTransform="matrix(0.77290068,-0.9015271,0.4066395,0.34862174,-111.85097,215.11933)" 134 + cx="23.48217" 135 + cy="269.32919" 136 + fx="23.48217" 137 + fy="269.32919" 138 + r="10.573204" /> 139 + <linearGradient 140 + inkscape:collect="always" 141 + xlink:href="#linearGradient973" 142 + id="linearGradient975" 143 + x1="5.8321538" 144 + y1="275.4801" 145 + x2="21.037828" 146 + y2="292.65216" 147 + gradientUnits="userSpaceOnUse" 148 + gradientTransform="matrix(3.7795276,0,0,3.7795276,0,-1026.5196)" /> 149 + <linearGradient 150 + inkscape:collect="always" 151 + xlink:href="#linearGradient973" 152 + id="linearGradient1014" 153 + gradientUnits="userSpaceOnUse" 154 + x1="5.8321538" 155 + y1="275.4801" 156 + x2="21.037828" 157 + y2="292.65216" 158 + gradientTransform="translate(-42.333335)" /> 159 + <radialGradient 160 + inkscape:collect="always" 161 + xlink:href="#linearGradient2302" 162 + id="radialGradient1016" 163 + gradientUnits="userSpaceOnUse" 164 + gradientTransform="matrix(1.1874875,-1.9679213e-8,1.9699479e-8,1.1887104,-44.714686,-53.649456)" 165 + cx="7.2493401" 166 + cy="278.65524" 167 + fx="7.2493401" 168 + fy="278.65524" 169 + r="10.573204" /> 170 + <radialGradient 171 + inkscape:collect="always" 172 + xlink:href="#linearGradient2302" 173 + id="radialGradient1018" 174 + gradientUnits="userSpaceOnUse" 175 + gradientTransform="matrix(0.77290068,-0.9015271,0.4066395,0.34862174,-154.18433,215.11933)" 176 + cx="23.48217" 177 + cy="269.32919" 178 + fx="23.48217" 179 + fy="269.32919" 180 + r="10.573204" /> 181 + <filter 182 + inkscape:collect="always" 183 + style="color-interpolation-filters:sRGB" 184 + id="filter1222" 185 + x="-0.12898994" 186 + width="1.2579799" 187 + y="-0.05840071" 188 + height="1.1168014"> 189 + <feGaussianBlur 190 + inkscape:collect="always" 191 + stdDeviation="0.33911411" 192 + id="feGaussianBlur1224" /> 193 + </filter> 194 + <filter 195 + inkscape:collect="always" 196 + style="color-interpolation-filters:sRGB" 197 + id="filter1352" 198 + x="-0.15389959" 199 + width="1.3077992" 200 + y="-0.077627083" 201 + height="1.1552542"> 202 + <feGaussianBlur 203 + inkscape:collect="always" 204 + stdDeviation="0.50074934" 205 + id="feGaussianBlur1354" /> 206 + </filter> 207 + <radialGradient 208 + inkscape:collect="always" 209 + xlink:href="#linearGradient1362" 210 + id="radialGradient1356" 211 + cx="6.5823746" 212 + cy="278.22546" 213 + fx="6.5823746" 214 + fy="278.22546" 215 + r="9.8748493" 216 + gradientTransform="matrix(2.0818304,0,0,2.0818304,-8.516817,-303.27685)" 217 + gradientUnits="userSpaceOnUse" /> 218 + <meshgradient 219 + y="277.31741" 220 + x="5.7174305" 221 + gradientUnits="userSpaceOnUse" 222 + id="meshgradient5802" 223 + inkscape:collect="always"> 224 + <meshrow 225 + id="meshrow5804"> 226 + <meshpatch 227 + id="meshpatch5806"> 228 + <stop 229 + path="c 3.85637,-3.85637 10.1088,-3.85637 13.9651,0" 230 + style="stop-color:#eccaff;stop-opacity:1" 231 + id="stop5808" /> 232 + <stop 233 + path="c 3.85637,3.85637 3.85637,10.1088 0,13.9651" 234 + style="stop-color:#800080;stop-opacity:1" 235 + id="stop5810" /> 236 + <stop 237 + path="c -3.85637,3.85637 -10.1088,3.85637 -13.9651,0" 238 + style="stop-color:#ef9aff;stop-opacity:1" 239 + id="stop5812" /> 240 + <stop 241 + path="c -3.85637,-3.85637 -3.85637,-10.1088 0,-13.9651" 242 + style="stop-color:#800080;stop-opacity:1" 243 + id="stop5814" /> 244 + </meshpatch> 245 + </meshrow> 246 + </meshgradient> 247 + </defs> 248 + <sodipodi:namedview 249 + id="base" 250 + pagecolor="#313131" 251 + bordercolor="#666666" 252 + borderopacity="1.0" 253 + inkscape:pageopacity="0" 254 + inkscape:pageshadow="2" 255 + inkscape:zoom="3.959798" 256 + inkscape:cx="81.587655" 257 + inkscape:cy="1.9529273" 258 + inkscape:document-units="mm" 259 + inkscape:current-layer="layer1" 260 + showgrid="false" 261 + units="in" 262 + inkscape:window-width="1920" 263 + inkscape:window-height="1042" 264 + inkscape:window-x="0" 265 + inkscape:window-y="38" 266 + inkscape:window-maximized="0" /> 267 + <metadata 268 + id="metadata5"> 269 + <rdf:RDF> 270 + <cc:Work 271 + rdf:about=""> 272 + <dc:format>image/svg+xml</dc:format> 273 + <dc:type 274 + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 275 + <dc:title></dc:title> 276 + </cc:Work> 277 + </rdf:RDF> 278 + </metadata> 279 + <g 280 + inkscape:label="Layer 1" 281 + inkscape:groupmode="layer" 282 + id="layer1" 283 + transform="translate(0,-271.59998)"> 284 + <circle 285 + style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:url(#radialGradient2304);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" 286 + id="path910" 287 + cx="12.7" 288 + cy="284.29999" 289 + r="9.8746281" /> 290 + <path 291 + id="rect829" 292 + style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 293 + d="m 11.543949,290.50847 -7e-6,0.52521 0.893337,0.89333 0.525444,2.4e-4 0.893329,-0.89333 -2.34e-4,-0.52545 -0.893338,-0.89333 h -0.525203 z" 294 + inkscape:connector-curvature="0" 295 + sodipodi:nodetypes="ccccccccc" /> 296 + <g 297 + aria-label="?" 298 + style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" 299 + id="text877" 300 + transform="matrix(2.1597213,0,0,2.1385442,0.73403254,-313.27713)"> 301 + <path 302 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" 303 + d="m 7.6924933,277.43408 c 0.1165795,0.95131 -0.6562552,1.66432 -1.3319477,2.19936 -0.6322781,0.5146 -0.3926261,1.03197 -0.696752,1.4212 -0.7264746,0.20404 -0.708357,-0.94298 -0.5084803,-1.3771 0.3220556,-0.77195 1.5189026,-1.18082 1.6106521,-2.0731 0.1697248,-0.85219 -1.0638516,-1.33257 -1.6164509,-0.77546 -0.4346018,0.40863 -1.0937506,0.72692 -1.0597346,-0.0803 0.061275,-0.68615 0.5563323,-0.90025 1.144623,-0.96295 0.9913021,-0.15129 2.2140894,0.34092 2.4316458,1.40994 l 0.018295,0.11853 z" 304 + id="path884" 305 + inkscape:connector-curvature="0" 306 + sodipodi:nodetypes="ccccccccccc" /> 307 + </g> 308 + <path 309 + style="opacity:1;vector-effect:none;fill:url(#meshgradient5802);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" 310 + d="M 12.7,274.42513 A 9.874628,9.874628 0 0 0 2.8251504,284.29998 9.874628,9.874628 0 0 0 12.7,294.17483 9.874628,9.874628 0 0 0 22.574849,284.29998 9.874628,9.874628 0 0 0 12.7,274.42513 Z m 0.160197,2.02055 c 1.946164,0.016 4.019091,1.07178 4.43022,3.07216 l 0.03979,0.25373 0.01757,0.25632 c 0.251779,2.03442 -1.41752,3.55938 -2.876827,4.70359 -1.365545,1.10049 -0.847991,2.2067 -1.504818,3.03909 -1.568983,0.43635 -1.529802,-2.01666 -1.098124,-2.94504 0.69555,-1.65085 3.280704,-2.52514 3.478857,-4.43332 0.366559,-1.82245 -2.297799,-2.8497 -3.491259,-1.6583 -0.938619,0.87387 -2.3622147,1.55471 -2.2887497,-0.17156 0.132337,-1.46737 1.2016567,-1.92522 2.4722007,-2.05931 0.267617,-0.0404 0.543115,-0.0596 0.821139,-0.0574 z m -0.422714,13.16922 h 0.525033 l 0.893485,0.89349 v 0.52555 l -0.893485,0.89348 -0.525033,-5.2e-4 -0.893485,-0.89348 v -0.52503 z" 311 + id="circle967" 312 + inkscape:connector-curvature="0" /> 313 + <circle 314 + r="9.8746281" 315 + cy="284.29999" 316 + cx="12.7" 317 + id="circle958" 318 + style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:url(#radialGradient960);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> 319 + <g 320 + aria-label="?" 321 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:31.11434174px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.26458335" 322 + id="text979" /> 323 + <circle 324 + r="9.8746281" 325 + cy="284.29999" 326 + cx="-29.633333" 327 + id="circle998" 328 + style="opacity:1;vector-effect:none;fill:url(#linearGradient1014);fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> 329 + <g 330 + transform="translate(-42.333335,-0.90874193)" 331 + id="g1006"> 332 + <path 333 + id="path1000" 334 + style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 335 + d="m 11.543949,291.41721 -7e-6,0.52521 0.893337,0.89333 0.525444,2.4e-4 0.893329,-0.89333 -2.34e-4,-0.52545 -0.893338,-0.89333 h -0.525203 z" 336 + inkscape:connector-curvature="0" 337 + sodipodi:nodetypes="ccccccccc" /> 338 + <g 339 + aria-label="?" 340 + style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" 341 + id="g1004" 342 + transform="matrix(2.1597213,0,0,2.1385442,0.73403254,-312.36839)"> 343 + <path 344 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" 345 + d="m 7.6924933,277.43408 c 0.1165795,0.95131 -0.6562552,1.66432 -1.3319477,2.19936 -0.6322781,0.5146 -0.3926261,1.03197 -0.696752,1.4212 -0.7264746,0.20404 -0.708357,-0.94298 -0.5084803,-1.3771 0.3220556,-0.77195 1.5189026,-1.18082 1.6106521,-2.0731 0.1697248,-0.85219 -1.0638516,-1.33257 -1.6164509,-0.77546 -0.4346018,0.40863 -1.0937506,0.72692 -1.0597346,-0.0803 0.061275,-0.68615 0.5563323,-0.90025 1.144623,-0.96295 0.9913021,-0.15129 2.2140894,0.34092 2.4316458,1.40994 l 0.018295,0.11853 z" 346 + id="path1002" 347 + inkscape:connector-curvature="0" 348 + sodipodi:nodetypes="ccccccccccc" /> 349 + </g> 350 + </g> 351 + <circle 352 + r="9.8746281" 353 + cy="284.29999" 354 + cx="-29.633333" 355 + id="circle1008" 356 + style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:url(#radialGradient1016);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> 357 + <circle 358 + style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:url(#radialGradient1018);stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" 359 + id="circle1010" 360 + cx="-29.633333" 361 + cy="284.29999" 362 + r="9.8746281" /> 363 + <g 364 + id="g1012" 365 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:31.11434174px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.26458335" 366 + aria-label="?" 367 + transform="translate(-42.333335)" /> 368 + <g 369 + transform="matrix(2.1597213,0,0,2.1385442,0.73403254,-313.27713)" 370 + id="g1024" 371 + style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" 372 + aria-label="?" /> 373 + <g 374 + transform="translate(-3.3333335e-8,-0.90874193)" 375 + id="g1039" 376 + style="opacity:0.64600004;filter:url(#filter1352)"> 377 + <path 378 + id="path1033" 379 + style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 380 + d="m 11.543949,291.41721 -7e-6,0.52521 0.893337,0.89333 0.525444,2.4e-4 0.893329,-0.89333 -2.34e-4,-0.52545 -0.893338,-0.89333 h -0.525203 z" 381 + inkscape:connector-curvature="0" 382 + sodipodi:nodetypes="ccccccccc" /> 383 + <g 384 + aria-label="?" 385 + style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" 386 + id="g1037" 387 + transform="matrix(2.1597213,0,0,2.1385442,0.73403254,-312.36839)"> 388 + <path 389 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';vector-effect:none;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.12330705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" 390 + d="m 7.6924933,277.43408 c 0.1165795,0.95131 -0.6562552,1.66432 -1.3319477,2.19936 -0.6322781,0.5146 -0.3926261,1.03197 -0.696752,1.4212 -0.7264746,0.20404 -0.708357,-0.94298 -0.5084803,-1.3771 0.3220556,-0.77195 1.5189026,-1.18082 1.6106521,-2.0731 0.1697248,-0.85219 -1.0638516,-1.33257 -1.6164509,-0.77546 -0.4346018,0.40863 -1.0937506,0.72692 -1.0597346,-0.0803 0.061275,-0.68615 0.5563323,-0.90025 1.144623,-0.96295 0.9913021,-0.15129 2.2140894,0.34092 2.4316458,1.40994 l 0.018295,0.11853 z" 391 + id="path1035" 392 + inkscape:connector-curvature="0" 393 + sodipodi:nodetypes="ccccccccccc" /> 394 + </g> 395 + </g> 396 + <path 397 + d="m 12.150391,277.24219 a 0.748792,0.748792 0 0 1 -0.0332,0.004 c -0.568389,0.06 -1.027224,0.19774 -1.306641,0.39649 -0.277209,0.19717 -0.442118,0.42727 -0.494141,0.97851 -7.25e-4,0.16027 0.0015,0.16112 0.01172,0.19922 0.194129,-0.0613 0.650675,-0.29317 1.017578,-0.63476 l -0.01953,0.0195 c 0.869948,-0.86845 2.158273,-0.89534 3.158203,-0.45899 0.990823,0.43239 1.8253,1.49756 1.589844,2.76368 -0.148683,1.24943 -1.014306,2.08321 -1.78711,2.75976 -0.782208,0.68479 -1.493175,1.28446 -1.730468,1.84766 a 0.748792,0.748792 0 0 1 -0.0098,0.0254 c -0.113045,0.24312 -0.238287,1.0468 -0.144531,1.52344 0.02878,0.14631 0.0631,0.17601 0.09961,0.24218 0.06761,-0.20148 0.132148,-0.34938 0.222656,-0.73047 0.140154,-0.5901 0.460972,-1.37295 1.275391,-2.02929 a 0.748792,0.748792 0 0 1 0.0078,-0.008 c 0.71169,-0.55801 1.441989,-1.19038 1.9375,-1.86914 0.495512,-0.67875 0.758472,-1.35793 0.660157,-2.15234 a 0.748792,0.748792 0 0 1 -0.0039,-0.041 l -0.01562,-0.2246 -0.0293,-0.19141 c -0.185302,-0.89101 -0.77691,-1.53674 -1.607422,-1.96484 -0.83249,-0.42913 -1.892004,-0.59212 -2.798828,-0.45508 z m 0.548828,13.16797 -0.359375,0.36132 0.359375,0.35938 0.361328,-0.35938 z" 398 + id="path1212" 399 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;vector-effect:none;fill:#fa93ff;fill-opacity:1;stroke:none;stroke-width:0.26500002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter1222)" 400 + inkscape:original="M 12.039062 276.50195 C 10.768518 276.63604 9.6987432 277.09514 9.5664062 278.5625 C 9.4929412 280.28878 10.91685 279.60825 11.855469 278.73438 C 13.048929 277.54298 15.712261 278.57013 15.345703 280.39258 C 15.14755 282.30076 12.562739 283.17532 11.867188 284.82617 C 11.43551 285.75456 11.397815 288.20783 12.966797 287.77148 C 13.623624 286.93909 13.105159 285.83097 14.470703 284.73047 C 15.930011 283.58626 17.599435 282.06176 17.347656 280.02734 L 17.330078 279.77148 L 17.291016 279.51758 C 16.821155 277.23144 14.179998 276.17841 12.039062 276.50195 z M 12.4375 289.61523 L 11.544922 290.50781 L 11.542969 291.0332 L 12.4375 291.92773 L 12.962891 291.92773 L 13.855469 291.0332 L 13.855469 290.50781 L 12.962891 289.61523 L 12.4375 289.61523 z " 401 + inkscape:radius="-0.74871713" 402 + sodipodi:type="inkscape:offset" /> 403 + <path 404 + sodipodi:type="inkscape:offset" 405 + inkscape:radius="-0.16717836" 406 + inkscape:original="M -14.34375 277.00195 C -14.35155 277.00395 -14.359347 277.00486 -14.367188 277.00586 C -14.957202 277.06816 -15.456819 277.209 -15.789062 277.44531 C -16.11815 277.67939 -16.326757 277.98861 -16.384766 278.59375 C -16.396596 278.94282 -16.323264 279.09764 -16.308594 279.11523 C -16.293644 279.13313 -16.320444 279.13779 -16.197266 279.12109 C -15.950904 279.08739 -15.37302 278.75785 -14.949219 278.36328 L -14.960938 278.375 C -14.179326 277.59474 -12.995641 277.56495 -12.070312 277.96875 C -11.152622 278.36922 -10.401691 279.31598 -10.617188 280.46484 C -10.74706 281.62372 -11.562681 282.41436 -12.332031 283.08789 C -13.106385 283.7658 -13.851986 284.37545 -14.125 285.02344 C -14.1275 285.02844 -14.130113 285.03396 -14.132812 285.03906 C -14.279093 285.35366 -14.401281 286.17218 -14.294922 286.71289 C -14.241742 286.98325 -14.139292 287.17048 -14.054688 287.24414 C -14.009657 287.28334 -13.897079 287.25733 -13.828125 287.26562 C -13.685143 287.00234 -13.611598 286.71247 -13.498047 286.23438 C -13.363381 285.66737 -13.078496 284.95798 -12.306641 284.33594 C -12.304641 284.33494 -12.302781 284.33303 -12.300781 284.33203 C -11.583283 283.76946 -10.83545 283.12505 -10.316406 282.41406 C -9.7973609 281.70308 -9.5060504 280.95629 -9.6132812 280.08984 C -9.6147813 280.08084 -9.6160875 280.0716 -9.6171875 280.0625 L -9.6328125 279.82812 L -9.6640625 279.61914 C -9.8643945 278.64441 -10.518063 277.93724 -11.400391 277.48242 C -12.28272 277.02761 -13.384016 276.85692 -14.34375 277.00195 z " 407 + xlink:href="#path1199" 408 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;vector-effect:none;fill:#ffe2fb;fill-opacity:1;stroke:none;stroke-width:0.26500002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" 409 + id="path5733" 410 + inkscape:href="#path1199" 411 + d="m -13.605469,277.11914 c -0.24345,-0.003 -0.482418,0.014 -0.71289,0.0488 -0.01058,0.002 -0.02313,0.003 -0.02734,0.004 a 0.16719508,0.16719508 0 0 1 -0.0039,0 c -0.57365,0.0606 -1.048495,0.20154 -1.341797,0.41015 -0.297394,0.21154 -0.469187,0.45403 -0.525391,1.01758 -3.1e-5,9.2e-4 3e-5,0.001 0,0.002 -0.0052,0.16054 0.0082,0.27296 0.02344,0.3418 0.07849,-0.0163 0.262351,-0.0861 0.46875,-0.20898 0.218361,-0.13004 0.460956,-0.30687 0.662109,-0.49415 a 0.16719508,0.16719508 0 0 1 0.05273,-0.0312 c 0.0011,-0.001 0.0028,-9.3e-4 0.0039,-0.002 0.838825,-0.77648 2.051057,-0.80558 3.001953,-0.39062 0.975575,0.42573 1.78232,1.4453 1.550781,2.67968 -0.141936,1.22067 -0.998063,2.04142 -1.769531,2.7168 -0.776996,0.68022 -1.502751,1.2928 -1.748047,1.875 a 0.16719508,0.16719508 0 0 1 -0.0039,0.01 c -7.97e-4,0.002 -0.0037,0.007 -0.0059,0.0117 -0.05727,0.12317 -0.127965,0.40183 -0.162109,0.70117 -0.03414,0.29933 -0.03604,0.62635 0.01172,0.86914 0.0462,0.2349 0.143766,0.38461 0.177734,0.41992 0.0013,-2e-5 0.0045,4e-5 0.0059,0 0.109999,-0.22507 0.183634,-0.46863 0.28711,-0.9043 0.139671,-0.58808 0.443488,-1.34184 1.248047,-1.99023 a 0.16719508,0.16719508 0 0 1 0.0078,-0.004 c 0.0021,-0.002 0.0037,-0.004 0.0059,-0.006 a 0.16719508,0.16719508 0 0 1 0.002,-0.002 c 0.71045,-0.55752 1.444993,-1.19161 1.945312,-1.87695 0.5037376,-0.69002 0.7731349,-1.38688 0.6718751,-2.20508 -0.00154,-0.01 -0.00277,-0.0199 -0.00391,-0.0293 a 0.16719508,0.16719508 0 0 1 0,-0.008 l -0.015625,-0.22852 -0.029297,-0.19336 -0.00195,-0.004 c -0.1901449,-0.91782 -0.7983519,-1.58039 -1.6464839,-2.01758 -0.635335,-0.32749 -1.398557,-0.50322 -2.128907,-0.51172 z" 412 + transform="translate(26.458334)" /> 413 + <path 414 + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:1.25;font-family:'Averia Serif Libre';-inkscape-font-specification:'Averia Serif Libre';letter-spacing:0px;word-spacing:0px;vector-effect:none;fill:#ffe2fb;fill-opacity:1;stroke:none;stroke-width:0.26500002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" 415 + d="m -14.343099,277.00195 c -0.0078,0.002 -0.0156,0.003 -0.02344,0.004 -0.590014,0.0623 -1.089633,0.20314 -1.421876,0.43945 -0.329088,0.23408 -0.537694,0.5433 -0.595703,1.14844 -0.01183,0.34907 0.0615,0.50389 0.07617,0.52148 0.01495,0.0179 -0.01185,0.0227 0.111328,0.006 0.246362,-0.0337 0.824247,-0.36324 1.248048,-0.75781 l -0.01172,0.0117 c 0.781612,-0.78026 1.965296,-0.81005 2.890625,-0.40625 0.91769,0.40047 1.668621,1.34723 1.453124,2.49609 -0.129872,1.15888 -0.945493,1.94952 -1.714843,2.62305 -0.774354,0.67791 -1.519955,1.28756 -1.792969,1.93555 -0.0025,0.005 -0.0051,0.0105 -0.0078,0.0156 -0.14628,0.3146 -0.268469,1.13312 -0.16211,1.67383 0.05318,0.27036 0.155631,0.45759 0.240235,0.53125 0.04503,0.0392 0.157608,0.0132 0.226562,0.0215 0.142982,-0.26329 0.216528,-0.55315 0.330079,-1.03124 0.134666,-0.56701 0.419551,-1.2764 1.191406,-1.89844 0.002,-10e-4 0.0039,-0.003 0.0059,-0.004 0.717498,-0.56257 1.46533,-1.20698 1.984374,-1.91797 0.5190453,-0.71098 0.8103562,-1.45777 0.7031253,-2.32422 -0.0015,-0.009 -0.0028,-0.0182 -0.0039,-0.0273 l -0.01563,-0.23438 -0.03125,-0.20898 c -0.200332,-0.97473 -0.8539993,-1.6819 -1.7363273,-2.13672 -0.882329,-0.45481 -1.983626,-0.6255 -2.94336,-0.48047 z" 416 + id="path1199" 417 + inkscape:connector-curvature="0" 418 + sodipodi:nodetypes="ccccccccccscccccccccccccccc" /> 419 + <path 420 + style="opacity:1;fill:#ffe7f0;fill-opacity:1;stroke:none;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" 421 + d="m 12.204228,290.65853 -3e-6,0.22524 0.383115,0.38311 0.225341,1.1e-4 0.383112,-0.38311 -1.01e-4,-0.22535 -0.383115,-0.38311 h -0.225238 z" 422 + id="path1232" 423 + inkscape:connector-curvature="0" /> 424 + </g> 425 +</svg>
Modified static/style.scss from [2a06f65525] to [9b25bded91].
9 9 @extend %sans; 10 10 background-color: tone(-55%); 11 11 color: tone(25%); 12 12 font-size: 14pt; 13 13 margin: 0; 14 14 padding: 0; 15 15 } 16 +::selection { 17 + color: tone(-60%); 18 + background-color: tone(-10%); 19 +} 20 +::placeholder { 21 + color: tone(0,-0.3); 22 + font-style: italic; 23 +} 16 24 a[href] { 17 25 color: tone(10%); 18 26 text-decoration-color: tone(10%,-0.5); 19 27 &:hover { 20 28 color: white; 21 29 text-shadow: 0 0 15px tone(20%); 22 30 text-decoration-color: tone(10%,-0.1); 23 31 } 32 + &.button { @extend %button; } 24 33 } 25 34 a[href^="//"], 26 35 a[href^="http://"], 27 36 a[href^="https://"] { // external link 28 37 &:hover::after { 29 38 color: black; 30 39 background-color: white; ................................................................................ 51 60 %glow { 52 61 box-shadow: 0 0 20px tone(0%,-0.8); 53 62 } 54 63 55 64 %button { 56 65 @extend %sans; 57 66 font-size: 14pt; 67 + box-sizing: border-box; 58 68 padding: 0.1in 0.2in; 59 69 border: 1px solid black; 60 70 color: tone(25%); 61 71 text-shadow: 1px 1px black; 62 72 text-decoration: none; 63 73 text-align: center; 64 74 cursor: default; ................................................................................ 124 134 } 125 135 126 136 $grad-ui-focus: linear-gradient(to bottom, 127 137 tone(-50%), 128 138 tone(-35%) 129 139 ); 130 140 131 -input[type='text'], input[type='password'], textarea { 141 +input[type='text'], input[type='password'], textarea, select { 132 142 @extend %serif; 133 143 padding: 0.08in 0.1in; 134 144 box-sizing: border-box; 135 145 border: 1px solid black; 136 146 background: linear-gradient(to bottom, tone(-55%), tone(-40%)); 137 147 font-size: 16pt; 138 148 color: tone(25%); ................................................................................ 141 151 color: white; 142 152 border-image: linear-gradient(to bottom, tone(-10%), tone(-30%)) 1 / 1px; 143 153 background: $grad-ui-focus; 144 154 outline: none; 145 155 @extend %glow; 146 156 } 147 157 } 158 +select { width: 100%; } 148 159 149 160 @mixin glass { 150 161 @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) { 151 162 backdrop-filter: blur(40px); 152 163 -webkit-backdrop-filter: blur(40px); 153 164 background-color: tone(-53%, -0.7); 154 165 } ................................................................................ 262 273 grid-row: 2 / 3; 263 274 } 264 275 } 265 276 > .stats { 266 277 grid-column: 3 / 4; 267 278 grid-row: 1 / 3; 268 279 } 269 - > .menu { 280 + > form.actions { 270 281 grid-column: 1 / 3; grid-row: 2 / 3; 271 282 padding-top: 0.075in; 272 283 flex-wrap: wrap; 273 284 display: flex; 274 285 justify-content: center; 275 286 align-items: center; 276 287 > a[href] { 277 - @extend %button; 278 288 display: block; 279 289 margin: 0.025in 0.05in; 280 290 } 281 291 > hr { 282 292 all: unset; 283 293 display: block; 284 294 height: 0.3in; ................................................................................ 289 299 } 290 300 291 301 .epithet { 292 302 display: inline-block; 293 303 background: tone(20%); 294 304 color: tone(-45%); 295 305 text-shadow: 0 0 3px tone(-30%, -0.4); 296 - border-radius: 3px; 306 + border-radius: 2px; 297 307 padding: 6px; 298 308 padding-top: 2px; 299 309 padding-bottom: 4px; 300 310 font-size: 80%; 301 311 vertical-align: top; 302 312 font-weight: 300; 303 313 letter-spacing: 0.5px; ................................................................................ 321 331 tone(-55%) 10%, 322 332 tone(-50%) 80%, 323 333 tone(-45%) 324 334 ); 325 335 // outline: 1px solid black; 326 336 } 327 337 328 -body.error .message { 338 +//body.error .message { 339 +.message { 329 340 @extend %box; 341 + display: block; 330 342 width: 4in; 331 343 margin:auto; 332 344 padding: 0.5in; 333 345 text-align: center; 334 346 } 335 347 336 348 div.login { ................................................................................ 469 481 padding: 0.1in; 470 482 padding-left: 0.15in; 471 483 >.nym { font-weight: bold; } 472 484 color: tone(0%,-0.4); 473 485 > span.nym { color: tone(10%) } 474 486 > span.handle { color: tone(-5%) } 475 487 background: linear-gradient(to right, tone(-55%), transparent); 488 + &:hover { 489 + > span.nym { color: white; } 490 + > span.handle { color: tone(15%) } 491 + } 476 492 } 477 493 >.content { 478 494 grid-column: 2/4; grid-row: 1/2; 479 495 padding: 0.2in; 480 496 @extend %serif; 481 497 font-size: 110%; 482 498 text-align: justify; ................................................................................ 510 526 } 511 527 } 512 528 513 529 body.conf main { 514 530 display: grid; 515 531 grid-template-columns: 2in 1fr; 516 532 grid-template-rows: max-content 1fr; 517 - > .menu { 533 + > menu { 518 534 margin-left: -0.25in; 519 535 grid-column: 1/2; grid-row: 1/2; 520 536 background: linear-gradient(to bottom, tone(-45%),tone(-55%)); 521 537 border: 1px solid black; 522 538 padding: 0.1in; 523 539 > a[href] { 524 540 @extend %button; ................................................................................ 546 562 border-left: none; 547 563 text-shadow: 1px 1px 0 black; 548 564 } 549 565 } 550 566 551 567 } 552 568 569 +hr { 570 + border: none; 571 + border-top: 1px solid tone(-30%); 572 + border-bottom: 1px solid tone(-55%); 573 +} 553 574 form { 575 + margin: 0.15in 0; 576 + > p:first-child { margin-top: 0; } 577 + > p:last-child { margin-bottom: 0; } 554 578 .elem { 555 579 margin: 0.1in 0; 556 580 label { display:block; font-weight: bold; padding: 0.03in 0; } 557 581 .txtbox { 558 582 @extend %serif; 559 583 box-sizing: border-box; 560 584 padding: 0.08in 0.1in; 561 585 border: 1px solid black; 562 586 background: tone(-55%); 563 587 } 564 - textarea { resize: vertical; min-height: 2in; } 565 588 input, textarea, .txtbox { 566 589 display: block; 567 590 width: 100%; 568 591 } 569 - button { float: right; width: 50%; } 592 + textarea { resize: vertical; min-height: 2in; } 593 + } 594 + .elem + %button { margin-left: 50%; width: 50%; } 595 +} 596 + 597 +menu.choice { 598 + display: flex; 599 + &.horizontal { 600 + flex-flow: row-reverse wrap; 601 + justify-content: space-evenly; 602 + } 603 + &.vertical { 604 + flex-flow: column; 605 + margin-left: 50%; 606 + } 607 + &.vertical-float { 608 + flex-flow: column; 609 + float: right; 610 + width: 40%; 611 + margin-left: 0.1in; 612 + } 613 + > %button { display: block; margin: 2px; flex-grow: 1 } 614 +} 615 + 616 +.check-panel { 617 + display: flex; 618 + flex-flow: row wrap; 619 + > label { 620 + display: block; 621 + box-sizing: border-box; 622 + width: calc(50% - 0.2in); 623 + padding: 0.1in 0.1in; 624 + margin: 0.1in 0.1in; 625 + background: tone(-45%); 626 + border: 1px solid black; 627 + text-shadow: 1px 1px black; 628 + flex-grow: 1; 629 + &:focus-within { 630 + border: 1px inset tone(-10%); 631 + background: tone(-50%); 632 + } 633 + } 634 + input[type="checkbox"] { 635 + -webkit-appearance: none; 636 + padding: 0.5em; 637 + background: tone(-35%); 638 + border: 1px outset tone(-50%); 639 + vertical-align: bottom; 640 + box-shadow: 0 1px tone(-50%); 641 + &:checked { 642 + border: 1px inset tone(-35%); 643 + background: tone(-60%); 644 + box-shadow: 0 1px tone(-40%); 645 + } 646 + &:focus { 647 + border-color: tone(10%); 648 + outline: none; 649 + } 570 650 } 571 651 } 572 652 573 653 @keyframes flashup { 574 654 0% { opacity: 0; transform: scale(0.8); } 575 655 10% { opacity: 1; transform: scale(1.1); } 576 656 80% { opacity: 1; transform: scale(1); } ................................................................................ 591 671 border-radius: 3px; 592 672 box-shadow: 0 0 50px tone(-55%); 593 673 color: white; 594 674 animation: ease forwards flashup; 595 675 //cubic-bezier(0.4, 0.63, 0.6, 0.31) 596 676 animation-duration: 3s; 597 677 } 678 + 679 +form.action-bar { 680 + display: flex; 681 + > * { 682 + flex-grow: 1; 683 + flex-basis: 0; 684 + margin-left: 0.1in; 685 + } 686 + > *:first-child { 687 + margin-left: 0; 688 + } 689 +}
Modified store.t from [f53ab94a55] to [004846cca6].
1 1 -- vim: ft=terra 2 2 local m = { 3 - timepoint = int64; 3 + timepoint = lib.osclock.time_t; 4 4 scope = lib.enum { 5 5 'public', 'private', 'local'; 6 6 'personal', 'direct', 'circle'; 7 7 }; 8 8 notiftype = lib.enum { 9 9 'mention', 'like', 'rt', 'react' 10 10 }; 11 11 12 - relation = lib.enum { 13 - 'follow', 'mute', 'block' 12 + relation = lib.set { 13 + 'silence', -- messages will not be accepted 14 + 'collapse', -- posts will be collapsed by default 15 + 'disemvowel', -- posts will be ritually humiliated, but shown 16 + 'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations 17 + 'follow', 18 + 'mute', -- posts will be completely hidden at all times 19 + 'block', -- no interactions will be permitted, but posts will remain visible 14 20 }; 15 21 credset = lib.set { 16 22 'pw', 'otp', 'challenge', 'trust' 17 23 }; 18 24 privset = lib.set { 19 25 'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite' 20 26 }; ................................................................................ 142 148 id: uint64 143 149 author: uint64 144 150 subject: str 145 151 body: str 146 152 acl: str 147 153 posted: m.timepoint 148 154 discovered: m.timepoint 155 + edited: m.timepoint 156 + chgcount: uint 149 157 mentions: lib.mem.ptr(uint64) 150 158 circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle 151 159 convoheaduri: str 152 160 parent: uint64 153 161 -- ephemera 154 162 localpost: bool 155 163 source: &m.source 164 + 165 + -- save :: bool -> {} (defined in acl.t due to dep. hell) 156 166 } 157 167 158 168 local cnf = terralib.memoize(function(ty,rty) 159 169 rty = rty or ty 160 170 return struct { 161 171 enum: {&opaque, uint64, rawstring} -> intptr 162 172 get: {&opaque, uint64, rawstring} -> rty ................................................................................ 227 237 aid: uint64 228 238 uid: uint64 229 239 aname: str 230 240 netmask: m.inet 231 241 privs: m.privset 232 242 blacklist: bool 233 243 } 244 + 245 +struct m.relationship { 246 + agent: uint64 247 + patient: uint64 248 + rel: m.relation -- agent → patient 249 + recip: m.relation -- patient → agent 250 +} 234 251 235 252 -- backends only handle content on the local server 236 253 struct m.backend { id: rawstring 237 254 open: &m.source -> &opaque 238 255 close: &m.source -> {} 239 256 dbsetup: &m.source -> bool -- creates the schema needed to call conprep (called only once per database e.g. with `parsav db init`) 240 257 conprep: {&m.source, m.prepmode.t} -> {} -- prepares queries and similar tasks that require the schema to already be in place ................................................................................ 256 273 actor_save_privs: {&m.source, &m.actor} -> {} 257 274 actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor) 258 275 actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor) 259 276 actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif) 260 277 actor_enum: {&m.source} -> lib.mem.ptr(&m.actor) 261 278 actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor) 262 279 actor_stats: {&m.source, uint64} -> m.actor_stats 280 + actor_rel: {&m.source, uint64, uint64} -> m.relationship 263 281 264 282 actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool} 265 283 -- returns a set of auth method categories that are available for a 266 284 -- given user from a certain origin 267 285 -- origin: inet 268 286 -- username: rawstring 269 287 actor_auth_otp: {&m.source, m.inet, rawstring, rawstring} ................................................................................ 283 301 -> {uint64, uint64, pstr} 284 302 -- handles API authentication 285 303 -- origin: inet 286 304 -- handle: rawstring 287 305 -- key: rawstring (X-API-Key) 288 306 actor_auth_record_fetch: {&m.source, uint64} -> lib.mem.ptr(m.auth) 289 307 actor_powers_fetch: {&m.source, uint64} -> m.powerset 290 - actor_session_fetch: {&m.source, uint64, m.inet} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)} 308 + actor_session_fetch: {&m.source, uint64, m.inet, m.timepoint} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)} 291 309 -- retrieves an auth record + actor combo suitable by AID suitable 292 310 -- for determining session validity & caps 293 311 -- aid: uint64 294 312 -- origin: inet 313 + -- cookie issue time: m.timepoint 295 314 actor_auth_register_uid: {&m.source, uint64, uint64} -> {} 296 315 -- notifies the backend module of the UID that has been assigned for 297 316 -- an authentication ID 298 317 -- aid: uint64 299 318 -- uid: uint64 300 319 301 320 actor_conf_str: cnf(rawstring, lib.mem.ptr(int8)) ................................................................................ 304 323 auth_create_pw: {&m.source, uint64, bool, lib.mem.ptr(int8)} -> {} 305 324 -- uid: uint64 306 325 -- reset: bool (delete other passwords?) 307 326 -- pw: pstring 308 327 auth_purge_pw: {&m.source, uint64, rawstring} -> {} 309 328 auth_purge_otp: {&m.source, uint64, rawstring} -> {} 310 329 auth_purge_trust: {&m.source, uint64, rawstring} -> {} 330 + auth_sigtime_user_fetch: {&m.source, uint64} -> m.timepoint 331 + -- authentication tokens and accounts have a property that controls 332 + -- whether auth cookies dated to a certain point are valid. cookies 333 + -- that are generated before the timepoint are considered invalid. 334 + -- this is used primarily to lock out untrusted sessions. 335 + -- uid: uint64 336 + auth_sigtime_user_alter: {&m.source, uint64, m.timepoint} -> {} 337 + -- uid: uint64 338 + -- timestamp: timepoint 311 339 312 340 post_save: {&m.source, &m.post} -> {} 313 341 post_create: {&m.source, &m.post} -> uint64 342 + post_fetch: {&m.source, uint64} -> lib.mem.ptr(m.post) 314 343 post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post)) 315 344 post_attach_ctl: {&m.source, uint64, uint64, bool} -> {} 316 345 -- attaches or detaches an existing database artifact 317 346 -- post id: uint64 318 347 -- artifact id: uint64 319 348 -- detach: bool 320 349 artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64
Modified str.t from [f2457b558f] to [265a0fa659].
1 1 -- vim: ft=terra 2 2 -- string.t: string classes 3 3 local util = lib.util 4 4 local pstr = lib.mem.ptr(int8) 5 5 local pref = lib.mem.ref(int8) 6 6 7 7 local m = { 8 + t = pstr, ref = pref; 8 9 sz = terralib.externfunction('strlen', rawstring -> intptr); 9 10 cmp = terralib.externfunction('strcmp', {rawstring, rawstring} -> int); 10 11 ncmp = terralib.externfunction('strncmp', {rawstring, rawstring, intptr} -> int); 11 12 cpy = terralib.externfunction('stpcpy',{rawstring, rawstring} -> rawstring); 12 13 ncpy = terralib.externfunction('stpncpy',{rawstring, rawstring, intptr} -> rawstring); 13 14 cat = terralib.externfunction('strcat',{rawstring, rawstring} -> rawstring); 14 15 ncat = terralib.externfunction('strncat',{rawstring, rawstring, intptr} -> rawstring); ................................................................................ 92 93 93 94 struct m.acc { 94 95 buf: rawstring 95 96 sz: intptr 96 97 run: intptr 97 98 space: intptr 98 99 } 100 + 101 +terra m.cdowncase(c: int8) 102 + if c >= @'A' and c <= @'Z' then 103 + return c + (@'a' - @'A') 104 + else return c end 105 +end 106 + 107 +terra m.cupcase(c: int8) 108 + if c >= @'a' and c <= @'z' then 109 + return c - (@'a' - @'A') 110 + else return c end 111 +end 99 112 100 113 local terra biggest(a: intptr, b: intptr) 101 114 if a > b then return a else return b end 102 115 end 103 116 104 117 terra m.acc:init(run: intptr) 105 118 --lib.dbg('initializing string accumulator')
Modified view/conf-profile.tpl from [f1f77d7014] to [d384fd3b9f].
1 1 <form method="post"> 2 - <div class="elem"><label>handle</label> <div class="txtbox">@!handle</div> 2 + <div class="elem"><label>handle</label> <div class="txtbox">@!handle</div></div> 3 3 <div class="elem"><label for="nym">display name</label> <input type="text" name="nym" id="nym" placeholder="j. random poster" value="@:nym"></div> 4 4 <div class="elem"><label for="bio">bio</label><textarea name="bio" id="bio" placeholder="tall, dark, and mysterious">@!bio</textarea></div> 5 5 <button>commit</button> 6 6 </form>
Added view/conf-sec-credmg.tpl version [43efff9618].
1 +<hr> 2 +<form method="post"> 3 + <p>your account can currently be accessed with the credentials listed below. if you fear a credential has been compromised, you can revoke or reset it.</p> 4 + <select size="6" name="cred"> 5 + @credlist 6 + </select> 7 + <menu class="horizontal choice"> 8 + <button name="act" value="reset">reset</button> 9 + <button name="act" value="revoke">revoke</button> 10 + </menu> 11 +</form> 12 +<hr> 13 +<form method="post"> 14 + <p>you can associate extra credentials with your account. you can also limit how much of your authority these credentials can be used to exercise — for instance, it might be useful to create API keys that can read your timeline, but not post as you or access any administrative powers you may have. if you don't select a capability set, the credential will be able to wield the full scope of your powers.</p> 15 + <div class="check-panel"> 16 + <label><input type="checkbox" name="allow-post"> post</label> 17 + <label><input type="checkbox" name="allow-edit"> edit</label> 18 + <label><input type="checkbox" name="allow-acct"> manage account</label> 19 + <label><input type="checkbox" name="allow-upload"> upload artifacts</label> 20 + <label><input type="checkbox" name="allow-censor"> moderation</label> 21 + <label><input type="checkbox" name="allow-admin"> other admin powers</label> 22 + <label><input type="checkbox" name="allow-invite"> invite</label> 23 + </div> 24 + <p>you can also specify an IP address range in CIDR format to associate with this credential. if you do so, this credential will only be usable when connecting from an IP address in that range. otherwise, it will be valid when connecting from anywhere on the internet.</p> 25 + <div class="elem"> 26 + <label for="netmask">netmask</label> 27 + <input type="text" name="netmask" id="netmask" placeholder="10.0.0.0/8"> 28 + </div> 29 + <menu class="vertical choice"> 30 + <button name="kind" value="pw">new password</button> 31 + <button name="kind" value="otp">new OTP key</button> 32 + <button name="kind" value="api">new API token</button> 33 + <button name="kind" value="challenge">new challenge key</button> 34 + </div> 35 +</form>
Modified view/conf-sec.tpl from [7ba95a81c5] to [de1cf7e8f0].
1 1 <form method="post"> 2 2 <p>if you are concerned that your account may have been compromised, you can terminate all other login sessions by invalidating their session cookies. note that this will not have any effect on API tokens; these must be revoked separately!</p> 3 - <label> 4 - sessions valid from 3 + <div class="elem"> 4 + <label> sessions valid from </label> 5 5 <div class="txtbox">@lastreset</div> 6 - </label> 6 + </div> 7 7 <button type="submit" name="act" value="invalidate"> 8 8 invalidate other sessions 9 9 </button> 10 10 </form>
Modified view/conf.tpl from [bd130ad9d3] to [09b447ff32].
1 -<div class="menu"> 1 +<menu> 2 2 <a href="/conf/profile">profile</a> 3 3 <a href="/conf/avi">avatar</a> 4 4 <a href="/conf/sec">security</a> 5 5 <a href="/conf/rel">relationships</a> 6 6 <a href="/conf/qnt">quarantine</a> 7 7 <a href="/conf/acl">ACL shortcuts</a> 8 8 <a href="/conf/rooms">chatrooms</a> 9 9 <a href="/conf/circles">circles</a> 10 10 @menu 11 -</div> 11 +</menu> 12 12 13 13 <div class="panel"> 14 14 @panel 15 15 </div>
Added view/confirm.tpl version [9198c794e9].
1 +<form class="message"> 2 + <img class="icon" src="/s/query.webp"> 3 + <h1>@title</h1> 4 + <p>@query</p> 5 + <menu class="horizontal choice"> 6 + <a class="button" href="@:cancel">cancel</a> 7 + <button name="act" value="confirm">confirm</button> 8 + </menu> 9 +</form>
Modified view/load.lua from [f63ef60595] to [212041720e].
1 1 -- because lua can't scan directories, we need a 2 2 -- file that indexes the templates manually, and 3 3 -- copies them into a data structure we can then 4 4 -- create templates from when we return to terra 5 5 local path = ... 6 6 local sources = { 7 7 'docskel'; 8 + 'confirm'; 8 9 'tweet'; 9 10 'profile'; 10 11 'compose'; 11 12 12 13 'login-username'; 13 14 'login-challenge'; 14 15 15 16 'conf'; 16 17 'conf-profile'; 18 + 'conf-sec'; 19 + 'conf-sec-credmg'; 17 20 } 18 21 19 22 local ingest = function(filename) 20 23 local hnd = io.open(path..'/'..filename) 21 24 local txt = hnd:read('*a') 22 25 io.close(hnd) 23 26 txt = txt:gsub('([^\\])!%b[]', '%1')
Modified view/profile.tpl from [d21ccabbe8] to [cfeb837b05].
9 9 <table class="stats"> 10 10 <tr><th>posts</th> <td>@nposts</td></tr> 11 11 <tr><th>following</th> <td>@nfollows</td></tr> 12 12 <tr><th>followers</th> <td>@nfollowers</td></tr> 13 13 <tr><th>mutuals</th> <td>@nmutuals</td></tr> 14 14 <tr><th>@timephrase</th> <td>@tweetday</td></tr> 15 15 </table> 16 - <div class="menu"> 17 - <a href="/@:xid">posts</a> 18 - <a href="/@:xid/media">media</a> 19 - <a href="/@:xid/social">associates</a> 16 + <form class="actions"> 17 + <a class="button" href="/@:xid">posts</a> 18 + <a class="button" href="/@:xid/media">media</a> 19 + <a class="button" href="/@:xid/social">associates</a> 20 20 <hr> 21 21 @auxbtn 22 - </div> 22 + </form> 23 23 </div>