| Comment: | work on admin ui |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
7129658e1d31700f1ac316f1d4d2962d |
| User & Date: | lexi on 2021-01-02 04:47:03 |
| Other Links: | manifest | tags |
|
2021-01-02
| ||
| 18:32 | iterate on user mgmt UI check-in: f09cd18161 user: lexi tags: trunk | |
| 04:47 | work on admin ui check-in: 7129658e1d user: lexi tags: trunk | |
|
2021-01-01
| ||
| 16:42 | handle (some) deletions in live.js check-in: 53ef86f7ff user: lexi tags: trunk | |
Modified backend/pgsql.t from [30375d8380] to [962a3e64e0].
26 26 27 27 actor_fetch_uid = { 28 28 params = {uint64}, sql = [[ 29 29 select a.id, a.nym, a.handle, a.origin, a.bio, 30 30 a.avataruri, a.rank, a.quota, a.key, a.epithet, 31 31 extract(epoch from a.knownsince)::bigint, 32 32 coalesce(a.handle || '@' || s.domain, 33 - '@' || a.handle) as xid 33 + '@' || a.handle) as xid, 34 + a.invites 34 35 35 36 from parsav_actors as a 36 37 left join parsav_servers as s 37 38 on a.origin = s.id 38 39 where a.id = $1::bigint 39 40 ]]; 40 41 }; ................................................................................ 42 43 actor_fetch_xid = { 43 44 params = {pstring}, sql = [[ 44 45 select a.id, a.nym, a.handle, a.origin, a.bio, 45 46 a.avataruri, a.rank, a.quota, a.key, a.epithet, 46 47 extract(epoch from a.knownsince)::bigint, 47 48 coalesce(a.handle || '@' || s.domain, 48 49 '@' || a.handle) as xid, 50 + a.invites, 49 51 50 52 coalesce(s.domain, 51 53 (select value from parsav_config 52 54 where key='domain' limit 1)) as domain 53 55 54 56 from parsav_actors as a 55 57 left join parsav_servers as s ................................................................................ 58 60 where $1::text = (a.handle || '@' || domain) or 59 61 $1::text = ('@' || a.handle || '@' || domain) or 60 62 (a.origin is null and 61 63 $1::text = a.handle or 62 64 $1::text = ('@' || a.handle)) 63 65 ]]; 64 66 }; 67 + 68 + actor_purge_uid = { 69 + params = {uint64}, cmd = true, sql = [[ 70 + with d as ( -- cheating 71 + delete from parsav_sanctions where victim = $1::bigint 72 + ) 73 + delete from parsav_actors where id = $1::bigint 74 + ]]; 75 + }; 65 76 66 77 actor_save = { 67 78 params = { 68 79 uint64, --id 69 80 rawstring, --nym 70 81 rawstring, --handle 71 82 rawstring, --bio 72 83 rawstring, --epithet 73 84 rawstring, --avataruri 74 85 uint64, --avatarid 75 86 uint16, --rank 76 - uint32 --quota 87 + uint32, --quota 88 + uint32 --invites 77 89 }, cmd = true, sql = [[ 78 90 update parsav_actors set 79 91 nym = $2::text, 80 92 handle = $3::text, 81 93 bio = $4::text, 82 94 epithet = $5::text, 83 95 avataruri = $6::text, 84 96 avatarid = $7::bigint, 85 97 rank = $8::smallint, 86 - quota = $9::integer 87 - --invites are controlled by their own specialized routines 98 + quota = $9::integer, 99 + invites = $10::integer 88 100 where id = $1::bigint 89 101 ]]; 90 102 }; 91 103 92 104 actor_create = { 93 105 params = { 94 106 rawstring, rawstring, uint64, lib.store.timepoint, 95 107 rawstring, rawstring, lib.mem.ptr(uint8), 96 - rawstring, uint16, uint32 108 + rawstring, uint16, uint32, uint32 97 109 }; 98 110 sql = [[ 99 111 insert into parsav_actors ( 100 112 nym,handle, 101 113 origin,knownsince, 102 114 bio,avataruri,key, 103 - epithet,rank,quota 115 + epithet,rank,quota, 116 + invites 104 117 ) values ($1::text, $2::text, 105 118 case when $3::bigint = 0 then null 106 119 else $3::bigint end, 107 120 to_timestamp($4::bigint), 108 121 $5::bigint, $6::bigint, $7::bytea, 109 - $8::text, $9::smallint, $10::integer 122 + $8::text, $9::smallint, $10::integer, 123 + $11::integer 110 124 ) returning id 111 125 ]]; 112 126 }; 113 127 114 128 actor_auth_pw = { 115 129 params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[ 116 130 select a.aid, a.uid, a.name from parsav_auth as a ................................................................................ 125 139 }; 126 140 127 141 actor_enum_local = { 128 142 params = {}, sql = [[ 129 143 select id, nym, handle, origin, bio, 130 144 null::text, rank, quota, key, epithet, 131 145 extract(epoch from knownsince)::bigint, 132 - handle ||'@'|| 133 - (select value from parsav_config 134 - where key='domain' limit 1) as xid 146 + '@' || handle, 147 + invites 135 148 from parsav_actors where origin is null 149 + order by nullif(rank,0) nulls last, handle 136 150 ]]; 137 151 }; 138 152 139 153 actor_enum = { 140 154 params = {}, sql = [[ 141 155 select a.id, a.nym, a.handle, a.origin, a.bio, 142 156 a.avataruri, a.rank, a.quota, a.key, a.epithet, 143 157 extract(epoch from a.knownsince)::bigint, 144 158 coalesce(a.handle || '@' || s.domain, 145 - '@' || a.handle) as xid 159 + '@' || a.handle) as xid, 160 + invites 146 161 from parsav_actors a 147 162 left join parsav_servers s on s.id = a.origin 163 + order by nullif(a.rank,0) nulls last, a.handle, a.origin 148 164 ]]; 149 165 }; 150 166 151 167 actor_stats = { 152 168 params = {uint64}, sql = ([[ 153 169 with tweets as ( 154 170 select from parsav_posts where author = $1::bigint ................................................................................ 786 802 end 787 803 local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor) 788 804 var a: lib.mem.ptr(lib.store.actor) 789 805 var av: rawstring, avlen: intptr 790 806 var nym: rawstring, nymlen: intptr 791 807 var bio: rawstring, biolen: intptr 792 808 var epi: rawstring, epilen: intptr 793 - if r:null(row,5) then avlen = 0 av = nil else 809 + var origin: uint64 = 0 810 + var handle = r:_string(row, 2) 811 + if not r:null(row,3) then origin = r:int(uint64,row,3) end 812 + 813 + var avia = lib.str.acc {buf=nil} 814 + if origin == 0 then 815 + avia:compose('/avi/',handle) 816 + av = avia.buf 817 + avlen = avia.sz+1 818 + elseif r:null(row,5) then 794 819 av = r:string(row,5) 795 820 avlen = r:len(row,5)+1 821 + else 822 + av = '/s/default-avatar.webp' 823 + avlen = 22 796 824 end 825 + 797 826 if r:null(row,1) then nymlen = 0 nym = nil else 798 827 nym = r:string(row,1) 799 828 nymlen = r:len(row,1)+1 800 829 end 801 830 if r:null(row,4) then biolen = 0 bio = nil else 802 831 bio = r:string(row,4) 803 832 biolen = r:len(row,4)+1 ................................................................................ 807 836 epilen = r:len(row,9)+1 808 837 end 809 838 a = [ lib.str.encapsulate(lib.store.actor, { 810 839 nym = {`nym, `nymlen}; 811 840 bio = {`bio, `biolen}; 812 841 epithet = {`epi, `epilen}; 813 842 avatar = {`av,`avlen}; 814 - handle = {`r:string(row, 2); `r:len(row,2) + 1}; 843 + handle = {`handle.ptr, `handle.ct + 1}; 815 844 xid = {`r:string(row, 11); `r:len(row,11) + 1}; 816 845 }) ] 817 846 a.ptr.id = r:int(uint64, row, 0); 818 847 a.ptr.rights = lib.store.rights_default(); 819 848 a.ptr.rights.rank = r:int(uint16, row, 6); 820 849 a.ptr.rights.quota = r:int(uint32, row, 7); 850 + a.ptr.rights.invites = r:int(uint32, row, 12); 821 851 a.ptr.knownsince = r:int(int64,row, 10); 822 852 if r:null(row,8) then 823 853 a.ptr.key.ct = 0 a.ptr.key.ptr = nil 824 854 else 825 855 a.ptr.key = r:bin(row,8) 826 856 end 827 - if r:null(row,3) then a.ptr.origin = 0 828 - else a.ptr.origin = r:int(uint64,row,3) end 857 + a.ptr.origin = origin 858 + if avia.buf ~= nil then avia:free() end 829 859 return a 830 860 end 831 861 832 862 local privmap = lib.store.privmap 833 863 834 864 local checksha = function(src, hash, origin, username, pw) 835 865 local validate = function(kind, cred, credlen) ................................................................................ 873 903 local schema = sqlsquash(lib.util.ingest('backend/schema/pgsql.sql')) 874 904 local obliterator = sqlsquash(lib.util.ingest('backend/schema/pgsql-drop.sql')) 875 905 876 906 local privupdate = terra( 877 907 src: &lib.store.source, 878 908 ac: &lib.store.actor 879 909 ): {} 880 - var pdef = lib.store.rights_default().powers 910 + var pdef: lib.store.powerset pdef:clear() 881 911 var map = array([privmap]) 882 912 for i=0, [map.type.N] do 883 913 var d = pdef and map[i].priv 884 914 var u = ac.rights.powers and map[i].priv 885 915 queries.actor_power_delete.exec(src, ac.id, map[i].name) 886 916 if d:sz() > 0 and u:sz() == 0 then 887 917 lib.dbg('blocking power ', {map[i].name.ptr, map[i].name.ct}) ................................................................................ 1042 1072 return a 1043 1073 end 1044 1074 end]; 1045 1075 1046 1076 actor_enum = [terra(src: &lib.store.source) 1047 1077 var r = queries.actor_enum.exec(src) 1048 1078 if r.sz == 0 then 1049 - return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil } 1079 + return [lib.mem.lstptr(lib.store.actor)].null() 1050 1080 else defer r:free() 1051 - var mem = lib.mem.heapa([&lib.store.actor], r.sz) 1081 + var mem = lib.mem.heapa([lib.mem.ptr(lib.store.actor)], r.sz) 1052 1082 for i=0,r.sz do 1053 - mem.ptr[i] = row_to_actor(&r, i).ptr 1054 - mem.ptr[i].source = src 1083 + mem.ptr[i] = row_to_actor(&r, i) 1084 + mem(i).ptr.source = src 1055 1085 end 1056 - return [lib.mem.ptr(&lib.store.actor)] { ct = r.sz, ptr = mem.ptr } 1086 + return [lib.mem.lstptr(lib.store.actor)] { ct = r.sz, ptr = mem.ptr } 1057 1087 end 1058 1088 end]; 1059 1089 1060 1090 actor_enum_local = [terra(src: &lib.store.source) 1061 1091 var r = queries.actor_enum_local.exec(src) 1062 1092 if r.sz == 0 then 1063 - return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil } 1093 + return [lib.mem.lstptr(lib.store.actor)].null() 1064 1094 else defer r:free() 1065 - var mem = lib.mem.heapa([&lib.store.actor], r.sz) 1095 + var mem = lib.mem.heapa([lib.mem.ptr(lib.store.actor)], r.sz) 1066 1096 for i=0,r.sz do 1067 - mem.ptr[i] = row_to_actor(&r, i).ptr 1068 - mem.ptr[i].source = src 1097 + mem.ptr[i] = row_to_actor(&r, i) 1098 + mem(i).ptr.source = src 1069 1099 end 1070 - return [lib.mem.ptr(&lib.store.actor)] { ct = r.sz, ptr = mem.ptr } 1100 + return [lib.mem.lstptr(lib.store.actor)] { ct = r.sz, ptr = mem.ptr } 1071 1101 end 1072 1102 end]; 1073 1103 1074 1104 actor_auth_how = [terra( 1075 1105 src: &lib.store.source, 1076 1106 ip: lib.store.inet, 1077 1107 username: rawstring ................................................................................ 1129 1159 1130 1160 var a = row_to_actor(&r, 0) 1131 1161 a.ptr.source = src 1132 1162 1133 1163 var au = [lib.stat(lib.store.auth)] { ok = true } 1134 1164 au.val.aid = aid 1135 1165 au.val.uid = a.ptr.id 1136 - if not r:null(0,13) then -- restricted? 1166 + if not r:null(0,14) then -- restricted? 1137 1167 au.val.privs:clear() 1138 - (au.val.privs.post << r:bool(0,14)) 1139 - (au.val.privs.edit << r:bool(0,15)) 1140 - (au.val.privs.acct << r:bool(0,16)) 1141 - (au.val.privs.upload << r:bool(0,17)) 1142 - (au.val.privs.censor << r:bool(0,18)) 1143 - (au.val.privs.admin << r:bool(0,19)) 1168 + (au.val.privs.post << r:bool(0,15)) 1169 + (au.val.privs.edit << r:bool(0,16)) 1170 + (au.val.privs.acct << r:bool(0,17)) 1171 + (au.val.privs.upload << r:bool(0,18)) 1172 + (au.val.privs.censor << r:bool(0,19)) 1173 + (au.val.privs.admin << r:bool(0,20)) 1144 1174 else au.val.privs:fill() end 1145 1175 1146 1176 return au, a 1147 1177 end 1148 1178 1149 1179 ::fail:: return [lib.stat (lib.store.auth) ] { ok = false }, 1150 1180 [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 } ................................................................................ 1219 1249 end]; 1220 1250 1221 1251 actor_powers_fetch = getpow; 1222 1252 actor_save = [terra( 1223 1253 src: &lib.store.source, 1224 1254 ac: &lib.store.actor 1225 1255 ): {} 1256 + var avatar = ac.avatar 1257 + if ac.origin == 0 then avatar = nil end 1226 1258 queries.actor_save.exec(src, 1227 1259 ac.id, ac.nym, ac.handle, 1228 - ac.bio, ac.epithet, ac.avatar, 1229 - ac.avatarid, ac.rights.rank, ac.rights.quota) 1260 + ac.bio, ac.epithet, avatar, 1261 + ac.avatarid, ac.rights.rank, ac.rights.quota, ac.rights.invites) 1230 1262 end]; 1231 1263 1232 1264 actor_save_privs = privupdate; 1233 1265 1234 1266 actor_create = [terra( 1235 1267 src: &lib.store.source, 1236 1268 ac: &lib.store.actor 1237 1269 ): uint64 1238 - var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.epithet, ac.rights.rank, ac.rights.quota) 1270 + var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.epithet, ac.rights.rank, ac.rights.quota,ac.rights.invites) 1239 1271 if r.sz == 0 then lib.bail('failed to create actor!') end 1240 1272 ac.id = r:int(uint64,0,0) 1241 1273 1242 1274 -- check against default rights, insert records for wherever powers differ 1243 1275 lib.dbg('created new actor, establishing powers') 1244 1276 privupdate(src,ac) 1245 1277 1246 1278 lib.dbg('powers established') 1247 1279 return ac.id 1248 1280 end]; 1249 1281 1282 + actor_purge_uid = [terra( 1283 + src: &lib.store.source, 1284 + uid: uint64 1285 + ) queries.actor_purge_uid.exec(src,uid) end]; 1286 + 1250 1287 auth_enum_uid = [terra( 1251 1288 src: &lib.store.source, 1252 1289 uid: uint64 1253 1290 ): lib.mem.ptr(lib.mem.ptr(lib.store.auth)) 1254 1291 var r = queries.auth_enum_uid.exec(src,uid) 1255 1292 if r.sz == 0 then return [lib.mem.ptr(lib.mem.ptr(lib.store.auth))].null() end 1256 1293 var ret = lib.mem.heapa([lib.mem.ptr(lib.store.auth)], r.sz)
Modified backend/schema/pgsql.sql from [b4d8dee98e] to [72a3e65e6e].
30 30 on delete cascade, -- null origin = local actor 31 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 + invites integer not null default 0, 37 38 key bytea, -- private if localactor; public if remote 38 39 epithet text, 39 40 authtime timestamp not null default now(), -- cookies earlier than this timepoint will not be accepted 40 41 41 42 unique (handle,origin) 42 43 ); 43 44 ................................................................................ 142 143 143 144 create table parsav_room_members ( 144 145 room bigint not null references parsav_rooms(id) on delete cascade, 145 146 member bigint not null references parsav_actors(id) on delete cascade, 146 147 rank smallint not null default 0, 147 148 admin boolean not null default false, -- non-admins with rank can only moderate + invite 148 149 title text, -- admin-granted title like reddit flair 149 - vouchedby bigint references parsav_actors(id) 150 + vouchedby bigint references parsav_actors(id) on delete set null 150 151 ); 151 152 152 153 create table parsav_invites ( 153 154 id bigint primary key default (1+random()*(2^63-1))::bigint, 154 155 -- when a user is created from an invite, the invite is deleted and the invite 155 156 -- ID becomes the user ID. privileges granted on the invite ID during the invite 156 157 -- process are thus inherited by the user
Modified config.lua from [6a4b9180fe] to [5a4f5a8d5b].
52 52 -- we should add support for content-encoding headers and pre-compress 53 53 -- the damn things before compiling (also making the binary smaller) 54 54 {'style.css', 'text/css'}; 55 55 {'live.js', 'text/javascript'}; -- rrrrrrrr 56 56 {'default-avatar.webp', 'image/webp'}; -- needs inkscape-exclusive svg features 57 57 {'padlock.svg', 'image/svg+xml'}; 58 58 {'warn.svg', 'image/svg+xml'}; 59 - {'query.svg', 'image/svg+xml'}; 59 + {'query.webp', 'image/webp'}; 60 60 }; 61 61 default_ui_accent = tonumber(default('parsav_ui_default_accent',323)); 62 62 } 63 63 if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then 64 64 conf.braingeniousmode = true -- SOUND GENERAL QUARTERS 65 65 end 66 66 if u.ping '.fslckout' or u.ping '_FOSSIL_' then
Modified makefile from [e6a5371547] to [eedbd28993].
1 1 dl = git 2 2 dbg-flags = $(if $(dbg),-g) 3 3 4 -images = static/default-avatar.webp 4 +images = static/default-avatar.webp static/query.webp 5 5 #$(addsuffix .webp, $(basename $(wildcard static/*.svg))) 6 6 styles = $(addsuffix .css, $(basename $(wildcard static/*.scss))) 7 7 8 8 parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles) 9 9 terra $(dbg-flags) $< 10 10 parsav.o parsavd.o: parsav.t config.lua pkgdata.lua $(images) $(styles) 11 11 env parsav_link=no terra $(dbg-flags) $<
Modified mem.t from [1f9397ac82] to [a0c3213659].
123 123 end 124 124 end 125 125 return t 126 126 end 127 127 128 128 m.ptr = terralib.memoize(function(ty) return mkptr(ty, true) end) 129 129 m.ref = terralib.memoize(function(ty) return mkptr(ty, false) end) 130 +m.lstptr = function(ty) return m.ptr(m.ptr(ty)) end -- make code more readable 130 131 131 132 m.vec = terralib.memoize(function(ty) 132 133 local v = terralib.types.newstruct(string.format('vec<%s>', ty.name)) 133 134 v.entries = { 134 135 {field = 'storage', type = m.ptr(ty)}; 135 136 {field = 'sz', type = intptr}; 136 137 {field = 'run', type = intptr};
Modified mgtool.t from [39281cf1cf] to [ee3dfa15a8].
21 21 22 22 local ctlcmds = { 23 23 { 'start', 'start a new instance of the server' }; 24 24 { 'stop', 'stop a running instance' }; 25 25 { 'ls', 'list all running instances' }; 26 26 { 'attach', 'capture log output from a running instance' }; 27 27 { 'db', 'set up and manage the database' }; 28 - { 'user', 'manage users, privileges, and credentials'}; 28 + { 'user', 'create and manage users, privileges, and credentials'}; 29 + { 'actor', 'manage and purge actors, epithets, and ranks'}; 29 30 { 'mkroot <handle>', 'establish a new root user with the given handle' }; 30 - { 'actor <xid> purge-all', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' }; 31 - { 'actor <xid> create', 'instantiate a new actor' }; 32 - { 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' }; 33 31 { 'conf', 'manage the server configuration'}; 32 + { 'grow <count> [<acl>]', 'grant a new round of invites to all users, or those who match the given ACL' }; 34 33 { 'serv dl', 'initiate an update cycle over foreign actors' }; 35 34 { 'tl', 'print the current local timeline to standard out' }; 36 35 { 'be pgsql setup-auth (managed|unmanaged)', '(PGSQL backends) select the authentication strategy to use' }; 37 36 } 38 37 39 38 local cmdhelp = function(tbl) 40 39 local str = '\ncommands:\n' ................................................................................ 130 129 if acks(i).success then 131 130 lib.report('instance #',num,' reports successful ',rep) 132 131 else 133 132 lib.report('instance #',num,' reports failed ',rep) 134 133 end 135 134 end 136 135 end 136 + 137 +local terra gen_cfstr(cfmstr: rawstring, seed: intptr) 138 + var confirmstrs = array( 139 + 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa', 140 + 'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst' 141 + ) 142 + var tdx = lib.osclock.time(nil) / 60 143 + cfmstr[0] = 0 144 + for i=0,3 do 145 + if i ~= 0 then lib.str.cat(cfmstr, '-') end 146 + lib.str.cat(cfmstr, confirmstrs[(seed ^ tdx ^ (173*i)) % [confirmstrs.type.N]]) 147 + end 148 +end 137 149 138 150 local emp = lib.ipc.global_emperor 139 151 local terra entry_mgtool(argc: int, argv: &rawstring): int 140 152 if argc < 1 then lib.bail('bad invocation!') end 141 153 142 154 lib.noise.init(2) 143 155 [lib.init] ................................................................................ 272 284 return 1 273 285 end 274 286 if dbmode.arglist.ct < 1 then goto cmderr end 275 287 276 288 srv:setup(cnf) 277 289 if lib.str.cmp(dbmode.arglist(0),'init') == 0 and dbmode.arglist.ct == 2 then 278 290 lib.report('initializing new database structure for domain ', dbmode.arglist(1)) 291 + dlg:tx_enter() 279 292 if dlg:dbsetup() then 280 293 srv:conprep(lib.store.prepmode.conf) 281 294 dlg:conf_set('instance-name', dbmode.arglist(1)) 295 + dlg:conf_set('domain', dbmode.arglist(1)) 282 296 do var sec: int8[65] gensec(&sec[0]) 297 + dlg:conf_set('server-secret', &sec[0]) 283 298 dlg:conf_set('server-secret', &sec[0]) 284 299 end 285 300 lib.report('database setup complete; use mkroot to create an administrative user') 286 301 else lib.bail('initialization process interrupted') end 302 + dlg:tx_complete() 287 303 elseif lib.str.cmp(dbmode.arglist(0),'obliterate') == 0 then 288 - var confirmstrs = array( 289 - 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa', 290 - 'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst' 291 - ) 292 - var cfmstr: int8[64] cfmstr[0] = 0 293 - var tdx = lib.osclock.time(nil) / 60 294 - for i=0,3 do 295 - if i ~= 0 then lib.str.cat(&cfmstr[0], '-') end 296 - lib.str.cat(&cfmstr[0], confirmstrs[(tdx ^ (173*i)) % [confirmstrs.type.N]]) 297 - end 304 + var cfmstr: int8[64] gen_cfstr(&cfmstr[0],0) 298 305 299 306 if dbmode.arglist.ct == 1 then 300 307 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]) 301 308 elseif dbmode.arglist.ct == 2 then 302 309 if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then 303 310 lib.warn('completely obliterating all data!') 304 311 dlg:obliterate_everything() ................................................................................ 392 399 dlg:conf_set('master',root.handle) 393 400 lib.report('created new administrator') 394 401 if mg then 395 402 var tmppw: int8[33] 396 403 pwset(dlg, &tmppw, ruid, false) 397 404 lib.report('temporary root pw: ', {&tmppw[0], 32}) 398 405 end 406 + else goto cmderr end 407 + elseif lib.str.cmp(mode.arglist(0),'actor') == 0 then 408 + var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0)) 409 + if umode.help then 410 + [ lib.emit(false, 1, 'usage: ', `argv[0], ' actor ', umode.type.helptxt.flags, ' <xid> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp { 411 + { 'actor <xid> rank <value>', 'set an actor\'s rank to <value> (remote actors cannot exercise rank-related powers, but benefit from rank immunities)' }; 412 + { 'actor <xid> degrade', 'alias for `actor <xid> rank 0`' }; 413 + { 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' }; 414 + { 'actor <xid> instantiate', 'instantiate a remote actor, retrieving their profile and posts even if no one follows them' }; 415 + { 'actor <xid> proscribe', 'globally ban an actor from interacting with your server' }; 416 + { 'actor <xid> rehabilitate', 'lift a proscription on an actor' }; 417 + { 'actor <xid> purge-all <confirm-str>', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' }; 418 + }) ] 419 + return 1 420 + end 421 + if umode.arglist.ct >= 2 then 422 + var degrade = lib.str.cmp(umode.arglist(1),'degrade') == 0 423 + var xid = umode.arglist(0) 424 + var usr = dlg:actor_fetch_xid(pstr {ptr=xid, ct=lib.str.sz(xid)}) 425 + if not usr then lib.bail('no such actor') end 426 + if degrade or lib.str.cmp(umode.arglist(1),'rank') == 0 then 427 + var rank: uint16 428 + if degrade and umode.arglist.ct == 2 then 429 + rank = 0 430 + elseif (not degrade) and umode.arglist.ct == 3 then 431 + var r, ok = lib.math.decparse(pstr { 432 + ptr = umode.arglist(2); 433 + ct = lib.str.sz(umode.arglist(2)); 434 + }) 435 + if not ok then goto cmderr end 436 + rank = r 437 + else goto cmderr end 438 + usr.ptr.rights.rank = rank 439 + dlg:actor_save(usr.ptr) 440 + lib.report('set user rank') 441 + elseif umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(1),'bestow') == 0 then 442 + if umode.arglist(2)[0] == 0 443 + then usr.ptr.epithet = nil 444 + else usr.ptr.epithet = umode.arglist(2) 445 + end 446 + dlg:actor_save(usr.ptr) 447 + lib.report('bestowed a new epithet on ', usr.ptr.xid) 448 + elseif lib.str.cmp(umode.arglist(1),'purge-all') == 0 then 449 + var cfmstr: int8[64] gen_cfstr(&cfmstr[0],usr.ptr.id) 450 + if umode.arglist.ct == 2 then 451 + lib.bail('you are attempting to completely purge the actor ', usr.ptr.xid, ' and all related content from the database! if you really want to do this, pass the confirmation string ', &cfmstr[0]) 452 + elseif umode.arglist.ct == 3 then 453 + if lib.str.ncmp(&cfmstr[0],umode.arglist(2),64) ~= 0 then 454 + lib.bail('you have supplied an invalid confirmation string; if you really want to purge this actor, pass ', &cfmstr[0]) 455 + end 456 + lib.warn('completely purging actor ', usr.ptr.xid, ' and all related content from database') 457 + dlg:actor_purge_uid(usr.ptr.id) 458 + lib.report('actor purged') 459 + else goto cmderr end 460 + else goto cmderr end 399 461 else goto cmderr end 400 462 elseif lib.str.cmp(mode.arglist(0),'user') == 0 then 401 463 var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0)) 402 464 if umode.help then 403 465 [ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' <handle> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp { 466 + { 'user <handle> create', 'add a new user' }; 404 467 { 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' }; 405 468 { 'user <handle> auth <type> reset', '(where applicable, managed auth only) delete all of a user\'s authentication tokens of the given type and issue a new one' }; 406 469 { 'user <handle> auth (<type>|all) purge', 'delete all credentials that would allow this user to log in (where possible)' }; 407 470 { 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' }; 408 - { 'user <handle> emasculate', 'strip all administrative powers from a user' }; 471 + { 'user <handle> emasculate', 'strip all administrative powers and rank from a user' }; 409 472 { 'user <handle> forgive', 'restore all default powers to a user' }; 410 473 { 'user <handle> suspend [<timespec>]', '(e.g. \27[1muser jokester suspend 5d 6h 7m 3s\27[m to suspend "jokester" for five days, six hours, seven minutes, and three seconds) suspend a user'}; 411 474 }) ] 412 475 return 1 413 476 end 414 - if umode.arglist.ct >= 3 then 477 + var handle = umode.arglist(0) 478 + var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)}) 479 + if umode.arglist.ct == 2 and lib.str.cmp(umode.arglist(1),'create')==0 then 480 + if usr:ref() then lib.bail('that user already exists') end 481 + if not lib.store.actor.handle_validate(handle) then 482 + lib.bail('invalid user handle') end 483 + var kbuf: uint8[lib.crypt.const.maxdersz] 484 + var na = lib.store.actor.mk(&kbuf[0]) 485 + na.handle = handle 486 + dlg:actor_create(&na) 487 + lib.report('created new user @',na.handle,'; assign credentials to enable login') 488 + elseif umode.arglist.ct >= 3 then 415 489 var grant = lib.str.cmp(umode.arglist(1),'grant') == 0 416 - var handle = umode.arglist(0) 417 - var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)}) 490 + if not usr then lib.bail('no such user') end 418 491 if grant or lib.str.cmp(umode.arglist(1),'revoke') == 0 then 419 - if not usr then lib.bail('unknown handle') end 420 492 var newprivs = usr.ptr.rights.powers 421 493 var map = array([lib.store.privmap]) 422 494 if umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(2),'all') == 0 then 423 495 if grant 424 496 then newprivs:fill() 425 497 else newprivs:clear() 426 498 end
Modified parsav.t from [4b4b2876e4] to [2bbe093dad].
394 394 'http', 'html', 'session', 'tpl', 'store', 'acl'; 395 395 396 396 'smackdown'; -- md-alike parser 397 397 } 398 398 399 399 local be = {} 400 400 for _, b in pairs(config.backends) do 401 - be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')() 401 + be[#be+1] = terralib.loadfile(string.format('backend/%s.t',b))() 402 402 end 403 403 lib.store.backends = global(`array([be])) 404 404 405 405 lib.cmdparse = terralib.loadfile('cmdparse.t')() 406 406 407 407 do local collate = function(path,f, ...) 408 408 return loadfile(path..'/'..f..'.lua')(path, ...)
Modified render/conf.t from [fa178f73b5] to [60d6b764a8].
11 11 {url = 'qnt', title = 'quarantine', render = 'quarantine'}; 12 12 {url = 'acl', title = 'access control shortcuts', render = 'acl'}; 13 13 {url = 'rooms', title = 'chatrooms', render = 'rooms'}; 14 14 {url = 'circles', title = 'circles', render = 'circles'}; 15 15 16 16 {url = 'srv', title = 'server settings', render = 'srv'}; 17 17 {url = 'brand', title = 'instance branding', render = 'rebrand'}; 18 + {url = 'badge', title = 'user badges', render = 'badge'}; 19 + {url = 'emoji', title = 'custom emoji packs', render = 'emojo'}; 18 20 {url = 'censor', title = 'censorship & badthink suppression', render = 'rebrand'}; 19 21 {url = 'users', title = 'user accounting', render = 'users'}; 20 22 21 23 } 22 24 23 25 local path = symbol(lib.mem.ptr(pref)) 24 26 local co = symbol(&lib.srv.convo) ................................................................................ 41 43 42 44 local terra 43 45 render_conf([co], [path], notify: pstr) 44 46 var menu: lib.str.acc menu:init(64):lpush('<hr>') defer menu:free() 45 47 46 48 -- build menu 47 49 do var p = co.who.rights.powers 48 - if p.config() then menu:lpush '<a href="/conf/srv">server settings</a>' end 49 - if p.rebrand() then menu:lpush '<a href="/conf/brand">instance branding</a>' end 50 + if p:affect_users() then menu:lpush '<a href="/conf/users">users</a>' end 50 51 if p.censor() then menu:lpush '<a href="/conf/censor">badthink alerts</a>' end 51 - if p:affect_users() then menu:lpush '<a href="/conf/users">users</a>' end 52 + if p.config() then menu:lpush([ 53 + '<a href="/conf/srv">server & policy</a>' .. 54 + '<a href="/conf/badge">badges</a>' .. 55 + '<a href="/conf/emoji">emoji packs</a>' 56 + ]) end 57 + if p.rebrand() then menu:lpush '<a href="/conf/brand">instance branding</a>' end 52 58 end 53 59 54 60 -- select the appropriate panel 55 61 var [panel] = pstr { ptr = ''; ct = 0 } 56 62 if path.ct >= 2 then [invoker] end 57 63 58 64 -- avoid the hr if we didn't add any elements
Modified render/conf/users.t from [6e4ba75dd2] to [4494d99300].
1 1 -- vim: ft=terra 2 2 local pstr = lib.mem.ptr(int8) 3 3 local pref = lib.mem.ref(int8) 4 +local P = lib.str.plit 4 5 5 6 local terra cs(s: rawstring) 6 7 return pstr { ptr = s, ct = lib.str.sz(s) } 7 8 end 8 9 9 10 local terra 11 +regalia(acc: &lib.str.acc, rank: uint16) 12 + switch rank do -- TODO customizability 13 + case [uint16](1) then acc:lpush('👑') end 14 + case [uint16](2) then acc:lpush('🔱') end 15 + case [uint16](3) then acc:lpush('⚜️') end 16 + case [uint16](4) then acc:lpush('🗡') end 17 + case [uint16](5) then acc:lpush('🗝') end 18 + else acc:lpush('🕴') 19 + end 20 +end 21 + 22 +local num_field = macro(function(acc,name,lbl,min,max,value) 23 + name = name:asvalue() 24 + lbl = lbl:asvalue() 25 + return quote 26 + var decbuf: int8[21] 27 + in acc:lpush([string.format('<div class="elem small"><label for="%s">%s</label><input type="number" id="%s" name="%s" min="', name, lbl, name, name)]) 28 + :push(lib.math.decstr(min, &decbuf[20]),0) 29 + :lpush('" max="'):push(lib.math.decstr(max, &decbuf[20]),0) 30 + :lpush('" value="'):push(lib.math.decstr(value, &decbuf[20]),0):lpush('"></div>') 31 + end 32 +end) 33 + 34 +local terra 35 +push_checkbox(acc: &lib.str.acc, name: pstr, lbl: pstr, on: bool, enabled: bool) 36 + acc:lpush('<label><input type="checkbox" name="'):ppush(name):lpush('"') 37 + if on then acc:lpush(' checked') end 38 + if not enabled then acc:lpush(' disabled') end 39 + acc:lpush('> '):ppush(lbl):lpush('</label>') 40 +end 41 + 42 +local mode_local, mode_remote, mode_staff, mode_peers, mode_peons, mode_all = 0,1,2,3,4,5 43 +local terra 10 44 render_conf_users(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr 11 - if path.ct == 2 then 12 - var uid, ok = lib.math.shorthand.parse(path(1).ptr,path(1).ct) 45 + if path.ct == 3 then 46 + var uid, ok = lib.math.shorthand.parse(path(2).ptr,path(2).ct) 47 + if not ok then goto e404 end 13 48 var user = co.srv:actor_fetch_uid(uid) 49 + -- FIXME allow xids as well, for manual queries 14 50 if not user then goto e404 end 51 + defer user:free() 52 + if not co.who:overpowers(user.ptr) then goto e403 end 53 + 15 54 var islinkct = false 16 - var cinp: lib.str.acc 55 + var cinp: lib.str.acc cinp:init(128) 17 56 var clnk: lib.str.acc clnk:compose('<hr>') 57 + cinp:lpush('<div class="elem-group">') 58 + if co.who.rights.powers.herald() then 59 + var sanitized: pstr 60 + if user.ptr.epithet == nil 61 + then sanitized = pstr {ptr='', ct=0} 62 + else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true) 63 + end 64 + cinp:lpush('<div class="elem"><label for="epithet">epithet</label><input type="text" id="epithet" name="epithet" value="'):ppush(sanitized):lpush('"></div>') 65 + if user.ptr.epithet ~= nil then sanitized:free() end 66 + end 67 + if user.ptr.rights.rank > 0 and (co.who.rights.powers.elevate() or co.who.rights.powers.demote()) then 68 + var max = co.who.rights.rank 69 + if not co.who.rights.powers.elevate() then max = user.ptr.rights.rank end 70 + var min = co.srv.cfg.nranks 71 + if not co.who.rights.powers.demote() then min = user.ptr.rights.rank end 72 + 73 + num_field(cinp, 'rank', 'rank', max, min, user.ptr.rights.rank) 74 + end 75 + if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then 76 + var min = 0 77 + if not (co.who.rights.powers.discipline() or 78 + co.who.rights.powers.demote() and co.who.rights.powers.invite()) 79 + then min = user.ptr.rights.invites end 80 + var max = co.srv.cfg.maxinvites 81 + if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end 82 + 83 + num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites) 84 + end 85 + cinp:lpush('</div><div class="check-panel">') 86 + 87 + if (user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or 88 + (user.ptr.rights.rank > 0 and co.who.rights.powers.demote()) then 89 + push_checkbox(&cinp, 'staff', 'site staff member', user.ptr.rights.rank > 0, true) 90 + end 91 + 92 + cinp:lpush('</div>') 93 + 94 + if co.who.rights.powers.elevate() or 95 + co.who.rights.powers.demote() then 96 + var map = array([lib.store.privmap]) 97 + cinp:lpush('<label>powers</label><div class="check-panel">') 98 + for i=0, [map.type.N] do 99 + if (co.who.rights.powers and map[i].priv) == map[i].priv then 100 + var name: int8[64] 101 + var on = (user.ptr.rights.powers and map[i].priv) == map[i].priv 102 + var enabled = (on and co.who.rights.powers.demote()) or 103 + ((not on) and co.who.rights.powers.elevate()) 104 + lib.str.cpy(&name[0], 'allow-') 105 + lib.str.ncpy(&name[6], map[i].name.ptr, map[i].name.ct) 106 + push_checkbox(&cinp, pstr{ptr=&name[0],ct=map[i].name.ct+6}, 107 + map[i].name, on, enabled) 108 + end 109 + end 110 + cinp:lpush('</div>') 111 + end 112 + 113 + -- TODO black mark system? e.g. resolution option for badthink reports 114 + -- adds a black mark to the offending user; they can be automatically banned 115 + -- or brought up for review after a certain number of offenses; possibly lower 116 + -- set of default privs for marked users 18 117 19 118 var cinpp = cinp:finalize() defer cinpp:free() 20 119 var clnkp: pstr 21 120 if islinkct then clnkp = clnk:finalize() else 22 121 clnk:free() 23 122 clnkp = pstr { ptr='', ct=0 } 24 123 end 124 + var unym: lib.str.acc unym:init(64) 125 + unym:lpush('<a href="/') 126 + if user(0).origin ~= 0 then unym:lpush('@') end 127 + do var sanxid = lib.html.sanitize(user(0).xid, true) 128 + unym:ppush(sanxid) 129 + sanxid:free() end 130 + unym:lpush('" class="id">') 131 + lib.render.nym(user.ptr,0,&unym) 132 + unym:lpush('</a>') 25 133 var pg = data.view.conf_user_ctl { 26 - name = cs(user(0).handle); 134 + name = unym:finalize(); 27 135 inputcontent = cinpp; 28 136 linkcontent = clnkp; 29 137 } 30 138 var ret = pg:tostr() 139 + pg.name:free() 31 140 if islinkct then clnkp:free() end 32 141 return ret 33 142 else 34 - 143 + var modes = array(P'local', P'remote', P'staff', P'titled', P'peons', P'all') 144 + var idbuf: int8[lib.math.shorthand.maxlen] 145 + var ulst: lib.str.acc ulst:init(256) 146 + var mode: uint8 = mode_local 147 + var modestr = co:pgetv('show') 148 + ulst:lpush('<div style="text-align: right"><em>showing ') 149 + for i=0,[modes.type.N] do 150 + if modestr:ref() and modes[i]:cmp(modestr) then mode = i end 151 + end 152 + for i=0,[modes.type.N] do 153 + if i > 0 then ulst:lpush(' · ') end 154 + if mode == i then 155 + ulst:lpush('<strong>'):ppush(modes[i]):lpush('</strong>') 156 + else 157 + ulst:lpush('<a href="?show='):ppush(modes[i]):lpush('">') 158 + :ppush(modes[i]):lpush('</a>') 159 + end 160 + end 161 + var users: lib.mem.lstptr(lib.store.actor) 162 + if mode == mode_local then 163 + users = co.srv:actor_enum_local() 164 + else 165 + users = co.srv:actor_enum() 166 + end 167 + ulst:lpush('</em></div>') 168 + ulst:lpush('<ul class="user-list">') 169 + for i=0,users.ct do var usr = users(i).ptr 170 + if mode == mode_staff and usr.rights.rank == 0 then goto skip 171 + elseif mode == mode_peons and usr.rights.rank ~= 0 then goto skip 172 + elseif mode == mode_remote and usr.origin == 0 then goto skip 173 + elseif mode == mode_peers and usr.epithet == nil then goto skip end 174 + var idlen = lib.math.shorthand.gen(usr.id, &idbuf[0]) 175 + ulst:lpush('<li>') 176 + if usr.rights.rank ~= 0 then 177 + ulst:lpush('<span class="regalia">') 178 + regalia(&ulst, usr.rights.rank) 179 + ulst:lpush('</span>') 180 + end 181 + if co.who:overpowers(usr) then 182 + ulst:lpush('<a class="id" href="users/'):push(&idbuf[0],idlen):lpush('">') 183 + lib.render.nym(usr, 0, &ulst) 184 + ulst:lpush('</a></li>') 185 + else 186 + ulst:lpush('<span class="id">') 187 + lib.render.nym(usr, 0, &ulst) 188 + ulst:lpush('</span></li>') 189 + end 190 + ::skip::end 191 + ulst:lpush('</ul>') 192 + return ulst:finalize() 35 193 end 36 194 do return pstr.null() end 37 - ::e404:: co:complain(404, 'not found', 'there is no user or resource by that identifier on this server') 195 + ::e404:: co:complain(404, 'not found', 'there is no user or resource by that identifier on this server') goto quit 196 + ::e403:: co:complain(403, 'forbidden', 'you do not have sufficient authority to control that resource') 38 197 39 - do return pstr.null() end 198 + ::quit:: return pstr.null() 40 199 end 41 200 42 201 return render_conf_users
Modified render/nym.t from [0d2437aadd] to [74775ce158].
1 1 -- vim: ft=terra 2 -local pstr = lib.mem.ptr(int8) 2 +local pstr = lib.str.t 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 -render_nym(who: &lib.store.actor, scope: uint64) 9 - var n: lib.str.acc n:init(128) 8 +render_nym(who: &lib.store.actor, scope: uint64, tgt: &lib.str.acc) 9 + var acc: lib.str.acc 10 + var n: &lib.str.acc 11 + if tgt ~= nil then n = tgt else 12 + n = &acc 13 + n:init(128) 14 + end 10 15 var xidsan = lib.html.sanitize(cs(who.xid),false) 11 16 if who.nym ~= nil and who.nym[0] ~= 0 then 12 17 var nymsan = lib.html.sanitize(cs(who.nym),false) 13 - n:compose('<span class="nym">',nymsan,'</span> [<span class="handle">', 14 - xidsan,'</span>]') 18 + n:lpush('<span class="nym">'):ppush(nymsan) 19 + :lpush('</span> [<span class="handle">'):ppush(xidsan) 20 + :lpush('</span>]') 15 21 nymsan:free() 16 - else n:compose('<span class="handle">',xidsan,'</span>') end 22 + else n:lpush('<span class="handle">'):ppush(xidsan):lpush('</span>') end 17 23 xidsan:free() 18 24 19 25 if who.epithet ~= nil then 20 26 var episan = lib.html.sanitize(cs(who.epithet),false) 21 - n:lpush(' <span class="epithet">'):ppush(episan):lpush('</span>') 27 + n:lpush('<span class="epithet">'):ppush(episan):lpush('</span>') 22 28 episan:free() 23 29 end 24 30 25 31 -- TODO: if scope == chat room then lookup titles in room member db 26 - return n:finalize() 32 + if tgt == nil then 33 + return n:finalize() 34 + else return pstr.null() end 27 35 end 28 36 29 37 return render_nym
Modified render/profile.t from [5ac1497f7a] to [5d5ed1c86e].
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 + var followed = false -- FIXME 11 11 if co.aid ~= 0 and co.who.id == actor.id then 12 - aux:compose('<a class="button" href="/conf/profile?go=/',actor.xid,'">alter</a>') 12 + aux:compose('<a class="button" href="/conf/profile?go=/@',actor.handle,'">alter</a>') 13 13 elseif co.aid ~= 0 then 14 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>') 15 + aux:compose('<button method="post" class="pos" name="act" value="follow">follow</button>') 16 + elseif followed then 17 + aux:compose('<button method="post" class="neg" name="act" value="unfollow">unfollow</button>') 18 18 end 19 - aux:lpush('<a href="/'):push(actor.xid,0):lpush('/chat">chat</a>') 20 - if co.who.rights.powers:affect_users() then 19 + aux:lpush('<a class="button" href="/'):push(actor.xid,0):lpush('/chat">chat</a>') 20 + if co.who.rights.powers:affect_users() and co.who:overpowers(actor) then 21 21 aux:lpush('<a class="button" href="/'):push(actor.xid,0):lpush('/ctl">control</a>') 22 22 end 23 23 else 24 24 aux:compose('<a class="button" href="/', actor.xid, '/follow">remote follow</a>') 25 25 end 26 26 var auxp = aux:finalize() 27 - var avistr: lib.str.acc if actor.origin == 0 then 28 - avistr:compose('/avi/',actor.handle) 29 - end 30 27 var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, ×tr[0]) 31 28 32 29 var strfbuf: int8[28*4] 33 30 var stats = co.srv:actor_stats(actor.id) 34 31 var sn_posts = cs(lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ])) 35 32 var sn_follows = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1)) 36 33 var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1)) 37 34 var sn_mutuals = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1)) 38 35 var bio = lib.str.plit "<em>tall, dark, and mysterious</em>" 39 36 if actor.bio ~= nil then 40 37 bio = lib.smackdown.html(cs(actor.bio)) 41 38 end 42 - var fullname = lib.render.nym(actor,0) defer fullname:free() 39 + var fullname = lib.render.nym(actor,0,nil) defer fullname:free() 40 + var comments: lib.str.acc comments:init(64) 41 + -- this is really more what epithets are for, i think 42 + --if actor.rights.rank > 0 then comments:lpush('<li>staff member</li>') end 43 + if co.aid ~= 0 and actor.rights.rank ~= 0 then 44 + if co.who:outranks(actor) then 45 + comments:lpush('<li style="--co:50">underling</li>') 46 + elseif actor:outranks(co.who) then 47 + comments:lpush('<li style="--co:-50">outranks you</li>') 48 + end 49 + end 50 + 43 51 var profile = data.view.profile { 44 52 nym = fullname; 45 53 bio = bio; 46 54 xid = cs(actor.xid); 47 - avatar = lib.trn(actor.origin == 0, pstr{ptr=avistr.buf,ct=avistr.sz}, 48 - cs(lib.coalesce(actor.avatar, '/s/default-avatar.svg'))); 55 + avatar = cs(actor.avatar); 49 56 50 57 nposts = sn_posts, nfollows = sn_follows; 51 58 nfollowers = sn_followers, nmutuals = sn_mutuals; 52 59 tweetday = cs(timestr); 53 60 timephrase = lib.trn(actor.origin == 0, lib.str.plit'joined', lib.str.plit'known since'); 61 + 62 + remarks = ''; 54 63 55 64 auxbtn = auxp; 56 65 } 66 + if comments.sz > 0 then profile.remarks = comments:finalize() end 57 67 58 68 var ret = profile:tostr() 59 - if actor.origin == 0 then avistr:free() end 60 69 auxp:free() 61 70 if actor.bio ~= nil then bio:free() end 71 + if comments.sz > 0 then profile.remarks:free() end 62 72 return ret 63 73 end 64 74 65 75 return render_profile
Modified render/tweet.t from [2b64155fcc] to [43aca48007].
22 22 var timestr: int8[26] lib.osclock.ctime_r(&p.posted, ×tr[0]) 23 23 24 24 var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) defer bhtml:free() 25 25 26 26 var idbuf: int8[lib.math.shorthand.maxlen] 27 27 var idlen = lib.math.shorthand.gen(p.id, idbuf) 28 28 var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen}) 29 - var fullname = lib.render.nym(author,0) defer fullname:free() 29 + var fullname = lib.render.nym(author,0,nil) defer fullname:free() 30 30 var tpl = data.view.tweet { 31 31 text = bhtml; 32 32 subject = cs(lib.coalesce(p.subject,'')); 33 33 nym = fullname; 34 34 when = cs(×tr[0]); 35 - avatar = cs(lib.trn(author.origin == 0, avistr.buf, 36 - lib.coalesce(author.avatar, '/s/default-avatar.svg'))); 35 + avatar = cs(author.avatar); 37 36 acctlink = cs(author.xid); 38 37 permalink = permalink:finalize(); 39 38 attr = '' 40 39 } 41 40 42 41 var attrbuf: int8[32] 43 42 if p.accent ~= -1 and p.accent ~= co.ui_hue then
Modified route.t from [35b3cb0b8a] to [2f7668c3df].
245 245 246 246 ::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end 247 247 ::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end 248 248 end 249 249 250 250 terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t) 251 251 var msg = pstring.null() 252 + -- first things first, do priv checks 253 + if path.ct >= 1 then 254 + if not co.who.rights.powers.config() and ( 255 + path(1):cmp(lib.str.lit 'srv') or 256 + path(1):cmp(lib.str.lit 'badge') or 257 + path(1):cmp(lib.str.lit 'emoji') 258 + ) then goto nopriv 259 + 260 + elseif not co.who.rights.powers.rebrand() and ( 261 + path(1):cmp(lib.str.lit 'brand') 262 + ) then goto nopriv 263 + 264 + elseif not co.who.rights.powers.acct() and ( 265 + path(1):cmp(lib.str.lit 'profile') or 266 + path(1):cmp(lib.str.lit 'acct') 267 + ) then goto nopriv 268 + 269 + elseif not co.who.rights.powers:affect_users() and ( 270 + path(1):cmp(lib.str.lit 'users') 271 + ) then goto nopriv end 272 + end 273 + 252 274 if meth == method.post and path.ct >= 1 then 253 275 var user_refresh = false var fail = false 254 276 if path(1):cmp(lib.str.lit 'profile') then 255 277 lib.dbg('updating profile') 256 278 co.who.bio = co:postv('bio')._0 257 279 co.who.nym = co:postv('nym')._0 258 280 if co.who.bio ~= nil and @co.who.bio == 0 then co.who.bio = nil end ................................................................................ 279 301 if resethue then 280 302 co.srv:actor_conf_int_reset(co.who.id, 'ui-accent') 281 303 co.ui_hue = co.srv.cfg.ui_hue 282 304 end 283 305 284 306 msg = lib.str.plit 'profile changes saved' 285 307 --user_refresh = true -- not really necessary here, actually 286 - elseif path(1):cmp(lib.str.lit 'srv') then 287 - if not co.who.rights.powers.config() then goto nopriv end 288 - elseif path(1):cmp(lib.str.lit 'brand') then 289 - if not co.who.rights.powers.rebrand() then goto nopriv end 290 - elseif path(1):cmp(lib.str.lit 'users') then 291 - if not co.who.rights.powers:affect_users() then goto nopriv end 292 308 293 309 elseif path(1):cmp(lib.str.lit 'sec') then 294 310 var act = co:ppostv('act') 295 311 if act:cmp(lib.str.plit 'invalidate') then 296 312 lib.dbg('setting user\'s cookie validation time to now') 297 313 co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil)) 298 314 -- 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 299 315 co:installkey('/conf/sec',co.aid) 300 316 return 317 + end 318 + elseif path(1):cmp(lib.str.lit 'users') and path.ct >= 2 then 319 + var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct) 320 + if ok then 321 + var usr = co.srv:actor_fetch_uid(userid) defer usr:free() 322 + if not co.who:overpowers(usr.ptr) then goto nopriv end 301 323 end 302 324 end 303 325 304 326 if user_refresh then -- refresh the user info for the renderer 305 327 var usr = co.srv:actor_fetch_uid(co.who.id) 306 328 lib.mem.heapf(co.who) 307 329 co.who = usr.ptr
Modified srv.t from [b092edff32] to [6be667433b].
8 8 pol_sec: secmode.t 9 9 pol_reg: bool 10 10 credmgd: bool 11 11 maxupsz: intptr 12 12 instance: lib.mem.ptr(int8) 13 13 overlord: &srv 14 14 ui_hue: uint16 15 + nranks: uint16 16 + maxinvites: uint16 15 17 } 16 18 local struct srv { 17 19 sources: lib.mem.ptr(lib.store.source) 18 20 webmgr: lib.net.mg_mgr 19 21 webcon: &lib.net.mg_connection 20 22 cfg: cfgcache 21 23 id: rawstring ................................................................................ 644 646 self.sources(i).backend.actor_auth_pw ~= nil then 645 647 var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw) 646 648 if aid ~= 0 then 647 649 if uid == 0 then 648 650 lib.dbg('new user just logged in, creating account entry') 649 651 var kbuf: uint8[lib.crypt.const.maxdersz] 650 652 var na = lib.store.actor.mk(&kbuf[0]) 653 + na.handle = newhnd.ptr 651 654 var newuid: uint64 652 655 if self.sources(i).backend.actor_create ~= nil then 653 656 newuid = self.sources(i):actor_create(&na) 654 657 else newuid = self:actor_create(&na) end 655 658 656 659 if self.sources(i).backend.actor_auth_register_uid ~= nil then 657 660 self.sources(i):actor_auth_register_uid(aid,newuid) ................................................................................ 723 726 lib.net.mg_mgr_free(&self.webmgr) 724 727 for i=0,self.sources.ct do var src = self.sources.ptr + i 725 728 lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')') 726 729 src:close() 727 730 end 728 731 self.sources:free() 729 732 end 733 + 734 +terra cfgcache:cfint(name: rawstring, default: intptr) 735 + var str = self.overlord:conf_get(name) 736 + if str.ptr ~= nil then 737 + var i,ok = lib.math.decparse(str) 738 + if ok then default = i else 739 + lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected integer; using default value instead') 740 + end 741 + str:free() 742 + end 743 + return default 744 +end 745 + 746 +terra cfgcache:cfbool(name: rawstring, default: bool) 747 + var str = self.overlord:conf_get(name) 748 + if str.ptr ~= nil then 749 + if str:cmp(lib.str.plit 'true') or str:cmp(lib.str.plit 'on') or 750 + str:cmp(lib.str.plit 'yes') or str:cmp(lib.str.plit '1') then 751 + default = true 752 + elseif str:cmp(lib.str.plit 'false') or str:cmp(lib.str.plit 'off') or 753 + str:cmp(lib.str.plit 'no') or str:cmp(lib.str.plit '0') then 754 + default = false 755 + else 756 + lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected boolean; using default value instead') 757 + end 758 + str:free() 759 + end 760 + return default 761 +end 730 762 731 763 terra cfgcache:load() 732 764 self.instance = self.overlord:conf_get('instance-name') 733 765 self.secret = self.overlord:conf_get('server-secret') 734 766 735 - do self.pol_reg = false 736 - var sreg = self.overlord:conf_get('policy-self-register') 737 - if sreg:ref() then 738 - if lib.str.cmp(sreg.ptr, 'on') == 0 739 - then self.pol_reg = true 740 - else self.pol_reg = false 741 - end 742 - sreg:free() 743 - end end 767 + self.pol_reg = self:cfbool('policy-self-register', false) 744 768 745 769 do self.credmgd = false 746 770 var sreg = self.overlord:conf_get('credential-store') 747 771 if sreg:ref() then 748 772 if lib.str.cmp(sreg.ptr, 'managed') == 0 749 773 then self.credmgd = true 750 774 else self.credmgd = false ................................................................................ 773 797 self.pol_sec = secmode.lockdown 774 798 elseif lib.str.cmp(smode.ptr, 'isolate') == 0 then 775 799 self.pol_sec = secmode.isolate 776 800 end 777 801 smode:free() 778 802 end 779 803 780 - self.ui_hue = config.default_ui_accent 781 - var shue = self.overlord:conf_get('ui-accent') 782 - if shue.ptr ~= nil then 783 - var hue,ok = lib.math.decparse(shue) 784 - if ok then self.ui_hue = hue end 785 - shue:free() 786 - end 804 + self.ui_hue = self:cfint('ui-accent',config.default_ui_accent) 805 + self.nranks = self:cfint('user-ranks',10) 806 + self.maxinvites = self:cfint('max-invites',64) 787 807 end 788 808 789 809 return { 790 810 overlord = srv; 791 811 convo = convo; 792 812 route = route; 793 813 secmode = secmode; 794 814 }
Modified static/style.scss from [a256539ae3] to [f40a20016f].
1 1 $default-color: hsl(323,100%,65%); 2 2 %sans { font-family: "Alegreya Sans", "Open Sans", sans-serif; } 3 3 %serif { font-family: Alegreya, GaramondNo8, "Garamond Premier Pro", "Adobe Garamond", Garamond, Junicode, serif; } 4 4 %teletype { font-family: "Inconsolata LGC", Inconsolata, monospace; font-size: 12pt !important; } 5 5 6 6 // @function tone($pct, $alpha: 0) { @return adjust-color($color, $lightness: $pct, $alpha: $alpha) } 7 -@function tone($pct, $alpha: 0) { 8 - @return hsla(var(--hue), 100%, 65% + $pct, 1 + $alpha) 9 -} 7 +@function tone($pct, $alpha: 0) { @return hsla(var(--hue), 100%, 65% + $pct, 1 + $alpha) } 8 +@function vtone($pct, $vary, $alpha: 0) { @return hsla(calc(var(--hue) + $vary), 100%, 65% + $pct, 1 + $alpha) } 9 +@function otone($pct, $alpha: 0) { @return hsla(calc(var(--hue) + var(--co)), 100%, 65% + $pct, 1 + $alpha) } 10 10 11 -:root { --hue: 323; } 11 +:root { --hue: 323; --co: 0; } 12 12 body { 13 13 @extend %sans; 14 14 background-color: tone(-55%); 15 15 color: tone(25%); 16 16 font-size: 14pt; 17 17 margin: 0; 18 18 padding: 0; ................................................................................ 24 24 ::placeholder { 25 25 color: tone(0,-0.3); 26 26 font-style: italic; 27 27 } 28 28 a[href] { 29 29 color: tone(10%); 30 30 text-decoration-color: tone(10%,-0.5); 31 + text-decoration-skip-ink: all; 32 + text-decoration-thickness: 1px; 33 + text-underline-offset: 0.1em; 31 34 &:hover, &:focus { 32 35 color: white; 33 36 text-shadow: 0 0 15px tone(20%); 34 37 text-decoration-color: tone(10%,-0.1); 35 38 outline: none; 36 39 } 37 40 &.button { @extend %button; } ................................................................................ 68 71 69 72 %button { 70 73 @extend %sans; 71 74 font-size: 14pt; 72 75 box-sizing: border-box; 73 76 padding: 0.1in 0.2in; 74 77 border: 1px solid black; 75 - color: tone(25%); 78 + color: otone(25%); 76 79 text-shadow: 1px 1px black; 77 80 text-decoration: none; 78 81 text-align: center; 79 82 cursor: default; 80 83 user-select: none; 81 84 -webkit-user-drag: none; 82 85 -webkit-app-region: no-drag; 83 86 background: linear-gradient(to bottom, 84 - tone(-47%), 85 - tone(-50%) 15%, 86 - tone(-50%) 75%, 87 - tone(-53%) 87 + otone(-47%), 88 + otone(-50%) 15%, 89 + otone(-50%) 75%, 90 + otone(-53%) 88 91 ); 89 92 &:hover, &:focus { 90 93 @extend %glow; 91 94 outline: none; 92 95 color: tone(-55%); 93 96 text-shadow: none; 94 97 background: linear-gradient(to bottom, 95 - tone(-27%), 96 - tone(-30%) 15%, 97 - tone(-30%) 75%, 98 - tone(-35%) 98 + otone(-27%), 99 + otone(-30%) 15%, 100 + otone(-30%) 75%, 101 + otone(-35%) 99 102 ); 100 103 } 101 104 &:active { 102 105 color: black; 103 106 padding-bottom: calc(0.1in - 2px); 104 107 padding-top: calc(0.1in + 2px); 105 108 background: linear-gradient(to top, 106 - tone(-25%), 107 - tone(-30%) 15%, 108 - tone(-30%) 75%, 109 - tone(-35%) 109 + otone(-25%), 110 + otone(-30%) 15%, 111 + otone(-30%) 75%, 112 + otone(-35%) 110 113 ); 111 114 } 112 115 } 113 116 114 117 button { @extend %button; 115 118 &:first-of-type { 116 119 @extend %button; 117 120 color: white; 118 - box-shadow: inset 0 1px tone(-25%), 119 - inset 0 -1px tone(-50%); 121 + box-shadow: inset 0 1px otone(-25%), 122 + inset 0 -1px otone(-50%); 120 123 background: linear-gradient(to bottom, 121 - tone(-35%), 122 - tone(-40%) 15%, 123 - tone(-40%) 75%, 124 - tone(-45%) 124 + otone(-35%), 125 + otone(-40%) 15%, 126 + otone(-40%) 75%, 127 + otone(-45%) 125 128 ); 126 129 &:hover, &:focus { 127 - box-shadow: inset 0 1px tone(-15%), 128 - inset 0 -1px tone(-40%); 130 + box-shadow: inset 0 1px otone(-15%), 131 + inset 0 -1px otone(-40%); 129 132 } 130 133 &:active { 131 - box-shadow: inset 0 1px tone(-50%), 132 - inset 0 -1px tone(-25%); 134 + box-shadow: inset 0 1px otone(-50%), 135 + inset 0 -1px otone(-25%); 133 136 background: linear-gradient(to top, 134 - tone(-30%), 135 - tone(-35%) 15%, 136 - tone(-35%) 75%, 137 - tone(-40%) 137 + otone(-30%), 138 + otone(-35%) 15%, 139 + otone(-35%) 75%, 140 + otone(-40%) 138 141 ); 139 142 } 140 143 } 141 - &:hover { font-weight: bold; } 144 + //&:hover { font-weight: bold; } 142 145 } 143 146 144 147 $grad-ui-focus: linear-gradient(to bottom, 145 148 tone(-50%), 146 149 tone(-35%) 147 150 ); 148 151 149 -input[type='text'], input[type='password'], textarea, select { 152 +input[type='text'], input[type='number'], input[type='password'], textarea, select { 150 153 @extend %serif; 151 154 padding: 0.08in 0.1in; 152 155 box-sizing: border-box; 153 156 border: 1px solid black; 154 157 background: linear-gradient(to bottom, tone(-55%), tone(-40%)); 155 158 font-size: 16pt; 156 159 color: tone(25%); ................................................................................ 236 239 padding-bottom: 0.1in; 237 240 background-color: tone(-45%,-0.3); 238 241 border: { 239 242 left: 1px solid black; 240 243 right: 1px solid black; 241 244 } 242 245 } 246 + 247 +.id { 248 + color: tone(25%,-0.4); 249 + > .nym { 250 + font-weight: bold; 251 + color: tone(25%); 252 + } 253 + > .xid { 254 + color: tone(20%,-0.1); 255 + font-size: 80%; 256 + vertical-align: text-top; 257 + } 258 +} 243 259 244 260 div.profile { 245 261 padding: 0.1in; 246 262 position: relative; 247 263 display: grid; 248 264 margin-bottom: 0.4in; 249 265 grid-template-columns: 2fr 1fr; ................................................................................ 261 277 grid-column: 1 / 2; 262 278 grid-row: 1 / 3; 263 279 border: 1px solid black; 264 280 } 265 281 > .id { 266 282 grid-column: 2 / 3; 267 283 grid-row: 1 / 2; 268 - color: tone(25%,-0.4); 269 - > .nym { 270 - font-weight: bold; 271 - color: tone(25%); 272 - } 273 - > .xid { 274 - color: tone(20%,-0.1); 275 - font-size: 80%; 276 - vertical-align: text-top; 277 - } 278 284 } 279 285 > .bio { 280 286 grid-column: 2 / 3; 281 287 grid-row: 2 / 3; 282 288 } 283 289 } 284 290 > .stats { 285 291 grid-column: 3 / 4; 286 292 grid-row: 1 / 3; 293 + display: flex; 294 + flex-flow: column; 295 + > * { flex-grow: 1; } 296 + table { td, th { text-align: center; } } 287 297 } 288 298 > form.actions { 289 299 grid-column: 1 / 3; grid-row: 2 / 3; 290 300 padding-top: 0.075in; 291 301 flex-wrap: wrap; 292 302 display: flex; 293 303 justify-content: center; ................................................................................ 606 616 } 607 617 input, textarea, .txtbox { 608 618 display: block; 609 619 width: 100%; 610 620 } 611 621 textarea { resize: vertical; min-height: 2in; } 612 622 } 613 - .elem + %button { margin-left: 50%; width: 50%; } 623 + :is(.elem,.elem-group) + %button { margin-left: 50%; width: 50%; } 624 + .elem-group { 625 + display: flex; 626 + flex-flow: row; 627 + > .elem { 628 + flex-shrink: 1; 629 + flex-grow: 1; 630 + margin-left: 0.1in; 631 + &:first-child { margin-left: 0; } 632 + } 633 + > .small { flex-shrink: 5; } 634 + } 614 635 } 615 636 616 637 menu.choice { 617 638 display: flex; 618 639 &.horizontal { 619 640 flex-flow: row-reverse wrap; 620 641 justify-content: space-evenly; ................................................................................ 713 734 714 735 .color-picker { 715 736 /* implemented using javascript, alas */ 716 737 @extend %box; 717 738 label { text-shadow: 1px 1px black; } 718 739 padding: 0.1in; 719 740 } 741 + 742 +ul.user-list { 743 + list-style-type: none; 744 + margin: 0.5em 0; 745 + padding: 0; 746 + box-shadow: 0 0 10px -3px black inset; 747 + border: 1px solid tone(-50%); 748 + li { 749 + background-color: tone(-20%, -0.8); 750 + padding: 0.5em; 751 + .regalia { margin-right: 0.3em; vertical-align: bottom; } 752 + &:nth-child(odd) { 753 + background-color: tone(-30%, -0.8); 754 + } 755 + } 756 +} 757 + 758 +ul.remarks { 759 + margin: 0; padding: 0; 760 + list-style-type: none; 761 + li { 762 + border-top: 1px solid otone(-22%); 763 + border-bottom: 2px solid otone(-55%); 764 + border-radius: 3px; 765 + background: otone(-25%,-0.4); 766 + color: otone(25%); 767 + text-align: center; 768 + padding: 0.3em 0; 769 + margin: 0.2em 0.1em; 770 + cursor: default; 771 + } 772 +} 773 + 774 +:is(%button, a[href]).neg { --co: 60 } 775 +:is(%button, a[href]).pos { --co: -30 }
Modified store.t from [e7b33e5534] to [da1c9184b0].
22 22 'pw', 'otp', 'challenge', 'trust' 23 23 }; 24 24 privset = lib.set { 25 25 'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite' 26 26 }; 27 27 powerset = lib.set { 28 28 -- user powers -- default on 29 - 'login', 'visible', 'post', 'shout', 30 - 'propagate', 'upload', 'acct', 'edit'; 29 + 'login', -- not locked out 30 + 'visible', -- account & posts can be seen by others 31 + 'post', -- can do poasts 32 + 'shout', -- posts show up on local timeline 33 + 'propagate', -- posts are sent to other instances 34 + 'artifact', -- upload, claim, and manage artifacts 35 + 'acct', -- configure own account 36 + 'edit'; -- edit own poasts 31 37 32 38 -- admin powers -- default off 33 - 'purge', 'config', 'censor', 'suspend', 34 - 'cred', 'elevate', 'demote', 'rebrand', -- modify site's brand identity 35 - 'herald', -- grant serverwide epithets 39 + 'purge', -- permanently delete users 40 + 'config', -- change daemon policy & config UI 41 + 'censor', -- dispose of badthink 42 + 'discipline', -- enforced timeouts, stripping badges and epithets, punitive actions that do not permanently deprive of powers; can remove own injunctions but not others' 43 + 'vacate', -- can remove others' injunctions, but not apply them 44 + 'cred', -- alter credentials 45 + 'elevate', 'demote', -- change user rank, give and take powers, including the ability to log in 46 + 'rebrand', -- modify site's brand identity 47 + 'herald', -- grant serverwide epithets and badges 36 48 'invite' -- *unlimited* invites 37 49 }; 38 50 prepmode = lib.enum { 39 51 'full','conf','admin' 40 52 } 41 53 } 42 54 ................................................................................ 46 58 m.privmap[#m.privmap + 1] = quote 47 59 var ps: m.powerset ps:clear() 48 60 (ps.[v] << true) 49 61 in pt {name = lib.str.plit(v), priv = ps} end 50 62 end end 51 63 52 64 terra m.powerset:affect_users() 53 - return self.purge() or self.censor() or self.suspend() or 65 + return self.purge() or self.discipline() or self.herald() or 54 66 self.elevate() or self.demote() or self.cred() 55 67 end 56 68 57 69 local str = rawstring 58 70 local pstr = lib.mem.ptr(int8) 59 71 60 72 struct m.source ................................................................................ 64 76 -- creating staff automatically assigns rank immediately below you 65 77 quota: uint32 -- # of allowed tweets per day; 0 = no limit 66 78 invites: uint32 -- # of people left this user can invite 67 79 68 80 powers: m.powerset 69 81 } 70 82 71 -terra m.rights_default() 83 +terra m.rights_default() -- TODO make configurable 72 84 var pow: m.powerset pow:clear() 73 85 (pow.login << true) 74 86 (pow.visible << true) 75 87 (pow.post << true) 76 88 (pow.shout << true) 77 89 (pow.propagate << true) 78 - (pow.upload << true) 90 + (pow.artifact << true) 79 91 (pow.acct << true) 80 92 (pow.edit << true) 81 93 return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; } 82 94 end 83 95 84 96 struct m.actor { 85 97 id: uint64 ................................................................................ 94 106 rights: m.rights 95 107 key: lib.mem.ptr(uint8) 96 108 97 109 -- ephemera 98 110 xid: str 99 111 source: &m.source 100 112 } 113 + 114 +terra m.actor:outranks(other: &m.actor) 115 + -- this predicate determines where two users stand relative to 116 + -- each other in the formal staff hierarchy. it is used in 117 + -- authority calculations, but this function should only be 118 + -- used directly in rendering code and by other predicates. 119 + -- do not use it in authority calculation, as there are special 120 + -- cases where formal rank does not fully determine a user's 121 + -- capabilities (e.g. roots have the same rank, but can 122 + -- exercise power over each other, unlike lower ranks) 123 + if self.rights.rank == 0 then 124 + -- peons never outrank anybody 125 + return false 126 + end 127 + if other.rights.rank == 0 then 128 + -- everybody outranks peons 129 + return true 130 + end 131 + return self.rights.rank < other.rights.rank 132 + -- rank 1 is the highest possible, rank 2 is second-highest, and so on 133 +end 134 + 135 +terra m.actor:overpowers(other: &m.actor) 136 + -- this predicate determines whether one user may exercise their 137 + -- powers over another user. it does not affect what those powers 138 + -- actually are (for instance, you cannot revoke a power you do 139 + -- not have, no matter how much you outrank someone) 140 + if self.rights.rank == 1 and other.rights.rank == 1 then 141 + -- special case: root users always overpower each other 142 + -- otherwise, nobody could reset their passwords 143 + -- (also dissuades people from giving root lightly) 144 + return true 145 + end 146 + return self:outranks(other) 147 +end 148 + 149 +terra m.actor.methods.handle_validate(hnd: rawstring) 150 + if hnd[0] == 0 then 151 + return false 152 + end 153 + -- TODO validate fully 154 + return true 155 +end 101 156 102 157 terra m.actor.methods.mk(kbuf: &uint8) 103 158 var newkp = lib.crypt.genkp() 104 159 var privsz = lib.crypt.der(false,&newkp,kbuf) 105 160 return m.actor { 106 161 id = 0; nym = nil; handle = nil; 107 162 origin = 0; bio = nil; avatar = nil; ................................................................................ 280 335 conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8) 281 336 conf_set: {&m.source, rawstring, rawstring} -> {} 282 337 conf_reset: {&m.source, rawstring} -> {} 283 338 284 339 actor_create: {&m.source, &m.actor} -> uint64 285 340 actor_save: {&m.source, &m.actor} -> {} 286 341 actor_save_privs: {&m.source, &m.actor} -> {} 342 + actor_purge_uid: {&m.source, uint64} -> {} 287 343 actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor) 288 344 actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor) 289 345 actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif) 290 - actor_enum: {&m.source} -> lib.mem.ptr(&m.actor) 291 - actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor) 346 + actor_enum: {&m.source} -> lib.mem.lstptr(m.actor) 347 + actor_enum_local: {&m.source} -> lib.mem.lstptr(m.actor) 292 348 actor_stats: {&m.source, uint64} -> m.actor_stats 293 349 actor_rel: {&m.source, uint64, uint64} -> m.relationship 294 350 295 351 actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool} 296 352 -- returns a set of auth method categories that are available for a 297 353 -- given user from a certain origin 298 354 -- origin: inet ................................................................................ 326 382 -- cookie issue time: m.timepoint 327 383 actor_auth_register_uid: {&m.source, uint64, uint64} -> {} 328 384 -- notifies the backend module of the UID that has been assigned for 329 385 -- an authentication ID 330 386 -- aid: uint64 331 387 -- uid: uint64 332 388 333 - auth_enum_uid: {&m.source, uint64} -> lib.mem.ptr(lib.mem.ptr(m.auth)) 334 - auth_enum_handle: {&m.source, rawstring} -> lib.mem.ptr(lib.mem.ptr(m.auth)) 389 + auth_enum_uid: {&m.source, uint64} -> lib.mem.lstptr(m.auth) 390 + auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth) 335 391 auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {} 336 392 -- uid: uint64 337 393 -- reset: bool (delete other passwords?) 338 394 -- pw: pstring 339 395 -- comment: pstring 340 396 auth_purge_pw: {&m.source, uint64, rawstring} -> {} 341 397 auth_purge_otp: {&m.source, uint64, rawstring} -> {} ................................................................................ 350 406 -- uid: uint64 351 407 -- timestamp: timepoint 352 408 353 409 post_save: {&m.source, &m.post} -> {} 354 410 post_create: {&m.source, &m.post} -> uint64 355 411 post_destroy: {&m.source, uint64} -> {} 356 412 post_fetch: {&m.source, uint64} -> lib.mem.ptr(m.post) 357 - post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post)) 358 - post_enum_parent: {&m.source, uint64} -> lib.mem.ptr(lib.mem.ptr(m.post)) 413 + post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post) 414 + post_enum_parent: {&m.source, uint64} -> lib.mem.lstptr(m.post) 359 415 post_attach_ctl: {&m.source, uint64, uint64, bool} -> {} 360 416 -- attaches or detaches an existing database artifact 361 417 -- post id: uint64 362 418 -- artifact id: uint64 363 419 -- detach: bool 364 420 365 421 thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint ................................................................................ 402 458 -- proto: kompromat (null for all records, or a prototype describing the records to return) 403 459 nkvd_sanction_issue: {&m.source, &m.sanction} -> uint64 404 460 nkvd_sanction_vacate: {&m.source, uint64} -> {} 405 461 nkvd_sanction_enum_target: {&m.source, uint64} -> {} 406 462 nkvd_sanction_enum_issuer: {&m.source, uint64} -> {} 407 463 nkvd_sanction_review: {&m.source, m.timepoint} -> {} 408 464 409 - timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post)) 410 - timeline_instance_fetch: {&m.source, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post)) 465 + timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post) 466 + timeline_instance_fetch: {&m.source, m.range} -> lib.mem.lstptr(m.post) 411 467 } 412 468 413 469 m.user_conf_funcs(m.backend, 'str', rawstring, lib.mem.ptr(int8)) 414 470 m.user_conf_funcs(m.backend, 'int', intptr, intptr, bool) 415 471 416 472 struct m.source { 417 473 backend: &m.backend
Modified view/profile.tpl from [694fa2eec6] to [4ac5403896].
2 2 <div class="banner"> 3 3 <img class="avatar" src="@:avatar"> 4 4 <div class="id">@nym</div> 5 5 <div class="bio"> 6 6 @bio 7 7 </div> 8 8 </div> 9 - <table class="stats"> 10 - <tr><th>posts</th> <td>@nposts</td></tr> 11 - <tr><th>following</th> <td>@nfollows</td></tr> 12 - <tr><th>followers</th> <td>@nfollowers</td></tr> 13 - <tr><th>mutuals</th> <td>@nmutuals</td></tr> 14 - <tr><th>@timephrase</th> <td>@tweetday</td></tr> 15 - </table> 9 + <div class="stats"> 10 + <table> 11 + <tr><th>posts</th> <th>mutuals</th></tr> 12 + <tr><td>@nposts</td> <td>@nmutuals</td></tr> 13 + <tr><th>following</th> <th>followers</th></tr> 14 + <tr><td>@nfollows</td> <td>@nfollowers</td></tr> 15 + <tr><th>@timephrase</th> <td>@tweetday</td></tr> 16 + </table> 17 + <ul class="remarks">@remarks</ul> 18 + </div> 16 19 <form class="actions"> 17 20 <a class="button" href="/@:xid">posts</a> 18 21 <a class="button" href="/@:xid/arc">archive</a> 19 22 <a class="button" href="/@:xid/media">media</a> 20 23 <a class="button" href="/@:xid/social">associates</a> 21 24 <hr> 22 25 @auxbtn 23 26 </form> 24 27 </div>