parsav  Check-in [7129658e1d]

Overview
Comment:work on admin ui
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 7129658e1d31700f1ac316f1d4d2962d31f39dc951a2f2d34c5830cf8aed0a84
User & Date: lexi on 2021-01-02 04:47:03
Other Links: manifest | tags
Context
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
Changes

Modified backend/pgsql.t from [30375d8380] to [962a3e64e0].

26
27
28
29
30
31
32
33

34
35
36
37
38
39
40
..
42
43
44
45
46
47
48

49
50
51
52
53
54
55
..
58
59
60
61
62
63
64









65
66
67
68
69
70
71
72
73
74
75

76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

104
105
106
107
108
109

110
111
112
113
114
115
116
...
125
126
127
128
129
130
131
132
133
134
135

136
137
138
139
140
141
142
143
144
145

146
147

148
149
150
151
152
153
154
...
786
787
788
789
790
791
792









793
794
795



796

797
798
799
800
801
802
803
...
807
808
809
810
811
812
813
814
815
816
817
818
819
820

821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
...
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
....
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
....
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
....
1219
1220
1221
1222
1223
1224
1225


1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249





1250
1251
1252
1253
1254
1255
1256

	actor_fetch_uid = {
		params = {uint64}, sql = [[
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
			       extract(epoch from a.knownsince)::bigint,
				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid


			from      parsav_actors  as a
			left join parsav_servers as s
				on a.origin = s.id
			where a.id = $1::bigint
		]];
	};
................................................................................
	actor_fetch_xid = {
		params = {pstring}, sql = [[
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
			       extract(epoch from a.knownsince)::bigint,
				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid,


				coalesce(s.domain,
				        (select value from parsav_config
							where key='domain' limit 1)) as domain

			from      parsav_actors  as a
			left join parsav_servers as s
................................................................................
			where $1::text = (a.handle || '@' || domain) or
			      $1::text = ('@' || a.handle || '@' || domain) or
				  (a.origin is null and
					  $1::text = a.handle or
					  $1::text = ('@' || a.handle))
		]];
	};










	actor_save = {
		params = {
			uint64, --id
			rawstring, --nym
			rawstring, --handle
			rawstring, --bio 
			rawstring, --epithet
			rawstring, --avataruri
			uint64, --avatarid
			uint16, --rank

			uint32 --quota
		}, cmd = true, sql = [[
			update parsav_actors set
				nym = $2::text,
				handle = $3::text,
				bio = $4::text,
				epithet = $5::text,
				avataruri = $6::text,
				avatarid = $7::bigint,
				rank = $8::smallint,
				quota = $9::integer
				--invites are controlled by their own specialized routines
			where id = $1::bigint
		]];
	};

	actor_create = {
		params = {
			rawstring, rawstring, uint64, lib.store.timepoint,
			rawstring, rawstring, lib.mem.ptr(uint8),
			rawstring, uint16, uint32
		};
		sql = [[
			insert into parsav_actors (
				nym,handle,
				origin,knownsince,
				bio,avataruri,key,
				epithet,rank,quota

			) values ($1::text, $2::text,
				case when $3::bigint = 0 then null
				     else $3::bigint end,
				to_timestamp($4::bigint),
				$5::bigint, $6::bigint, $7::bytea,
				$8::text, $9::smallint, $10::integer

			) returning id
		]];
	};

	actor_auth_pw = {
		params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
			select a.aid, a.uid, a.name from parsav_auth as a
................................................................................
	};

	actor_enum_local = {
		params = {}, sql = [[
			select id, nym, handle, origin, bio,
			       null::text, rank, quota, key, epithet,
			       extract(epoch from knownsince)::bigint,
				handle ||'@'||
				(select value from parsav_config
					where key='domain' limit 1) as xid
			from parsav_actors where origin is null

		]];
	};

	actor_enum = {
		params = {}, sql = [[
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
			       extract(epoch from a.knownsince)::bigint,
				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid

			from parsav_actors a
			left join parsav_servers s on s.id = a.origin

		]];
	};

	actor_stats = {
		params = {uint64}, sql = ([[
			with tweets as (
				select from parsav_posts where author = $1::bigint
................................................................................
end
local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
	var a: lib.mem.ptr(lib.store.actor)
	var av: rawstring, avlen: intptr
	var nym: rawstring, nymlen: intptr
	var bio: rawstring, biolen: intptr
	var epi: rawstring, epilen: intptr









	if r:null(row,5) then avlen = 0 av = nil else
		av = r:string(row,5)
		avlen = r:len(row,5)+1



	end

	if r:null(row,1) then nymlen = 0 nym = nil else
		nym = r:string(row,1)
		nymlen = r:len(row,1)+1
	end
	if r:null(row,4) then biolen = 0 bio = nil else
		bio = r:string(row,4)
		biolen = r:len(row,4)+1
................................................................................
		epilen = r:len(row,9)+1
	end
	a = [ lib.str.encapsulate(lib.store.actor, {
		nym = {`nym, `nymlen};
		bio = {`bio, `biolen};
		epithet = {`epi, `epilen};
		avatar = {`av,`avlen};
		handle = {`r:string(row, 2); `r:len(row,2) + 1};
		xid = {`r:string(row, 11); `r:len(row,11) + 1};
	}) ]
	a.ptr.id = r:int(uint64, row, 0);
	a.ptr.rights = lib.store.rights_default();
	a.ptr.rights.rank = r:int(uint16, row, 6);
	a.ptr.rights.quota = r:int(uint32, row, 7);

	a.ptr.knownsince = r:int(int64,row, 10);
	if r:null(row,8) then
		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
	else
		a.ptr.key = r:bin(row,8)
	end
	if r:null(row,3) then a.ptr.origin = 0
	else a.ptr.origin = r:int(uint64,row,3) end
	return a
end

local privmap = lib.store.privmap

local checksha = function(src, hash, origin, username, pw)
	local validate = function(kind, cred, credlen)
................................................................................
local schema = sqlsquash(lib.util.ingest('backend/schema/pgsql.sql'))
local obliterator = sqlsquash(lib.util.ingest('backend/schema/pgsql-drop.sql'))

local privupdate = terra(
	src: &lib.store.source,
	ac: &lib.store.actor
): {}
	var pdef = lib.store.rights_default().powers
	var map = array([privmap])
	for i=0, [map.type.N] do
		var d = pdef and map[i].priv
		var u = ac.rights.powers and map[i].priv
		queries.actor_power_delete.exec(src, ac.id, map[i].name)
		if d:sz() > 0 and u:sz() == 0 then
			lib.dbg('blocking power ', {map[i].name.ptr, map[i].name.ct})
................................................................................
			return a
		end
	end];

	actor_enum = [terra(src: &lib.store.source)
		var r = queries.actor_enum.exec(src)
		if r.sz == 0 then
			return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil }
		else defer r:free()
			var mem = lib.mem.heapa([&lib.store.actor], r.sz)
			for i=0,r.sz do
				mem.ptr[i] = row_to_actor(&r, i).ptr
				mem.ptr[i].source = src
			end
			return [lib.mem.ptr(&lib.store.actor)] { ct = r.sz, ptr = mem.ptr }
		end
	end];

	actor_enum_local = [terra(src: &lib.store.source)
		var r = queries.actor_enum_local.exec(src)
		if r.sz == 0 then
			return [lib.mem.ptr(&lib.store.actor)] { ct = 0, ptr = nil }
		else defer r:free()
			var mem = lib.mem.heapa([&lib.store.actor], r.sz)
			for i=0,r.sz do
				mem.ptr[i] = row_to_actor(&r, i).ptr
				mem.ptr[i].source = src
			end
			return [lib.mem.ptr(&lib.store.actor)] { ct = r.sz, ptr = mem.ptr }
		end
	end];

	actor_auth_how = [terra(
			src: &lib.store.source,
			ip: lib.store.inet,
			username: rawstring
................................................................................

			var a = row_to_actor(&r, 0)
			a.ptr.source = src

			var au = [lib.stat(lib.store.auth)] { ok = true }
			au.val.aid = aid
			au.val.uid = a.ptr.id
			if not r:null(0,13) then -- restricted?
				au.val.privs:clear()
				(au.val.privs.post   << r:bool(0,14)) 
				(au.val.privs.edit   << r:bool(0,15))
				(au.val.privs.acct   << r:bool(0,16))
				(au.val.privs.upload << r:bool(0,17))
				(au.val.privs.censor << r:bool(0,18))
				(au.val.privs.admin  << r:bool(0,19))
			else au.val.privs:fill() end

			return au, a
		end

		::fail:: return [lib.stat   (lib.store.auth) ] { ok = false        },
			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
................................................................................
	end];

	actor_powers_fetch = getpow;
	actor_save = [terra(
		src: &lib.store.source,
		ac: &lib.store.actor
	): {}


		queries.actor_save.exec(src,
			ac.id, ac.nym, ac.handle,
			ac.bio, ac.epithet, ac.avatar,
			ac.avatarid, ac.rights.rank, ac.rights.quota)
	end];

	actor_save_privs = privupdate;

	actor_create = [terra(
		src: &lib.store.source,
		ac: &lib.store.actor
	): uint64
		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)
		if r.sz == 0 then lib.bail('failed to create actor!') end
		ac.id = r:int(uint64,0,0)

		-- check against default rights, insert records for wherever powers differ
		lib.dbg('created new actor, establishing powers')
		privupdate(src,ac)

		lib.dbg('powers established')
		return ac.id
	end];






	auth_enum_uid = [terra(
		src: &lib.store.source,
		uid: uint64
	): lib.mem.ptr(lib.mem.ptr(lib.store.auth))
		var r = queries.auth_enum_uid.exec(src,uid)
		if r.sz == 0 then return [lib.mem.ptr(lib.mem.ptr(lib.store.auth))].null() end
		var ret = lib.mem.heapa([lib.mem.ptr(lib.store.auth)], r.sz)







|
>







 







>







 







>
>
>
>
>
>
>
>
>











>
|









|
|








|






|
>





|
>







 







|
|
<

>









|
>


>







 







>
>
>
>
>
>
>
>
>
|


>
>
>

>







 







|






>






|
|







 







|







 







|

|

|
|

|






|

|

|
|

|







 







|

|
|
|
|
|
|







 







>
>


|
|








|











>
>
>
>
>







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
..
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
..
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
...
139
140
141
142
143
144
145
146
147

148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
...
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
...
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
...
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
....
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
....
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
....
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293

	actor_fetch_uid = {
		params = {uint64}, sql = [[
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
			       extract(epoch from a.knownsince)::bigint,
				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid,
			       a.invites

			from      parsav_actors  as a
			left join parsav_servers as s
				on a.origin = s.id
			where a.id = $1::bigint
		]];
	};
................................................................................
	actor_fetch_xid = {
		params = {pstring}, sql = [[
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
			       extract(epoch from a.knownsince)::bigint,
				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid,
			       a.invites,

				coalesce(s.domain,
				        (select value from parsav_config
							where key='domain' limit 1)) as domain

			from      parsav_actors  as a
			left join parsav_servers as s
................................................................................
			where $1::text = (a.handle || '@' || domain) or
			      $1::text = ('@' || a.handle || '@' || domain) or
				  (a.origin is null and
					  $1::text = a.handle or
					  $1::text = ('@' || a.handle))
		]];
	};

	actor_purge_uid = {
		params = {uint64}, cmd = true, sql = [[
			with d as ( -- cheating
				delete from parsav_sanctions where victim = $1::bigint
			)
			delete from parsav_actors where id = $1::bigint
		]];
	};

	actor_save = {
		params = {
			uint64, --id
			rawstring, --nym
			rawstring, --handle
			rawstring, --bio 
			rawstring, --epithet
			rawstring, --avataruri
			uint64, --avatarid
			uint16, --rank
			uint32, --quota
			uint32 --invites
		}, cmd = true, sql = [[
			update parsav_actors set
				nym = $2::text,
				handle = $3::text,
				bio = $4::text,
				epithet = $5::text,
				avataruri = $6::text,
				avatarid = $7::bigint,
				rank = $8::smallint,
				quota = $9::integer,
				invites = $10::integer
			where id = $1::bigint
		]];
	};

	actor_create = {
		params = {
			rawstring, rawstring, uint64, lib.store.timepoint,
			rawstring, rawstring, lib.mem.ptr(uint8),
			rawstring, uint16, uint32, uint32
		};
		sql = [[
			insert into parsav_actors (
				nym,handle,
				origin,knownsince,
				bio,avataruri,key,
				epithet,rank,quota,
				invites
			) values ($1::text, $2::text,
				case when $3::bigint = 0 then null
				     else $3::bigint end,
				to_timestamp($4::bigint),
				$5::bigint, $6::bigint, $7::bytea,
				$8::text, $9::smallint, $10::integer,
				$11::integer
			) returning id
		]];
	};

	actor_auth_pw = {
		params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
			select a.aid, a.uid, a.name from parsav_auth as a
................................................................................
	};

	actor_enum_local = {
		params = {}, sql = [[
			select id, nym, handle, origin, bio,
			       null::text, rank, quota, key, epithet,
			       extract(epoch from knownsince)::bigint,
					'@' || handle,
				   invites

			from parsav_actors where origin is null
			order by nullif(rank,0) nulls last, handle
		]];
	};

	actor_enum = {
		params = {}, sql = [[
			select a.id, a.nym, a.handle, a.origin, a.bio,
			       a.avataruri, a.rank, a.quota, a.key, a.epithet,
			       extract(epoch from a.knownsince)::bigint,
				   coalesce(a.handle || '@' || s.domain,
				            '@' || a.handle) as xid,
				   invites
			from parsav_actors a
			left join parsav_servers s on s.id = a.origin
			order by nullif(a.rank,0) nulls last, a.handle, a.origin
		]];
	};

	actor_stats = {
		params = {uint64}, sql = ([[
			with tweets as (
				select from parsav_posts where author = $1::bigint
................................................................................
end
local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
	var a: lib.mem.ptr(lib.store.actor)
	var av: rawstring, avlen: intptr
	var nym: rawstring, nymlen: intptr
	var bio: rawstring, biolen: intptr
	var epi: rawstring, epilen: intptr
	var origin: uint64 = 0
	var handle = r:_string(row, 2)
	if not r:null(row,3) then origin = r:int(uint64,row,3) end

	var avia = lib.str.acc {buf=nil}
	if origin == 0 then
		avia:compose('/avi/',handle)
		av = avia.buf
		avlen = avia.sz+1
	elseif r:null(row,5) then
		av = r:string(row,5)
		avlen = r:len(row,5)+1
	else
		av = '/s/default-avatar.webp'
		avlen = 22
	end

	if r:null(row,1) then nymlen = 0 nym = nil else
		nym = r:string(row,1)
		nymlen = r:len(row,1)+1
	end
	if r:null(row,4) then biolen = 0 bio = nil else
		bio = r:string(row,4)
		biolen = r:len(row,4)+1
................................................................................
		epilen = r:len(row,9)+1
	end
	a = [ lib.str.encapsulate(lib.store.actor, {
		nym = {`nym, `nymlen};
		bio = {`bio, `biolen};
		epithet = {`epi, `epilen};
		avatar = {`av,`avlen};
		handle = {`handle.ptr, `handle.ct + 1};
		xid = {`r:string(row, 11); `r:len(row,11) + 1};
	}) ]
	a.ptr.id = r:int(uint64, row, 0);
	a.ptr.rights = lib.store.rights_default();
	a.ptr.rights.rank = r:int(uint16, row, 6);
	a.ptr.rights.quota = r:int(uint32, row, 7);
	a.ptr.rights.invites = r:int(uint32, row, 12);
	a.ptr.knownsince = r:int(int64,row, 10);
	if r:null(row,8) then
		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
	else
		a.ptr.key = r:bin(row,8)
	end
	a.ptr.origin = origin
	if avia.buf ~= nil then avia:free() end
	return a
end

local privmap = lib.store.privmap

local checksha = function(src, hash, origin, username, pw)
	local validate = function(kind, cred, credlen)
................................................................................
local schema = sqlsquash(lib.util.ingest('backend/schema/pgsql.sql'))
local obliterator = sqlsquash(lib.util.ingest('backend/schema/pgsql-drop.sql'))

local privupdate = terra(
	src: &lib.store.source,
	ac: &lib.store.actor
): {}
	var pdef: lib.store.powerset pdef:clear()
	var map = array([privmap])
	for i=0, [map.type.N] do
		var d = pdef and map[i].priv
		var u = ac.rights.powers and map[i].priv
		queries.actor_power_delete.exec(src, ac.id, map[i].name)
		if d:sz() > 0 and u:sz() == 0 then
			lib.dbg('blocking power ', {map[i].name.ptr, map[i].name.ct})
................................................................................
			return a
		end
	end];

	actor_enum = [terra(src: &lib.store.source)
		var r = queries.actor_enum.exec(src)
		if r.sz == 0 then
			return [lib.mem.lstptr(lib.store.actor)].null()
		else defer r:free()
			var mem = lib.mem.heapa([lib.mem.ptr(lib.store.actor)], r.sz)
			for i=0,r.sz do
				mem.ptr[i] = row_to_actor(&r, i)
				mem(i).ptr.source = src
			end
			return [lib.mem.lstptr(lib.store.actor)] { ct = r.sz, ptr = mem.ptr }
		end
	end];

	actor_enum_local = [terra(src: &lib.store.source)
		var r = queries.actor_enum_local.exec(src)
		if r.sz == 0 then
			return [lib.mem.lstptr(lib.store.actor)].null()
		else defer r:free()
			var mem = lib.mem.heapa([lib.mem.ptr(lib.store.actor)], r.sz)
			for i=0,r.sz do
				mem.ptr[i] = row_to_actor(&r, i)
				mem(i).ptr.source = src
			end
			return [lib.mem.lstptr(lib.store.actor)] { ct = r.sz, ptr = mem.ptr }
		end
	end];

	actor_auth_how = [terra(
			src: &lib.store.source,
			ip: lib.store.inet,
			username: rawstring
................................................................................

			var a = row_to_actor(&r, 0)
			a.ptr.source = src

			var au = [lib.stat(lib.store.auth)] { ok = true }
			au.val.aid = aid
			au.val.uid = a.ptr.id
			if not r:null(0,14) then -- restricted?
				au.val.privs:clear()
				(au.val.privs.post   << r:bool(0,15)) 
				(au.val.privs.edit   << r:bool(0,16))
				(au.val.privs.acct   << r:bool(0,17))
				(au.val.privs.upload << r:bool(0,18))
				(au.val.privs.censor << r:bool(0,19))
				(au.val.privs.admin  << r:bool(0,20))
			else au.val.privs:fill() end

			return au, a
		end

		::fail:: return [lib.stat   (lib.store.auth) ] { ok = false        },
			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
................................................................................
	end];

	actor_powers_fetch = getpow;
	actor_save = [terra(
		src: &lib.store.source,
		ac: &lib.store.actor
	): {}
		var avatar = ac.avatar
		if ac.origin == 0 then avatar = nil end
		queries.actor_save.exec(src,
			ac.id, ac.nym, ac.handle,
			ac.bio, ac.epithet, avatar,
			ac.avatarid, ac.rights.rank, ac.rights.quota, ac.rights.invites)
	end];

	actor_save_privs = privupdate;

	actor_create = [terra(
		src: &lib.store.source,
		ac: &lib.store.actor
	): uint64
		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)
		if r.sz == 0 then lib.bail('failed to create actor!') end
		ac.id = r:int(uint64,0,0)

		-- check against default rights, insert records for wherever powers differ
		lib.dbg('created new actor, establishing powers')
		privupdate(src,ac)

		lib.dbg('powers established')
		return ac.id
	end];

	actor_purge_uid = [terra(
		src: &lib.store.source,
		uid: uint64
	) queries.actor_purge_uid.exec(src,uid) end];

	auth_enum_uid = [terra(
		src: &lib.store.source,
		uid: uint64
	): lib.mem.ptr(lib.mem.ptr(lib.store.auth))
		var r = queries.auth_enum_uid.exec(src,uid)
		if r.sz == 0 then return [lib.mem.ptr(lib.mem.ptr(lib.store.auth))].null() end
		var ret = lib.mem.heapa([lib.mem.ptr(lib.store.auth)], r.sz)

Modified backend/schema/pgsql.sql from [b4d8dee98e] to [72a3e65e6e].

30
31
32
33
34
35
36

37
38
39
40
41
42
43
...
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
		on delete cascade, -- null origin = local actor
	knownsince timestamp not null default now(),
	bio       text,
	avatarid  bigint, -- artifact id, null if remote
	avataruri text, -- null if local
	rank      smallint not null default 0,
	quota     integer not null default 1000,

	key       bytea, -- private if localactor; public if remote
	epithet   text,
	authtime  timestamp not null default now(), -- cookies earlier than this timepoint will not be accepted
	
	unique (handle,origin)
);

................................................................................

create table parsav_room_members (
	room   bigint not null references parsav_rooms(id) on delete cascade,
	member bigint not null references parsav_actors(id) on delete cascade,
	rank   smallint not null default 0,
	admin  boolean not null default false, -- non-admins with rank can only moderate + invite
	title  text, -- admin-granted title like reddit flair
	vouchedby bigint references parsav_actors(id)
);

create table parsav_invites (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	-- when a user is created from an invite, the invite is deleted and the invite
	-- ID becomes the user ID. privileges granted on the invite ID during the invite
	-- process are thus inherited by the user







>







 







|







30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
...
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
		on delete cascade, -- null origin = local actor
	knownsince timestamp not null default now(),
	bio       text,
	avatarid  bigint, -- artifact id, null if remote
	avataruri text, -- null if local
	rank      smallint not null default 0,
	quota     integer not null default 1000,
	invites   integer not null default 0,
	key       bytea, -- private if localactor; public if remote
	epithet   text,
	authtime  timestamp not null default now(), -- cookies earlier than this timepoint will not be accepted
	
	unique (handle,origin)
);

................................................................................

create table parsav_room_members (
	room   bigint not null references parsav_rooms(id) on delete cascade,
	member bigint not null references parsav_actors(id) on delete cascade,
	rank   smallint not null default 0,
	admin  boolean not null default false, -- non-admins with rank can only moderate + invite
	title  text, -- admin-granted title like reddit flair
	vouchedby bigint references parsav_actors(id) on delete set null
);

create table parsav_invites (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	-- when a user is created from an invite, the invite is deleted and the invite
	-- ID becomes the user ID. privileges granted on the invite ID during the invite
	-- process are thus inherited by the user

Modified config.lua from [6a4b9180fe] to [5a4f5a8d5b].

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
		-- we should add support for content-encoding headers and pre-compress
		-- the damn things before compiling (also making the binary smaller)
		{'style.css', 'text/css'};
		{'live.js', 'text/javascript'}; -- rrrrrrrr
		{'default-avatar.webp', 'image/webp'}; -- needs inkscape-exclusive svg features
		{'padlock.svg', 'image/svg+xml'};
		{'warn.svg', 'image/svg+xml'};
		{'query.svg', 'image/svg+xml'};
	};
	default_ui_accent = tonumber(default('parsav_ui_default_accent',323));
}
if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
	conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
end
if u.ping '.fslckout' or u.ping '_FOSSIL_' then







|







52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
		-- we should add support for content-encoding headers and pre-compress
		-- the damn things before compiling (also making the binary smaller)
		{'style.css', 'text/css'};
		{'live.js', 'text/javascript'}; -- rrrrrrrr
		{'default-avatar.webp', 'image/webp'}; -- needs inkscape-exclusive svg features
		{'padlock.svg', 'image/svg+xml'};
		{'warn.svg', 'image/svg+xml'};
		{'query.webp', 'image/webp'};
	};
	default_ui_accent = tonumber(default('parsav_ui_default_accent',323));
}
if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
	conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
end
if u.ping '.fslckout' or u.ping '_FOSSIL_' then

Modified makefile from [e6a5371547] to [eedbd28993].

1
2
3
4
5
6
7
8
9
10
11
dl = git
dbg-flags = $(if $(dbg),-g)

images = static/default-avatar.webp
#$(addsuffix .webp, $(basename $(wildcard static/*.svg)))
styles = $(addsuffix .css, $(basename $(wildcard static/*.scss)))

parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles)
	terra $(dbg-flags) $<
parsav.o parsavd.o: parsav.t config.lua pkgdata.lua $(images) $(styles)
	env parsav_link=no terra $(dbg-flags) $<



|







1
2
3
4
5
6
7
8
9
10
11
dl = git
dbg-flags = $(if $(dbg),-g)

images = static/default-avatar.webp static/query.webp
#$(addsuffix .webp, $(basename $(wildcard static/*.svg)))
styles = $(addsuffix .css, $(basename $(wildcard static/*.scss)))

parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles)
	terra $(dbg-flags) $<
parsav.o parsavd.o: parsav.t config.lua pkgdata.lua $(images) $(styles)
	env parsav_link=no terra $(dbg-flags) $<

Modified mem.t from [1f9397ac82] to [a0c3213659].

123
124
125
126
127
128
129

130
131
132
133
134
135
136
		end
	end
	return t
end

m.ptr = terralib.memoize(function(ty) return mkptr(ty, true) end)
m.ref = terralib.memoize(function(ty) return mkptr(ty, false) end)


m.vec = terralib.memoize(function(ty)
	local v = terralib.types.newstruct(string.format('vec<%s>', ty.name))
	v.entries = {
		{field = 'storage', type = m.ptr(ty)};
		{field = 'sz', type = intptr};
		{field = 'run', type = intptr};







>







123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
		end
	end
	return t
end

m.ptr = terralib.memoize(function(ty) return mkptr(ty, true) end)
m.ref = terralib.memoize(function(ty) return mkptr(ty, false) end)
m.lstptr = function(ty) return m.ptr(m.ptr(ty)) end -- make code more readable

m.vec = terralib.memoize(function(ty)
	local v = terralib.types.newstruct(string.format('vec<%s>', ty.name))
	v.entries = {
		{field = 'storage', type = m.ptr(ty)};
		{field = 'sz', type = intptr};
		{field = 'run', type = intptr};

Modified mgtool.t from [39281cf1cf] to [ee3dfa15a8].

21
22
23
24
25
26
27
28

29
30
31
32
33

34
35
36
37
38
39
40
...
130
131
132
133
134
135
136













137
138
139
140
141
142
143
...
272
273
274
275
276
277
278

279
280
281

282

283
284
285
286

287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
...
392
393
394
395
396
397
398
399























































400
401
402
403

404
405
406
407
408
409
410
411
412
413
414
415
416
417










418
419

420
421
422
423
424
425
426

local ctlcmds = {
	{ 'start', 'start a new instance of the server' };
	{ 'stop', 'stop a running instance' };
	{ 'ls', 'list all running instances' };
	{ 'attach', 'capture log output from a running instance' };
	{ 'db', 'set up and manage the database' };
	{ 'user', 'manage users, privileges, and credentials'};

	{ 'mkroot <handle>', 'establish a new root user with the given handle' };
	{ '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)' };
	{ 'actor <xid> create', 'instantiate a new actor' };
	{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
	{ 'conf', 'manage the server configuration'};

	{ 'serv dl', 'initiate an update cycle over foreign actors' };
	{ 'tl', 'print the current local timeline to standard out' };
	{ 'be pgsql setup-auth (managed|unmanaged)', '(PGSQL backends) select the authentication strategy to use' };
}

local cmdhelp = function(tbl)
	local str = '\ncommands:\n'
................................................................................
		if acks(i).success then
			lib.report('instance #',num,' reports successful ',rep)
		else
			lib.report('instance #',num,' reports failed ',rep)
		end
	end
end














local emp = lib.ipc.global_emperor
local terra entry_mgtool(argc: int, argv: &rawstring): int
	if argc < 1 then lib.bail('bad invocation!') end

	lib.noise.init(2)
	[lib.init]
................................................................................
				return 1
			end
			if dbmode.arglist.ct < 1 then goto cmderr end

			srv:setup(cnf) 
			if lib.str.cmp(dbmode.arglist(0),'init') == 0 and dbmode.arglist.ct == 2 then
				lib.report('initializing new database structure for domain ', dbmode.arglist(1))

				if dlg:dbsetup() then
					srv:conprep(lib.store.prepmode.conf)
					dlg:conf_set('instance-name', dbmode.arglist(1))

					do var sec: int8[65] gensec(&sec[0])

						dlg:conf_set('server-secret', &sec[0])
					end
					lib.report('database setup complete; use mkroot to create an administrative user')
				else lib.bail('initialization process interrupted') end

			elseif lib.str.cmp(dbmode.arglist(0),'obliterate') == 0 then
				var confirmstrs = array(
					'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa',
					'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst'
				)
				var cfmstr: int8[64] cfmstr[0] = 0
				var tdx = lib.osclock.time(nil) / 60
				for i=0,3 do
					if i ~= 0 then lib.str.cat(&cfmstr[0], '-') end
					lib.str.cat(&cfmstr[0], confirmstrs[(tdx ^ (173*i)) % [confirmstrs.type.N]])
				end

				if dbmode.arglist.ct == 1 then
					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])
				elseif dbmode.arglist.ct == 2 then
					if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then
						lib.warn('completely obliterating all data!')
						dlg:obliterate_everything()
................................................................................
					dlg:conf_set('master',root.handle)
					lib.report('created new administrator')
					if mg then
						var tmppw: int8[33]
						pwset(dlg, &tmppw, ruid, false)
						lib.report('temporary root pw: ', {&tmppw[0], 32})
					end
				else goto cmderr end























































			elseif lib.str.cmp(mode.arglist(0),'user') == 0 then
				var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
				if umode.help then
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' <handle> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {

						{ 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' };
						{ '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' };
						{ 'user <handle> auth (<type>|all) purge', 'delete all credentials that would allow this user to log in (where possible)' };
						{ 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' };
						{ 'user <handle> emasculate', 'strip all administrative powers from a user' };
						{ 'user <handle> forgive', 'restore all default powers to a user' };
						{ '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'};
					}) ]
					return 1
				end
				if umode.arglist.ct >= 3 then
					var grant = lib.str.cmp(umode.arglist(1),'grant') == 0
					var handle = umode.arglist(0)
					var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)})










					if grant or lib.str.cmp(umode.arglist(1),'revoke') == 0 then
						if not usr then lib.bail('unknown handle') end

						var newprivs = usr.ptr.rights.powers
						var map = array([lib.store.privmap])
						if umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(2),'all') == 0 then
							if grant
								then newprivs:fill()
								else newprivs:clear()
							end







|
>

<
<
<

>







 







>
>
>
>
>
>
>
>
>
>
>
>
>







 







>



>

>




>

<
<
<
<
|
<
<
<
<
<







 








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>




>




|





<
<
|
|
>
>
>
>
>
>
>
>
>
>
|
|
>







21
22
23
24
25
26
27
28
29
30



31
32
33
34
35
36
37
38
39
...
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
...
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303




304





305
306
307
308
309
310
311
...
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476


477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498

local ctlcmds = {
	{ 'start', 'start a new instance of the server' };
	{ 'stop', 'stop a running instance' };
	{ 'ls', 'list all running instances' };
	{ 'attach', 'capture log output from a running instance' };
	{ 'db', 'set up and manage the database' };
	{ 'user', 'create and manage users, privileges, and credentials'};
	{ 'actor', 'manage and purge actors, epithets, and ranks'};
	{ 'mkroot <handle>', 'establish a new root user with the given handle' };



	{ 'conf', 'manage the server configuration'};
	{ 'grow <count> [<acl>]', 'grant a new round of invites to all users, or those who match the given ACL' };
	{ 'serv dl', 'initiate an update cycle over foreign actors' };
	{ 'tl', 'print the current local timeline to standard out' };
	{ 'be pgsql setup-auth (managed|unmanaged)', '(PGSQL backends) select the authentication strategy to use' };
}

local cmdhelp = function(tbl)
	local str = '\ncommands:\n'
................................................................................
		if acks(i).success then
			lib.report('instance #',num,' reports successful ',rep)
		else
			lib.report('instance #',num,' reports failed ',rep)
		end
	end
end

local terra gen_cfstr(cfmstr: rawstring, seed: intptr)
	var confirmstrs = array(
		'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa',
		'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst'
	)
	var tdx = lib.osclock.time(nil) / 60
	cfmstr[0] = 0
	for i=0,3 do
		if i ~= 0 then lib.str.cat(cfmstr, '-') end
		lib.str.cat(cfmstr, confirmstrs[(seed ^ tdx ^ (173*i)) % [confirmstrs.type.N]])
	end
end

local emp = lib.ipc.global_emperor
local terra entry_mgtool(argc: int, argv: &rawstring): int
	if argc < 1 then lib.bail('bad invocation!') end

	lib.noise.init(2)
	[lib.init]
................................................................................
				return 1
			end
			if dbmode.arglist.ct < 1 then goto cmderr end

			srv:setup(cnf) 
			if lib.str.cmp(dbmode.arglist(0),'init') == 0 and dbmode.arglist.ct == 2 then
				lib.report('initializing new database structure for domain ', dbmode.arglist(1))
				dlg:tx_enter()
				if dlg:dbsetup() then
					srv:conprep(lib.store.prepmode.conf)
					dlg:conf_set('instance-name', dbmode.arglist(1))
					dlg:conf_set('domain', dbmode.arglist(1))
					do var sec: int8[65] gensec(&sec[0])
						dlg:conf_set('server-secret', &sec[0])
						dlg:conf_set('server-secret', &sec[0])
					end
					lib.report('database setup complete; use mkroot to create an administrative user')
				else lib.bail('initialization process interrupted') end
				dlg:tx_complete()
			elseif lib.str.cmp(dbmode.arglist(0),'obliterate') == 0 then




				var cfmstr: int8[64] gen_cfstr(&cfmstr[0],0)






				if dbmode.arglist.ct == 1 then
					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])
				elseif dbmode.arglist.ct == 2 then
					if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then
						lib.warn('completely obliterating all data!')
						dlg:obliterate_everything()
................................................................................
					dlg:conf_set('master',root.handle)
					lib.report('created new administrator')
					if mg then
						var tmppw: int8[33]
						pwset(dlg, &tmppw, ruid, false)
						lib.report('temporary root pw: ', {&tmppw[0], 32})
					end
				else goto cmderr end
			elseif lib.str.cmp(mode.arglist(0),'actor') == 0 then
				var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
				if umode.help then
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' actor ', umode.type.helptxt.flags, ' <xid> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
						{ 'actor <xid> rank <value>', 'set an actor\'s rank to <value> (remote actors cannot exercise rank-related powers, but benefit from rank immunities)' };
						{ 'actor <xid> degrade', 'alias for `actor <xid> rank 0`' };
						{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
						{ 'actor <xid> instantiate', 'instantiate a remote actor, retrieving their profile and posts even if no one follows them' };
						{ 'actor <xid> proscribe', 'globally ban an actor from interacting with your server' };
						{ 'actor <xid> rehabilitate', 'lift a proscription on an actor' };
						{ '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)' };
					}) ]
					return 1
				end
				if umode.arglist.ct >= 2 then
					var degrade = lib.str.cmp(umode.arglist(1),'degrade') == 0
					var xid = umode.arglist(0)
					var usr = dlg:actor_fetch_xid(pstr {ptr=xid, ct=lib.str.sz(xid)})
					if not usr then lib.bail('no such actor') end
					if degrade or lib.str.cmp(umode.arglist(1),'rank') == 0 then
						var rank: uint16
						if degrade and umode.arglist.ct == 2 then
							rank = 0
						elseif (not degrade) and umode.arglist.ct == 3 then
							var r, ok = lib.math.decparse(pstr {
								ptr = umode.arglist(2);
								ct = lib.str.sz(umode.arglist(2));
							})
							if not ok then goto cmderr end
							rank = r
						else goto cmderr end
						usr.ptr.rights.rank = rank
						dlg:actor_save(usr.ptr)
						lib.report('set user rank')
					elseif umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(1),'bestow') == 0 then
						if umode.arglist(2)[0] == 0
							then usr.ptr.epithet = nil
							else usr.ptr.epithet = umode.arglist(2)
						end
						dlg:actor_save(usr.ptr)
						lib.report('bestowed a new epithet on ', usr.ptr.xid)
					elseif lib.str.cmp(umode.arglist(1),'purge-all') == 0 then
						var cfmstr: int8[64] gen_cfstr(&cfmstr[0],usr.ptr.id)
						if umode.arglist.ct == 2 then
							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])
						elseif umode.arglist.ct == 3 then
							if lib.str.ncmp(&cfmstr[0],umode.arglist(2),64) ~= 0 then
								lib.bail('you have supplied an invalid confirmation string; if you really want to purge this actor, pass ', &cfmstr[0])
							end
							lib.warn('completely purging actor ', usr.ptr.xid, ' and all related content from database')
							dlg:actor_purge_uid(usr.ptr.id)
							lib.report('actor purged')
						else goto cmderr end
					else goto cmderr end
				else goto cmderr end
			elseif lib.str.cmp(mode.arglist(0),'user') == 0 then
				var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
				if umode.help then
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' <handle> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
						{ 'user <handle> create', 'add a new user' };
						{ 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' };
						{ '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' };
						{ 'user <handle> auth (<type>|all) purge', 'delete all credentials that would allow this user to log in (where possible)' };
						{ 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' };
						{ 'user <handle> emasculate', 'strip all administrative powers and rank from a user' };
						{ 'user <handle> forgive', 'restore all default powers to a user' };
						{ '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'};
					}) ]
					return 1
				end


				var handle = umode.arglist(0)
				var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)})
				if umode.arglist.ct == 2 and lib.str.cmp(umode.arglist(1),'create')==0 then
					if usr:ref() then lib.bail('that user already exists') end
					if not lib.store.actor.handle_validate(handle) then
						lib.bail('invalid user handle') end
					var kbuf: uint8[lib.crypt.const.maxdersz]
					var na = lib.store.actor.mk(&kbuf[0])
					na.handle = handle
					dlg:actor_create(&na)
					lib.report('created new user @',na.handle,'; assign credentials to enable login')
				elseif umode.arglist.ct >= 3 then
					var grant = lib.str.cmp(umode.arglist(1),'grant') == 0
					if not usr then lib.bail('no such user') end
					if grant or lib.str.cmp(umode.arglist(1),'revoke') == 0 then
						var newprivs = usr.ptr.rights.powers
						var map = array([lib.store.privmap])
						if umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(2),'all') == 0 then
							if grant
								then newprivs:fill()
								else newprivs:clear()
							end

Modified parsav.t from [4b4b2876e4] to [2bbe093dad].

394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
	'http', 'html', 'session', 'tpl', 'store', 'acl';

	'smackdown'; -- md-alike parser
}

local be = {}
for _, b in pairs(config.backends) do
	be[#be+1] = terralib.loadfile('backend/' .. b .. '.t')()
end
lib.store.backends = global(`array([be]))

lib.cmdparse = terralib.loadfile('cmdparse.t')()

do local collate = function(path,f, ...)
	return loadfile(path..'/'..f..'.lua')(path, ...)







|







394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
	'http', 'html', 'session', 'tpl', 'store', 'acl';

	'smackdown'; -- md-alike parser
}

local be = {}
for _, b in pairs(config.backends) do
	be[#be+1] = terralib.loadfile(string.format('backend/%s.t',b))()
end
lib.store.backends = global(`array([be]))

lib.cmdparse = terralib.loadfile('cmdparse.t')()

do local collate = function(path,f, ...)
	return loadfile(path..'/'..f..'.lua')(path, ...)

Modified render/conf.t from [fa178f73b5] to [60d6b764a8].

11
12
13
14
15
16
17


18
19
20
21
22
23
24
..
41
42
43
44
45
46
47
48
49
50





51
52
53
54
55
56
57
58
	{url = 'qnt', title = 'quarantine', render = 'quarantine'};
	{url = 'acl', title = 'access control shortcuts', render = 'acl'};
	{url = 'rooms', title = 'chatrooms', render = 'rooms'};
	{url = 'circles', title = 'circles', render = 'circles'};

	{url = 'srv', title = 'server settings', render = 'srv'};
	{url = 'brand', title = 'instance branding', render = 'rebrand'};


	{url = 'censor', title = 'censorship &amp; badthink suppression', render = 'rebrand'};
	{url = 'users', title = 'user accounting', render = 'users'};

}

local path = symbol(lib.mem.ptr(pref))
local co = symbol(&lib.srv.convo)
................................................................................

local terra 
render_conf([co], [path], notify: pstr)
	var menu: lib.str.acc menu:init(64):lpush('<hr>') defer menu:free()

	-- build menu
	do var p = co.who.rights.powers
		if p.config() then menu:lpush '<a href="/conf/srv">server settings</a>' end
		if p.rebrand() then menu:lpush '<a href="/conf/brand">instance branding</a>' end
		if p.censor() then menu:lpush '<a href="/conf/censor">badthink alerts</a>' end





		if p:affect_users() then menu:lpush '<a href="/conf/users">users</a>' end
	end

	-- select the appropriate panel
	var [panel] = pstr { ptr = ''; ct = 0 }
	if path.ct >= 2 then [invoker] end

	-- avoid the hr if we didn't add any elements







>
>







 







|
<

>
>
>
>
>
|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
..
43
44
45
46
47
48
49
50

51
52
53
54
55
56
57
58
59
60
61
62
63
64
	{url = 'qnt', title = 'quarantine', render = 'quarantine'};
	{url = 'acl', title = 'access control shortcuts', render = 'acl'};
	{url = 'rooms', title = 'chatrooms', render = 'rooms'};
	{url = 'circles', title = 'circles', render = 'circles'};

	{url = 'srv', title = 'server settings', render = 'srv'};
	{url = 'brand', title = 'instance branding', render = 'rebrand'};
	{url = 'badge', title = 'user badges', render = 'badge'};
	{url = 'emoji', title = 'custom emoji packs', render = 'emojo'};
	{url = 'censor', title = 'censorship &amp; badthink suppression', render = 'rebrand'};
	{url = 'users', title = 'user accounting', render = 'users'};

}

local path = symbol(lib.mem.ptr(pref))
local co = symbol(&lib.srv.convo)
................................................................................

local terra 
render_conf([co], [path], notify: pstr)
	var menu: lib.str.acc menu:init(64):lpush('<hr>') defer menu:free()

	-- build menu
	do var p = co.who.rights.powers
		if p:affect_users() then menu:lpush '<a href="/conf/users">users</a>' end

		if p.censor() then menu:lpush '<a href="/conf/censor">badthink alerts</a>' end
		if p.config() then menu:lpush([
			'<a href="/conf/srv">server &amp; policy</a>' ..
			'<a href="/conf/badge">badges</a>' ..
			'<a href="/conf/emoji">emoji packs</a>'
		]) end
		if p.rebrand() then menu:lpush '<a href="/conf/brand">instance branding</a>' end
	end

	-- select the appropriate panel
	var [panel] = pstr { ptr = ''; ct = 0 }
	if path.ct >= 2 then [invoker] end

	-- avoid the hr if we didn't add any elements

Modified render/conf/users.t from [6e4ba75dd2] to [4494d99300].

1
2
3

4
5
6
7
8
9

































10
11
12

13

14



15
16
17















18













































19
20
21
22
23
24









25
26

27
28
29
30

31
32
33
34








35










































36
37

38
39
40
41
42
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)


local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 

































render_conf_users(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
	if path.ct == 2 then
		var uid, ok = lib.math.shorthand.parse(path(1).ptr,path(1).ct)

		var user = co.srv:actor_fetch_uid(uid)

		if not user then goto e404 end



		var islinkct = false
		var cinp: lib.str.acc
		var clnk: lib.str.acc clnk:compose('<hr>')





























































		var cinpp = cinp:finalize() defer cinpp:free()
		var clnkp: pstr
		if islinkct then clnkp = clnk:finalize() else
			clnk:free()
			clnkp = pstr { ptr='', ct=0 }
		end









		var pg = data.view.conf_user_ctl {
			name = cs(user(0).handle);

			inputcontent = cinpp;
			linkcontent = clnkp;
		}
		var ret = pg:tostr()

		if islinkct then clnkp:free() end
		return ret
	else









	end










































	do return pstr.null() end
	::e404:: co:complain(404, 'not found', 'there is no user or resource by that identifier on this server')


	do return pstr.null() end
end

return render_conf_users



>






>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|
|
>

>

>
>
>

|

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>






>
>
>
>
>
>
>
>
>

<
>




>



<
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|
>

|



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

134
135
136
137
138
139
140
141
142

143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)
local P = lib.str.plit

local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 
regalia(acc: &lib.str.acc, rank: uint16)
	switch rank do -- TODO customizability
		case [uint16](1) then acc:lpush('👑') end
		case [uint16](2) then acc:lpush('🔱') end
		case [uint16](3) then acc:lpush('⚜️') end
		case [uint16](4) then acc:lpush('🗡') end
		case [uint16](5) then acc:lpush('🗝') end
		else acc:lpush('🕴')
	end
end

local num_field = macro(function(acc,name,lbl,min,max,value)
	name = name:asvalue()
	lbl = lbl:asvalue()
	return quote
		var decbuf: int8[21]
	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)])
		:push(lib.math.decstr(min, &decbuf[20]),0)
		:lpush('" max="'):push(lib.math.decstr(max, &decbuf[20]),0)
		:lpush('" value="'):push(lib.math.decstr(value, &decbuf[20]),0):lpush('"></div>')
	end
end)

local terra 
push_checkbox(acc: &lib.str.acc, name: pstr, lbl: pstr, on: bool, enabled: bool)
	acc:lpush('<label><input type="checkbox" name="'):ppush(name):lpush('"')
	if on then acc:lpush(' checked') end
	if not enabled then acc:lpush(' disabled') end
	acc:lpush('> '):ppush(lbl):lpush('</label>')
end

local mode_local, mode_remote, mode_staff, mode_peers, mode_peons, mode_all = 0,1,2,3,4,5
local terra 
render_conf_users(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
	if path.ct == 3 then
		var uid, ok = lib.math.shorthand.parse(path(2).ptr,path(2).ct)
		if not ok then goto e404 end
		var user = co.srv:actor_fetch_uid(uid)
		-- FIXME allow xids as well, for manual queries
		if not user then goto e404 end
		defer user:free()
		if not co.who:overpowers(user.ptr) then goto e403 end

		var islinkct = false
		var cinp: lib.str.acc cinp:init(128)
		var clnk: lib.str.acc clnk:compose('<hr>')
		cinp:lpush('<div class="elem-group">')
		if co.who.rights.powers.herald() then
			var sanitized: pstr
			if user.ptr.epithet == nil
				then sanitized = pstr {ptr='', ct=0}
				else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true)
			end
			cinp:lpush('<div class="elem"><label for="epithet">epithet</label><input type="text" id="epithet" name="epithet" value="'):ppush(sanitized):lpush('"></div>')
			if user.ptr.epithet ~= nil then sanitized:free() end
		end
		if user.ptr.rights.rank > 0 and (co.who.rights.powers.elevate() or co.who.rights.powers.demote()) then
			var max = co.who.rights.rank
			if not co.who.rights.powers.elevate() then max = user.ptr.rights.rank end
			var min = co.srv.cfg.nranks
			if not co.who.rights.powers.demote() then min = user.ptr.rights.rank end

			num_field(cinp, 'rank', 'rank', max, min, user.ptr.rights.rank)
		end
		if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then
			var min = 0
			if not (co.who.rights.powers.discipline() or
				co.who.rights.powers.demote() and co.who.rights.powers.invite())
					then min = user.ptr.rights.invites end
			var max = co.srv.cfg.maxinvites
			if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end

			num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites)
		end
		cinp:lpush('</div><div class="check-panel">')

		if (user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or
		   (user.ptr.rights.rank >  0 and co.who.rights.powers.demote()) then
			push_checkbox(&cinp, 'staff', 'site staff member', user.ptr.rights.rank > 0, true)
		end

		cinp:lpush('</div>')

		if co.who.rights.powers.elevate() or
		   co.who.rights.powers.demote() then
			var map = array([lib.store.privmap])
			cinp:lpush('<label>powers</label><div class="check-panel">')
				for i=0, [map.type.N] do
					if (co.who.rights.powers and map[i].priv) == map[i].priv then
						var name: int8[64]
						var on = (user.ptr.rights.powers and map[i].priv) == map[i].priv
						var enabled = (on and co.who.rights.powers.demote()) or
									  ((not on) and co.who.rights.powers.elevate())
						lib.str.cpy(&name[0], 'allow-')
						lib.str.ncpy(&name[6], map[i].name.ptr, map[i].name.ct)
						push_checkbox(&cinp, pstr{ptr=&name[0],ct=map[i].name.ct+6},
							map[i].name, on, enabled)
					end
				end
			cinp:lpush('</div>')
		end

		-- TODO black mark system? e.g. resolution option for badthink reports
		-- adds a black mark to the offending user; they can be automatically banned
		-- or brought up for review after a certain number of offenses; possibly lower
		-- set of default privs for marked users

		var cinpp = cinp:finalize() defer cinpp:free()
		var clnkp: pstr
		if islinkct then clnkp = clnk:finalize() else
			clnk:free()
			clnkp = pstr { ptr='', ct=0 }
		end
		var unym: lib.str.acc unym:init(64)
		unym:lpush('<a href="/')
		if user(0).origin ~= 0 then unym:lpush('@') end
		do var sanxid = lib.html.sanitize(user(0).xid, true)
			unym:ppush(sanxid)
			sanxid:free() end
		unym:lpush('" class="id">')
		lib.render.nym(user.ptr,0,&unym)
		unym:lpush('</a>')
		var pg = data.view.conf_user_ctl {

			name = unym:finalize();
			inputcontent = cinpp;
			linkcontent = clnkp;
		}
		var ret = pg:tostr()
		pg.name:free()
		if islinkct then clnkp:free() end
		return ret
	else

		var modes = array(P'local', P'remote', P'staff', P'titled', P'peons', P'all')
		var idbuf: int8[lib.math.shorthand.maxlen]
		var ulst: lib.str.acc ulst:init(256)
		var mode: uint8 = mode_local
		var modestr = co:pgetv('show')
		ulst:lpush('<div style="text-align: right"><em>showing ')
		for i=0,[modes.type.N] do
			if modestr:ref() and modes[i]:cmp(modestr) then mode = i end
		end
		for i=0,[modes.type.N] do
			if i > 0 then ulst:lpush(' · ') end
			if mode == i then
				ulst:lpush('<strong>'):ppush(modes[i]):lpush('</strong>')
			else
				ulst:lpush('<a href="?show='):ppush(modes[i]):lpush('">')
					:ppush(modes[i]):lpush('</a>')
			end
		end
		var users: lib.mem.lstptr(lib.store.actor)
		if mode == mode_local then
			users = co.srv:actor_enum_local()
		else
			users = co.srv:actor_enum()
		end
		ulst:lpush('</em></div>')
		ulst:lpush('<ul class="user-list">')
		for i=0,users.ct do var usr = users(i).ptr
			if mode == mode_staff and usr.rights.rank == 0 then goto skip
			elseif mode == mode_peons and usr.rights.rank ~= 0 then goto skip
			elseif mode == mode_remote and usr.origin == 0 then goto skip 
			elseif mode == mode_peers and usr.epithet == nil then goto skip end
			var idlen = lib.math.shorthand.gen(usr.id, &idbuf[0])
			ulst:lpush('<li>')
			if usr.rights.rank ~= 0 then
				ulst:lpush('<span class="regalia">')
				regalia(&ulst, usr.rights.rank)
				ulst:lpush('</span>')
			end
			if co.who:overpowers(usr) then
				ulst:lpush('<a class="id" href="users/'):push(&idbuf[0],idlen):lpush('">')
				lib.render.nym(usr, 0, &ulst)
				ulst:lpush('</a></li>')
			else
				ulst:lpush('<span class="id">')
				lib.render.nym(usr, 0, &ulst)
				ulst:lpush('</span></li>')
			end
		::skip::end
		ulst:lpush('</ul>')
		return ulst:finalize()
	end
	do return pstr.null() end
	::e404:: co:complain(404, 'not found', 'there is no user or resource by that identifier on this server') goto quit
	::e403:: co:complain(403, 'forbidden', 'you do not have sufficient authority to control that resource')

	::quit:: return pstr.null()
end

return render_conf_users

Modified render/nym.t from [0d2437aadd] to [74775ce158].

1
2
3
4
5
6
7
8




9

10
11
12

13
14
15
16
17
18
19
20
21
22
23
24
25

26

27
28
29
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 
render_nym(who: &lib.store.actor, scope: uint64)




	var n: lib.str.acc n:init(128)

	var xidsan = lib.html.sanitize(cs(who.xid),false)
	if who.nym ~= nil and who.nym[0] ~= 0 then
		var nymsan = lib.html.sanitize(cs(who.nym),false)

		n:compose('<span class="nym">',nymsan,'</span> [<span class="handle">',
			xidsan,'</span>]')
		nymsan:free()
	else n:compose('<span class="handle">',xidsan,'</span>') end
	xidsan:free()

	if who.epithet ~= nil then
		var episan = lib.html.sanitize(cs(who.epithet),false)
		n:lpush(' <span class="epithet">'):ppush(episan):lpush('</span>')
		episan:free()
	end
	
	-- TODO: if scope == chat room then lookup titles in room member db

	return n:finalize()

end

return render_nym

|





|
>
>
>
>
|
>



>
|
|

|




|




>
|
>



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-- vim: ft=terra
local pstr = lib.str.t
local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 
render_nym(who: &lib.store.actor, scope: uint64, tgt: &lib.str.acc)
	var acc: lib.str.acc
	var n: &lib.str.acc
	if tgt ~= nil then n = tgt else
		n = &acc
		n:init(128)
	end
	var xidsan = lib.html.sanitize(cs(who.xid),false)
	if who.nym ~= nil and who.nym[0] ~= 0 then
		var nymsan = lib.html.sanitize(cs(who.nym),false)
		n:lpush('<span class="nym">'):ppush(nymsan)
			:lpush('</span> [<span class="handle">'):ppush(xidsan)
			:lpush('</span>]')
		nymsan:free()
	else n:lpush('<span class="handle">'):ppush(xidsan):lpush('</span>') end
	xidsan:free()

	if who.epithet ~= nil then
		var episan = lib.html.sanitize(cs(who.epithet),false)
		n:lpush('<span class="epithet">'):ppush(episan):lpush('</span>')
		episan:free()
	end
	
	-- TODO: if scope == chat room then lookup titles in room member db
	if tgt == nil then
		return n:finalize()
	else return pstr.null() end
end

return render_nym

Modified render/profile.t from [5ac1497f7a] to [5d5ed1c86e].

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42











43
44
45
46
47
48
49
50
51
52
53


54
55
56

57
58
59
60
61

62
63
64
65
local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 
render_profile(co: &lib.srv.convo, actor: &lib.store.actor)
	var aux: lib.str.acc
	var followed = true -- FIXME
	if co.aid ~= 0 and co.who.id == actor.id then
		aux:compose('<a class="button" href="/conf/profile?go=/',actor.xid,'">alter</a>')
	elseif co.aid ~= 0 then
		if not followed then
			aux:compose('<button method="post" name="act" value="follow">follow</a>')
		elseif not followed then
			aux:compose('<button method="post" name="act" value="unfollow">unfollow</a>')
		end
		aux:lpush('<a href="/'):push(actor.xid,0):lpush('/chat">chat</a>')
		if co.who.rights.powers:affect_users() then
			aux:lpush('<a class="button" href="/'):push(actor.xid,0):lpush('/ctl">control</a>')
		end
	else
		aux:compose('<a class="button" href="/', actor.xid, '/follow">remote follow</a>')
	end
	var auxp = aux:finalize()
	var avistr: lib.str.acc if actor.origin == 0 then
		avistr:compose('/avi/',actor.handle)
	end
	var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, &timestr[0])

	var strfbuf: int8[28*4]
	var stats = co.srv:actor_stats(actor.id)
		var sn_posts     = cs(lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ]))
		var sn_follows   = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1))
		var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1))
		var sn_mutuals   = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1))
	var bio = lib.str.plit "<em>tall, dark, and mysterious</em>"
	if actor.bio ~= nil then
		bio = lib.smackdown.html(cs(actor.bio))
	end
	var fullname = lib.render.nym(actor,0) defer fullname:free()











	var profile = data.view.profile {
		nym = fullname;
		bio = bio;
		xid = cs(actor.xid);
		avatar = lib.trn(actor.origin == 0, pstr{ptr=avistr.buf,ct=avistr.sz},
			cs(lib.coalesce(actor.avatar, '/s/default-avatar.svg')));

		nposts = sn_posts, nfollows = sn_follows;
		nfollowers = sn_followers, nmutuals = sn_mutuals;
		tweetday = cs(timestr);
		timephrase = lib.trn(actor.origin == 0, lib.str.plit'joined', lib.str.plit'known since');



		auxbtn = auxp;
	}


	var ret = profile:tostr()
	if actor.origin == 0 then avistr:free() end
	auxp:free() 
	if actor.bio ~= nil then bio:free() end

	return ret
end

return render_profile







|

|


|
|
|

|
|






<
<
<












|
>
>
>
>
>
>
>
>
>
>
>




|
<





>
>



>


<


>




3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

56
57
58
59
60
61
62
63
64
65
66
67
68

69
70
71
72
73
74
75
local terra cs(s: rawstring)
	return pstr { ptr = s, ct = lib.str.sz(s) }
end

local terra 
render_profile(co: &lib.srv.convo, actor: &lib.store.actor)
	var aux: lib.str.acc
	var followed = false -- FIXME
	if co.aid ~= 0 and co.who.id == actor.id then
		aux:compose('<a class="button" href="/conf/profile?go=/@',actor.handle,'">alter</a>')
	elseif co.aid ~= 0 then
		if not followed then
			aux:compose('<button method="post" class="pos" name="act" value="follow">follow</button>')
		elseif followed then
			aux:compose('<button method="post" class="neg" name="act" value="unfollow">unfollow</button>')
		end
		aux:lpush('<a class="button" href="/'):push(actor.xid,0):lpush('/chat">chat</a>')
		if co.who.rights.powers:affect_users() and co.who:overpowers(actor) then
			aux:lpush('<a class="button" href="/'):push(actor.xid,0):lpush('/ctl">control</a>')
		end
	else
		aux:compose('<a class="button" href="/', actor.xid, '/follow">remote follow</a>')
	end
	var auxp = aux:finalize()



	var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, &timestr[0])

	var strfbuf: int8[28*4]
	var stats = co.srv:actor_stats(actor.id)
		var sn_posts     = cs(lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ]))
		var sn_follows   = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1))
		var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1))
		var sn_mutuals   = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1))
	var bio = lib.str.plit "<em>tall, dark, and mysterious</em>"
	if actor.bio ~= nil then
		bio = lib.smackdown.html(cs(actor.bio))
	end
	var fullname = lib.render.nym(actor,0,nil) defer fullname:free()
	var comments: lib.str.acc comments:init(64)
	-- this is really more what epithets are for, i think
	--if actor.rights.rank > 0 then comments:lpush('<li>staff member</li>') end
	if co.aid ~= 0 and actor.rights.rank ~= 0 then
		if co.who:outranks(actor) then
			comments:lpush('<li style="--co:50">underling</li>')
		elseif actor:outranks(co.who) then
			comments:lpush('<li style="--co:-50">outranks you</li>')
		end
	end

	var profile = data.view.profile {
		nym = fullname;
		bio = bio;
		xid = cs(actor.xid);
		avatar = cs(actor.avatar);


		nposts = sn_posts, nfollows = sn_follows;
		nfollowers = sn_followers, nmutuals = sn_mutuals;
		tweetday = cs(timestr);
		timephrase = lib.trn(actor.origin == 0, lib.str.plit'joined', lib.str.plit'known since');

		remarks = '';

		auxbtn = auxp;
	}
	if comments.sz > 0 then profile.remarks = comments:finalize() end

	var ret = profile:tostr()

	auxp:free() 
	if actor.bio ~= nil then bio:free() end
	if comments.sz > 0 then profile.remarks:free() end
	return ret
end

return render_profile

Modified render/tweet.t from [2b64155fcc] to [43aca48007].

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
	var timestr: int8[26] lib.osclock.ctime_r(&p.posted, &timestr[0])

	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) defer bhtml:free()

	var idbuf: int8[lib.math.shorthand.maxlen]
	var idlen = lib.math.shorthand.gen(p.id, idbuf)
	var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen})
	var fullname = lib.render.nym(author,0) defer fullname:free()
	var tpl = data.view.tweet {
		text = bhtml;
		subject = cs(lib.coalesce(p.subject,''));
		nym = fullname;
		when = cs(&timestr[0]);
		avatar = cs(lib.trn(author.origin == 0, avistr.buf,
			lib.coalesce(author.avatar, '/s/default-avatar.svg')));
		acctlink = cs(author.xid);
		permalink = permalink:finalize();
		attr = ''
	}

	var attrbuf: int8[32]
	if p.accent ~= -1 and p.accent ~= co.ui_hue then







|





|
<







22
23
24
25
26
27
28
29
30
31
32
33
34
35

36
37
38
39
40
41
42
	var timestr: int8[26] lib.osclock.ctime_r(&p.posted, &timestr[0])

	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) defer bhtml:free()

	var idbuf: int8[lib.math.shorthand.maxlen]
	var idlen = lib.math.shorthand.gen(p.id, idbuf)
	var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen})
	var fullname = lib.render.nym(author,0,nil) defer fullname:free()
	var tpl = data.view.tweet {
		text = bhtml;
		subject = cs(lib.coalesce(p.subject,''));
		nym = fullname;
		when = cs(&timestr[0]);
		avatar = cs(author.avatar);

		acctlink = cs(author.xid);
		permalink = permalink:finalize();
		attr = ''
	}

	var attrbuf: int8[32]
	if p.accent ~= -1 and p.accent ~= co.ui_hue then

Modified route.t from [35b3cb0b8a] to [2f7668c3df].

245
246
247
248
249
250
251






















252
253
254
255
256
257
258
...
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300






301
302
303
304
305
306
307

	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
end

terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
	var msg = pstring.null()






















	if meth == method.post and path.ct >= 1 then
		var user_refresh = false var fail = false
		if path(1):cmp(lib.str.lit 'profile') then
			lib.dbg('updating profile')
			co.who.bio = co:postv('bio')._0
			co.who.nym = co:postv('nym')._0
			if co.who.bio ~= nil and @co.who.bio == 0 then co.who.bio = nil end
................................................................................
			if resethue then
				co.srv:actor_conf_int_reset(co.who.id, 'ui-accent')
				co.ui_hue = co.srv.cfg.ui_hue
			end

			msg = lib.str.plit 'profile changes saved'
			--user_refresh = true -- not really necessary here, actually
		elseif path(1):cmp(lib.str.lit 'srv') then
			if not co.who.rights.powers.config() then goto nopriv end
		elseif path(1):cmp(lib.str.lit 'brand') then
			if not co.who.rights.powers.rebrand() then goto nopriv end
		elseif path(1):cmp(lib.str.lit 'users') then
			if not co.who.rights.powers:affect_users() then goto nopriv end

		elseif path(1):cmp(lib.str.lit 'sec') then
			var act = co:ppostv('act')
			if act:cmp(lib.str.plit 'invalidate') then
				lib.dbg('setting user\'s cookie validation time to now')
				co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil))
				-- 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
				co:installkey('/conf/sec',co.aid)
				return






			end
		end

		if user_refresh then -- refresh the user info for the renderer
			var usr = co.srv:actor_fetch_uid(co.who.id)
			lib.mem.heapf(co.who)
			co.who = usr.ptr







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







<
<
<
<
<
<









>
>
>
>
>
>







245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
...
301
302
303
304
305
306
307






308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329

	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
end

terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
	var msg = pstring.null()
	-- first things first, do priv checks
	if path.ct >= 1 then
		if not co.who.rights.powers.config() and (
			path(1):cmp(lib.str.lit 'srv')   or
			path(1):cmp(lib.str.lit 'badge') or
			path(1):cmp(lib.str.lit 'emoji')
		) then goto nopriv

		elseif not co.who.rights.powers.rebrand() and (
			path(1):cmp(lib.str.lit 'brand')
		) then goto nopriv

		elseif not co.who.rights.powers.acct() and (
			path(1):cmp(lib.str.lit 'profile') or
			path(1):cmp(lib.str.lit 'acct')
		) then goto nopriv

		elseif not co.who.rights.powers:affect_users() and (
			path(1):cmp(lib.str.lit 'users')
		) then goto nopriv end
	end

	if meth == method.post and path.ct >= 1 then
		var user_refresh = false var fail = false
		if path(1):cmp(lib.str.lit 'profile') then
			lib.dbg('updating profile')
			co.who.bio = co:postv('bio')._0
			co.who.nym = co:postv('nym')._0
			if co.who.bio ~= nil and @co.who.bio == 0 then co.who.bio = nil end
................................................................................
			if resethue then
				co.srv:actor_conf_int_reset(co.who.id, 'ui-accent')
				co.ui_hue = co.srv.cfg.ui_hue
			end

			msg = lib.str.plit 'profile changes saved'
			--user_refresh = true -- not really necessary here, actually







		elseif path(1):cmp(lib.str.lit 'sec') then
			var act = co:ppostv('act')
			if act:cmp(lib.str.plit 'invalidate') then
				lib.dbg('setting user\'s cookie validation time to now')
				co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil))
				-- 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
				co:installkey('/conf/sec',co.aid)
				return
			end
		elseif path(1):cmp(lib.str.lit 'users') and path.ct >= 2 then
			var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct)
			if ok then
				var usr = co.srv:actor_fetch_uid(userid) defer usr:free()
				if not co.who:overpowers(usr.ptr) then goto nopriv end
			end
		end

		if user_refresh then -- refresh the user info for the renderer
			var usr = co.srv:actor_fetch_uid(co.who.id)
			lib.mem.heapf(co.who)
			co.who = usr.ptr

Modified srv.t from [b092edff32] to [6be667433b].

8
9
10
11
12
13
14


15
16
17
18
19
20
21
...
644
645
646
647
648
649
650

651
652
653
654
655
656
657
...
723
724
725
726
727
728
729





























730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
...
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
	pol_sec: secmode.t
	pol_reg: bool
	credmgd: bool
	maxupsz: intptr
	instance: lib.mem.ptr(int8)
	overlord: &srv
	ui_hue: uint16


}
local struct srv {
	sources: lib.mem.ptr(lib.store.source)
	webmgr: lib.net.mg_mgr
	webcon: &lib.net.mg_connection
	cfg: cfgcache
	id: rawstring
................................................................................
		   self.sources(i).backend.actor_auth_pw ~= nil then
			var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw)
			if aid ~= 0 then
				if uid == 0 then
					lib.dbg('new user just logged in, creating account entry')
					var kbuf: uint8[lib.crypt.const.maxdersz]
					var na = lib.store.actor.mk(&kbuf[0])

					var newuid: uint64
					if self.sources(i).backend.actor_create ~= nil then
						newuid = self.sources(i):actor_create(&na)
					else newuid = self:actor_create(&na) end

					if self.sources(i).backend.actor_auth_register_uid ~= nil then
						self.sources(i):actor_auth_register_uid(aid,newuid)
................................................................................
	lib.net.mg_mgr_free(&self.webmgr)
	for i=0,self.sources.ct do var src = self.sources.ptr + i
		lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')')
		src:close()
	end
	self.sources:free()
end






























terra cfgcache:load()
	self.instance = self.overlord:conf_get('instance-name')
	self.secret = self.overlord:conf_get('server-secret')

	do self.pol_reg = false
	var sreg = self.overlord:conf_get('policy-self-register')
	if sreg:ref() then
		if lib.str.cmp(sreg.ptr, 'on') == 0
			then self.pol_reg = true
			else self.pol_reg = false
		end
		sreg:free()
	end end

	do self.credmgd = false
	var sreg = self.overlord:conf_get('credential-store')
	if sreg:ref() then
		if lib.str.cmp(sreg.ptr, 'managed') == 0
			then self.credmgd = true
			else self.credmgd = false
................................................................................
			self.pol_sec = secmode.lockdown
		elseif lib.str.cmp(smode.ptr, 'isolate') == 0 then
			self.pol_sec = secmode.isolate
		end
		smode:free()
	end

	self.ui_hue = config.default_ui_accent
	var shue = self.overlord:conf_get('ui-accent')
	if shue.ptr ~= nil then
		var hue,ok = lib.math.decparse(shue)
		if ok then self.ui_hue = hue end
		shue:free()
	end
end

return {
	overlord = srv;
	convo = convo;
	route = route;
	secmode = secmode;
}







>
>







 







>







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>





<
|
<
<
<
<
<
<
<







 







|
|
|
<
<
<
<








8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
...
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766

767







768
769
770
771
772
773
774
...
797
798
799
800
801
802
803
804
805
806




807
808
809
810
811
812
813
814
	pol_sec: secmode.t
	pol_reg: bool
	credmgd: bool
	maxupsz: intptr
	instance: lib.mem.ptr(int8)
	overlord: &srv
	ui_hue: uint16
	nranks: uint16
	maxinvites: uint16
}
local struct srv {
	sources: lib.mem.ptr(lib.store.source)
	webmgr: lib.net.mg_mgr
	webcon: &lib.net.mg_connection
	cfg: cfgcache
	id: rawstring
................................................................................
		   self.sources(i).backend.actor_auth_pw ~= nil then
			var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw)
			if aid ~= 0 then
				if uid == 0 then
					lib.dbg('new user just logged in, creating account entry')
					var kbuf: uint8[lib.crypt.const.maxdersz]
					var na = lib.store.actor.mk(&kbuf[0])
					na.handle = newhnd.ptr
					var newuid: uint64
					if self.sources(i).backend.actor_create ~= nil then
						newuid = self.sources(i):actor_create(&na)
					else newuid = self:actor_create(&na) end

					if self.sources(i).backend.actor_auth_register_uid ~= nil then
						self.sources(i):actor_auth_register_uid(aid,newuid)
................................................................................
	lib.net.mg_mgr_free(&self.webmgr)
	for i=0,self.sources.ct do var src = self.sources.ptr + i
		lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')')
		src:close()
	end
	self.sources:free()
end

terra cfgcache:cfint(name: rawstring, default: intptr)
	var str = self.overlord:conf_get(name)
	if str.ptr ~= nil then
		var i,ok = lib.math.decparse(str)
		if ok then default = i else
			lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected integer; using default value instead')
		end
		str:free()
	end
	return default
end

terra cfgcache:cfbool(name: rawstring, default: bool)
	var str = self.overlord:conf_get(name)
	if str.ptr ~= nil then
		if str:cmp(lib.str.plit 'true') or str:cmp(lib.str.plit 'on') or
		   str:cmp(lib.str.plit 'yes')  or str:cmp(lib.str.plit '1') then
			default = true
		elseif str:cmp(lib.str.plit 'false') or str:cmp(lib.str.plit 'off') or
		       str:cmp(lib.str.plit 'no')    or str:cmp(lib.str.plit '0') then
			default = false
		else
			lib.warn('invalid configuration setting ',name,'="',{str.ptr,str.ct},'", expected boolean; using default value instead')
		end
		str:free()
	end
	return default
end

terra cfgcache:load()
	self.instance = self.overlord:conf_get('instance-name')
	self.secret = self.overlord:conf_get('server-secret')


	self.pol_reg = self:cfbool('policy-self-register', false)








	do self.credmgd = false
	var sreg = self.overlord:conf_get('credential-store')
	if sreg:ref() then
		if lib.str.cmp(sreg.ptr, 'managed') == 0
			then self.credmgd = true
			else self.credmgd = false
................................................................................
			self.pol_sec = secmode.lockdown
		elseif lib.str.cmp(smode.ptr, 'isolate') == 0 then
			self.pol_sec = secmode.isolate
		end
		smode:free()
	end

	self.ui_hue = self:cfint('ui-accent',config.default_ui_accent)
	self.nranks = self:cfint('user-ranks',10)
	self.maxinvites = self:cfint('max-invites',64)




end

return {
	overlord = srv;
	convo = convo;
	route = route;
	secmode = secmode;
}

Modified static/style.scss from [a256539ae3] to [f40a20016f].

1
2
3
4
5
6
7
8

9
10
11
12
13
14
15
16
17
18
..
24
25
26
27
28
29
30



31
32
33
34
35
36
37
..
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
...
236
237
238
239
240
241
242













243
244
245
246
247
248
249
...
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286




287
288
289
290
291
292
293
...
606
607
608
609
610
611
612
613











614
615
616
617
618
619
620
...
713
714
715
716
717
718
719



































$default-color: hsl(323,100%,65%);
%sans { font-family: "Alegreya Sans", "Open Sans", sans-serif; }
%serif { font-family: Alegreya, GaramondNo8, "Garamond Premier Pro", "Adobe Garamond", Garamond, Junicode, serif; }
%teletype { font-family: "Inconsolata LGC", Inconsolata, monospace; font-size: 12pt !important; }

// @function tone($pct, $alpha: 0) { @return adjust-color($color, $lightness: $pct, $alpha: $alpha) }
@function tone($pct, $alpha: 0) {
 @return hsla(var(--hue), 100%, 65% + $pct, 1 + $alpha)

}

:root { --hue: 323; }
body {
	@extend %sans;
	background-color: tone(-55%);
	color: tone(25%);
	font-size: 14pt;
	margin: 0;
	padding: 0;
................................................................................
::placeholder {
	color: tone(0,-0.3);
	font-style: italic;
}
a[href] {
	color: tone(10%);
	text-decoration-color: tone(10%,-0.5);



	&:hover, &:focus {
		color: white;
		text-shadow: 0 0 15px tone(20%);
		text-decoration-color: tone(10%,-0.1);
		outline: none;
	}
	&.button { @extend %button; }
................................................................................

%button {
	@extend %sans;
	font-size: 14pt;
	box-sizing: border-box;
	padding: 0.1in 0.2in;
	border: 1px solid black;
	color: tone(25%);
	text-shadow: 1px 1px black;
	text-decoration: none;
	text-align: center;
	cursor: default;
	user-select: none;
	-webkit-user-drag: none;
	-webkit-app-region: no-drag;
	background: linear-gradient(to bottom,
		tone(-47%),
		tone(-50%) 15%,
		tone(-50%) 75%,
		tone(-53%)
	);
	&:hover, &:focus {
		@extend %glow;
		outline: none;
		color: tone(-55%);
		text-shadow: none;
		background: linear-gradient(to bottom,
			tone(-27%),
			tone(-30%) 15%,
			tone(-30%) 75%,
			tone(-35%)
		);
	}
	&:active {
		color: black;
		padding-bottom: calc(0.1in - 2px);
		padding-top: calc(0.1in + 2px);
		background: linear-gradient(to top,
			tone(-25%),
			tone(-30%) 15%,
			tone(-30%) 75%,
			tone(-35%)
		);
	}
}

button { @extend %button;
	&:first-of-type {
		@extend %button;
		color: white;
		box-shadow: inset 0 1px  tone(-25%),
		            inset 0 -1px tone(-50%);
		background: linear-gradient(to bottom,
			tone(-35%),
			tone(-40%) 15%,
			tone(-40%) 75%,
			tone(-45%)
		);
		&:hover, &:focus {
			box-shadow: inset 0 1px  tone(-15%),
						inset 0 -1px tone(-40%);
		}
		&:active {
			box-shadow: inset 0 1px  tone(-50%),
						inset 0 -1px tone(-25%);
			background: linear-gradient(to top,
				tone(-30%),
				tone(-35%) 15%,
				tone(-35%) 75%,
				tone(-40%)
			);
		}
	}
	&:hover { font-weight: bold; }
}

$grad-ui-focus: linear-gradient(to bottom,
	tone(-50%),
	tone(-35%)
);

input[type='text'], input[type='password'], textarea, select {
	@extend %serif;
	padding: 0.08in 0.1in;
	box-sizing: border-box;
	border: 1px solid black;
	background: linear-gradient(to bottom, tone(-55%), tone(-40%));
	font-size: 16pt;
	color: tone(25%);
................................................................................
	padding-bottom: 0.1in;
	background-color: tone(-45%,-0.3);
	border: {
		left: 1px solid black;
		right: 1px solid black;
	}
}














div.profile {
	padding: 0.1in;
	position: relative;
	display: grid;
	margin-bottom: 0.4in;
	grid-template-columns: 2fr 1fr;
................................................................................
			grid-column: 1 / 2;
			grid-row: 1 / 3;
			border: 1px solid black;
		}
		> .id {
			grid-column: 2 / 3;
			grid-row: 1 / 2;
			color: tone(25%,-0.4);
			> .nym {
				font-weight: bold;
				color: tone(25%);
			}
			> .xid {
				color: tone(20%,-0.1);
				font-size: 80%;
				vertical-align: text-top;
			}
		}
		> .bio {
			grid-column: 2 / 3;
			grid-row: 2 / 3;
		}
	}
	> .stats {
		grid-column: 3 / 4;
		grid-row: 1 / 3;




	}
	> form.actions {
		grid-column: 1 / 3; grid-row: 2 / 3;
		padding-top: 0.075in;
		flex-wrap: wrap;
		display: flex;
		justify-content: center;
................................................................................
		}
		input, textarea, .txtbox {
			display: block;
			width: 100%;
		}
		textarea { resize: vertical; min-height: 2in; }
	}
	.elem + %button { margin-left: 50%; width: 50%; }











}

menu.choice {
	display: flex;
	&.horizontal {
		flex-flow: row-reverse wrap;
		justify-content: space-evenly;
................................................................................

.color-picker {
	/* implemented using javascript, alas */
	@extend %box;
	label { text-shadow: 1px 1px black; }
	padding: 0.1in;
}









































|
|
>
|
<
|







 







>
>
>







 







|








|
|
|
|







|
|
|
|







|
|
|
|








|
|

|
|
|
|


|
|


|
|

|
|
|
|



|







|







 







>
>
>
>
>
>
>
>
>
>
>
>
>







 







<
<
<
<
<
<
<
<
<
<









>
>
>
>







 







|
>
>
>
>
>
>
>
>
>
>
>







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17
18
..
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
..
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
...
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
...
277
278
279
280
281
282
283










284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
...
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
...
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
$default-color: hsl(323,100%,65%);
%sans { font-family: "Alegreya Sans", "Open Sans", sans-serif; }
%serif { font-family: Alegreya, GaramondNo8, "Garamond Premier Pro", "Adobe Garamond", Garamond, Junicode, serif; }
%teletype { font-family: "Inconsolata LGC", Inconsolata, monospace; font-size: 12pt !important; }

// @function tone($pct, $alpha: 0) { @return adjust-color($color, $lightness: $pct, $alpha: $alpha) }
@function tone($pct, $alpha: 0)         { @return hsla(var(--hue),                   100%, 65% + $pct, 1 + $alpha) }
@function vtone($pct, $vary, $alpha: 0) { @return hsla(calc(var(--hue) + $vary),     100%, 65% + $pct, 1 + $alpha) }
@function otone($pct, $alpha: 0)        { @return hsla(calc(var(--hue) + var(--co)), 100%, 65% + $pct, 1 + $alpha) }


:root { --hue: 323; --co: 0; }
body {
	@extend %sans;
	background-color: tone(-55%);
	color: tone(25%);
	font-size: 14pt;
	margin: 0;
	padding: 0;
................................................................................
::placeholder {
	color: tone(0,-0.3);
	font-style: italic;
}
a[href] {
	color: tone(10%);
	text-decoration-color: tone(10%,-0.5);
	text-decoration-skip-ink: all;
	text-decoration-thickness: 1px;
	text-underline-offset: 0.1em;
	&:hover, &:focus {
		color: white;
		text-shadow: 0 0 15px tone(20%);
		text-decoration-color: tone(10%,-0.1);
		outline: none;
	}
	&.button { @extend %button; }
................................................................................

%button {
	@extend %sans;
	font-size: 14pt;
	box-sizing: border-box;
	padding: 0.1in 0.2in;
	border: 1px solid black;
	color: otone(25%);
	text-shadow: 1px 1px black;
	text-decoration: none;
	text-align: center;
	cursor: default;
	user-select: none;
	-webkit-user-drag: none;
	-webkit-app-region: no-drag;
	background: linear-gradient(to bottom,
		otone(-47%),
		otone(-50%) 15%,
		otone(-50%) 75%,
		otone(-53%)
	);
	&:hover, &:focus {
		@extend %glow;
		outline: none;
		color: tone(-55%);
		text-shadow: none;
		background: linear-gradient(to bottom,
			otone(-27%),
			otone(-30%) 15%,
			otone(-30%) 75%,
			otone(-35%)
		);
	}
	&:active {
		color: black;
		padding-bottom: calc(0.1in - 2px);
		padding-top: calc(0.1in + 2px);
		background: linear-gradient(to top,
			otone(-25%),
			otone(-30%) 15%,
			otone(-30%) 75%,
			otone(-35%)
		);
	}
}

button { @extend %button;
	&:first-of-type {
		@extend %button;
		color: white;
		box-shadow: inset 0 1px  otone(-25%),
		            inset 0 -1px otone(-50%);
		background: linear-gradient(to bottom,
			otone(-35%),
			otone(-40%) 15%,
			otone(-40%) 75%,
			otone(-45%)
		);
		&:hover, &:focus {
			box-shadow: inset 0 1px  otone(-15%),
						inset 0 -1px otone(-40%);
		}
		&:active {
			box-shadow: inset 0 1px  otone(-50%),
						inset 0 -1px otone(-25%);
			background: linear-gradient(to top,
				otone(-30%),
				otone(-35%) 15%,
				otone(-35%) 75%,
				otone(-40%)
			);
		}
	}
	//&:hover { font-weight: bold; }
}

$grad-ui-focus: linear-gradient(to bottom,
	tone(-50%),
	tone(-35%)
);

input[type='text'], input[type='number'], input[type='password'], textarea, select {
	@extend %serif;
	padding: 0.08in 0.1in;
	box-sizing: border-box;
	border: 1px solid black;
	background: linear-gradient(to bottom, tone(-55%), tone(-40%));
	font-size: 16pt;
	color: tone(25%);
................................................................................
	padding-bottom: 0.1in;
	background-color: tone(-45%,-0.3);
	border: {
		left: 1px solid black;
		right: 1px solid black;
	}
}

.id {
	color: tone(25%,-0.4);
	> .nym {
		font-weight: bold;
		color: tone(25%);
	}
	> .xid {
		color: tone(20%,-0.1);
		font-size: 80%;
		vertical-align: text-top;
	}
}

div.profile {
	padding: 0.1in;
	position: relative;
	display: grid;
	margin-bottom: 0.4in;
	grid-template-columns: 2fr 1fr;
................................................................................
			grid-column: 1 / 2;
			grid-row: 1 / 3;
			border: 1px solid black;
		}
		> .id {
			grid-column: 2 / 3;
			grid-row: 1 / 2;










		}
		> .bio {
			grid-column: 2 / 3;
			grid-row: 2 / 3;
		}
	}
	> .stats {
		grid-column: 3 / 4;
		grid-row: 1 / 3;
		display: flex;
		flex-flow: column;
		> * { flex-grow: 1; }
		table { td, th { text-align: center; } }
	}
	> form.actions {
		grid-column: 1 / 3; grid-row: 2 / 3;
		padding-top: 0.075in;
		flex-wrap: wrap;
		display: flex;
		justify-content: center;
................................................................................
		}
		input, textarea, .txtbox {
			display: block;
			width: 100%;
		}
		textarea { resize: vertical; min-height: 2in; }
	}
	:is(.elem,.elem-group) + %button { margin-left: 50%; width: 50%; }
	.elem-group {
		display: flex;
		flex-flow: row;
		> .elem {
			flex-shrink: 1;
			flex-grow: 1;
			margin-left: 0.1in;
			&:first-child { margin-left: 0; }
		}
		> .small { flex-shrink: 5; }
	}
}

menu.choice {
	display: flex;
	&.horizontal {
		flex-flow: row-reverse wrap;
		justify-content: space-evenly;
................................................................................

.color-picker {
	/* implemented using javascript, alas */
	@extend %box;
	label { text-shadow: 1px 1px black; }
	padding: 0.1in;
}

ul.user-list {
	list-style-type: none;
	margin: 0.5em 0;
	padding: 0;
	box-shadow: 0 0 10px -3px black inset;
	border: 1px solid tone(-50%);
	li {
		background-color: tone(-20%, -0.8);
		padding: 0.5em;
		.regalia { margin-right: 0.3em; vertical-align: bottom; }
		&:nth-child(odd) {
			background-color: tone(-30%, -0.8);
		}
	}
}

ul.remarks {
	margin: 0; padding: 0;
	list-style-type: none;
	li {
		border-top: 1px solid otone(-22%);
		border-bottom: 2px solid otone(-55%);
		border-radius: 3px;
		background: otone(-25%,-0.4);
		color: otone(25%);
		text-align: center;
		padding: 0.3em 0;
		margin: 0.2em 0.1em;
		cursor: default;
	}
}

:is(%button, a[href]).neg { --co:  60 }
:is(%button, a[href]).pos { --co: -30 }

Modified store.t from [e7b33e5534] to [da1c9184b0].

22
23
24
25
26
27
28
29
30






31
32
33







34
35
36
37
38
39
40
41
42
..
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
..
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
..
94
95
96
97
98
99
100











































101
102
103
104
105
106
107
...
280
281
282
283
284
285
286

287
288
289
290
291
292
293
294
295
296
297
298
...
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
...
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
...
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
		'pw', 'otp', 'challenge', 'trust'
	};
	privset = lib.set {
		'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite'
	};
	powerset = lib.set {
		-- user powers -- default on
		'login', 'visible', 'post', 'shout',
		'propagate', 'upload', 'acct', 'edit';







		-- admin powers -- default off
		'purge', 'config', 'censor', 'suspend',







		'cred', 'elevate', 'demote', 'rebrand', -- modify site's brand identity
		'herald', -- grant serverwide epithets
		'invite' -- *unlimited* invites
	};
	prepmode = lib.enum {
		'full','conf','admin'
	}
}

................................................................................
	m.privmap[#m.privmap + 1] = quote
		var ps: m.powerset ps:clear()
		(ps.[v] << true)
	in pt {name = lib.str.plit(v), priv = ps} end
end end

terra m.powerset:affect_users()
	return self.purge() or self.censor() or self.suspend() or
	       self.elevate() or self.demote() or self.cred()
end

local str = rawstring
local pstr = lib.mem.ptr(int8)

struct m.source
................................................................................
	-- creating staff automatically assigns rank immediately below you
	quota: uint32 -- # of allowed tweets per day; 0 = no limit
	invites: uint32 -- # of people left this user can invite
	
	powers: m.powerset
}

terra m.rights_default()
	var pow: m.powerset pow:clear()
	(pow.login     << true)
	(pow.visible   << true)
	(pow.post      << true)
	(pow.shout     << true)
	(pow.propagate << true)
	(pow.upload    << true)
	(pow.acct      << true)
	(pow.edit      << true)
	return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; }
end

struct m.actor {
	id: uint64
................................................................................
	rights: m.rights
	key: lib.mem.ptr(uint8)

-- ephemera
	xid: str
	source: &m.source
}












































terra m.actor.methods.mk(kbuf: &uint8)
	var newkp = lib.crypt.genkp()
	var privsz = lib.crypt.der(false,&newkp,kbuf)
	return m.actor {
		id = 0; nym = nil; handle = nil;
		origin = 0; bio = nil; avatar = nil;
................................................................................
	conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8)
	conf_set: {&m.source, rawstring, rawstring} -> {}
	conf_reset: {&m.source, rawstring} -> {}

	actor_create: {&m.source, &m.actor} -> uint64
	actor_save: {&m.source, &m.actor} -> {}
	actor_save_privs: {&m.source, &m.actor} -> {}

	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
	actor_enum: {&m.source} -> lib.mem.ptr(&m.actor)
	actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor)
	actor_stats: {&m.source, uint64} -> m.actor_stats
	actor_rel: {&m.source, uint64, uint64} -> m.relationship

	actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool}
		-- returns a set of auth method categories that are available for a
		-- given user from a certain origin
			-- origin: inet
................................................................................
			-- cookie issue time: m.timepoint
	actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
		-- notifies the backend module of the UID that has been assigned for
		-- an authentication ID
			-- aid: uint64
			-- uid: uint64

	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.ptr(lib.mem.ptr(m.auth))
	auth_enum_handle: {&m.source, rawstring} -> lib.mem.ptr(lib.mem.ptr(m.auth))
	auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}
		-- uid: uint64
		-- reset: bool (delete other passwords?)
		-- pw: pstring
		-- comment: pstring
	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
................................................................................
			-- uid: uint64
			-- timestamp: timepoint

	post_save: {&m.source, &m.post} -> {}
	post_create: {&m.source, &m.post} -> uint64
	post_destroy: {&m.source, uint64} -> {}
	post_fetch: {&m.source, uint64} -> lib.mem.ptr(m.post)
	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
	post_enum_parent: {&m.source, uint64} -> lib.mem.ptr(lib.mem.ptr(m.post))
	post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
		-- attaches or detaches an existing database artifact
			-- post id: uint64
			-- artifact id: uint64
			-- detach: bool

	thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint
................................................................................
			-- proto: kompromat (null for all records, or a prototype describing the records to return)
	nkvd_sanction_issue:  {&m.source, &m.sanction} -> uint64
	nkvd_sanction_vacate: {&m.source, uint64} -> {}
	nkvd_sanction_enum_target: {&m.source, uint64} -> {}
	nkvd_sanction_enum_issuer: {&m.source, uint64} -> {}
	nkvd_sanction_review: {&m.source, m.timepoint} -> {}

	timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
	timeline_instance_fetch: {&m.source, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
}

m.user_conf_funcs(m.backend, 'str', rawstring, lib.mem.ptr(int8))
m.user_conf_funcs(m.backend, 'int', intptr, intptr, bool)

struct m.source {
	backend: &m.backend







|
|
>
>
>
>
>
>


<
>
>
>
>
>
>
>
|
|







 







|







 







|






|







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







>



|
|







 







|
|







 







|
|







 







|
|







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
..
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
..
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
...
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
...
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
...
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
...
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
...
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
		'pw', 'otp', 'challenge', 'trust'
	};
	privset = lib.set {
		'post', 'edit', 'acct', 'upload', 'censor', 'admin', 'invite'
	};
	powerset = lib.set {
		-- user powers -- default on
		'login', -- not locked out
		'visible', -- account & posts can be seen by others
		'post', -- can do poasts
		'shout', -- posts show up on local timeline
		'propagate', -- posts are sent to other instances
		'artifact', -- upload, claim, and manage artifacts
		'acct', -- configure own account
		'edit'; -- edit own poasts

		-- admin powers -- default off

		'purge', -- permanently delete users
		'config', -- change daemon policy & config UI
		'censor', -- dispose of badthink
		'discipline', -- enforced timeouts, stripping badges and epithets, punitive actions that do not permanently deprive of powers; can remove own injunctions but not others'
		'vacate', -- can remove others' injunctions, but not apply them
		'cred', -- alter credentials
		'elevate', 'demote', -- change user rank, give and take powers, including the ability to log in
		'rebrand', -- modify site's brand identity
		'herald', -- grant serverwide epithets and badges
		'invite' -- *unlimited* invites
	};
	prepmode = lib.enum {
		'full','conf','admin'
	}
}

................................................................................
	m.privmap[#m.privmap + 1] = quote
		var ps: m.powerset ps:clear()
		(ps.[v] << true)
	in pt {name = lib.str.plit(v), priv = ps} end
end end

terra m.powerset:affect_users()
	return self.purge() or self.discipline() or self.herald() or
	       self.elevate() or self.demote() or self.cred()
end

local str = rawstring
local pstr = lib.mem.ptr(int8)

struct m.source
................................................................................
	-- creating staff automatically assigns rank immediately below you
	quota: uint32 -- # of allowed tweets per day; 0 = no limit
	invites: uint32 -- # of people left this user can invite
	
	powers: m.powerset
}

terra m.rights_default() -- TODO make configurable
	var pow: m.powerset pow:clear()
	(pow.login     << true)
	(pow.visible   << true)
	(pow.post      << true)
	(pow.shout     << true)
	(pow.propagate << true)
	(pow.artifact  << true)
	(pow.acct      << true)
	(pow.edit      << true)
	return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; }
end

struct m.actor {
	id: uint64
................................................................................
	rights: m.rights
	key: lib.mem.ptr(uint8)

-- ephemera
	xid: str
	source: &m.source
}

terra m.actor:outranks(other: &m.actor)
 -- this predicate determines where two users stand relative to
 -- each other in the formal staff hierarchy. it is used in
 -- authority calculations, but this function should only be
 -- used directly in rendering code and by other predicates.
 -- do not use it in authority calculation, as there are special
 -- cases where formal rank does not fully determine a user's
 -- capabilities (e.g. roots have the same rank, but can
 -- exercise power over each other, unlike lower ranks)
	if self.rights.rank == 0 then
	 -- peons never outrank anybody
		return false
	end
	if other.rights.rank == 0 then
	 -- everybody outranks peons
		return true
	end
	return self.rights.rank < other.rights.rank
	-- rank 1 is the highest possible, rank 2 is second-highest, and so on
end

terra m.actor:overpowers(other: &m.actor)
 -- this predicate determines whether one user may exercise their
 -- powers over another user. it does not affect what those powers
 -- actually are (for instance, you cannot revoke a power you do
 -- not have, no matter how much you outrank someone)
	if self.rights.rank == 1 and other.rights.rank == 1 then
	 -- special case: root users always overpower each other
	 -- otherwise, nobody could reset their passwords
	 -- (also dissuades people from giving root lightly)
		return true
	end
	return self:outranks(other)
end

terra m.actor.methods.handle_validate(hnd: rawstring)
	if hnd[0] == 0 then
		return false
	end
	-- TODO validate fully
	return true
end

terra m.actor.methods.mk(kbuf: &uint8)
	var newkp = lib.crypt.genkp()
	var privsz = lib.crypt.der(false,&newkp,kbuf)
	return m.actor {
		id = 0; nym = nil; handle = nil;
		origin = 0; bio = nil; avatar = nil;
................................................................................
	conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8)
	conf_set: {&m.source, rawstring, rawstring} -> {}
	conf_reset: {&m.source, rawstring} -> {}

	actor_create: {&m.source, &m.actor} -> uint64
	actor_save: {&m.source, &m.actor} -> {}
	actor_save_privs: {&m.source, &m.actor} -> {}
	actor_purge_uid: {&m.source, uint64} -> {}
	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
	actor_enum: {&m.source} -> lib.mem.lstptr(m.actor)
	actor_enum_local: {&m.source} -> lib.mem.lstptr(m.actor)
	actor_stats: {&m.source, uint64} -> m.actor_stats
	actor_rel: {&m.source, uint64, uint64} -> m.relationship

	actor_auth_how: {&m.source, m.inet, rawstring} -> {m.credset, bool}
		-- returns a set of auth method categories that are available for a
		-- given user from a certain origin
			-- origin: inet
................................................................................
			-- cookie issue time: m.timepoint
	actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
		-- notifies the backend module of the UID that has been assigned for
		-- an authentication ID
			-- aid: uint64
			-- uid: uint64

	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
	auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}
		-- uid: uint64
		-- reset: bool (delete other passwords?)
		-- pw: pstring
		-- comment: pstring
	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
................................................................................
			-- uid: uint64
			-- timestamp: timepoint

	post_save: {&m.source, &m.post} -> {}
	post_create: {&m.source, &m.post} -> uint64
	post_destroy: {&m.source, uint64} -> {}
	post_fetch: {&m.source, uint64} -> lib.mem.ptr(m.post)
	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post)
	post_enum_parent: {&m.source, uint64} -> lib.mem.lstptr(m.post)
	post_attach_ctl: {&m.source, uint64, uint64, bool} -> {}
		-- attaches or detaches an existing database artifact
			-- post id: uint64
			-- artifact id: uint64
			-- detach: bool

	thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint
................................................................................
			-- proto: kompromat (null for all records, or a prototype describing the records to return)
	nkvd_sanction_issue:  {&m.source, &m.sanction} -> uint64
	nkvd_sanction_vacate: {&m.source, uint64} -> {}
	nkvd_sanction_enum_target: {&m.source, uint64} -> {}
	nkvd_sanction_enum_issuer: {&m.source, uint64} -> {}
	nkvd_sanction_review: {&m.source, m.timepoint} -> {}

	timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post)
	timeline_instance_fetch: {&m.source, m.range} -> lib.mem.lstptr(m.post)
}

m.user_conf_funcs(m.backend, 'str', rawstring, lib.mem.ptr(int8))
m.user_conf_funcs(m.backend, 'int', intptr, intptr, bool)

struct m.source {
	backend: &m.backend

Modified view/profile.tpl from [694fa2eec6] to [4ac5403896].

2
3
4
5
6
7
8
9

10

11
12
13
14
15


16
17
18
19
20
21
22
23
24
	<div class="banner">
		<img class="avatar" src="@:avatar">
		<div class="id">@nym</div>
		<div class="bio">
			@bio
		</div>
	</div>
	<table class="stats">

		<tr><th>posts</th> <td>@nposts</td></tr>

		<tr><th>following</th> <td>@nfollows</td></tr>
		<tr><th>followers</th> <td>@nfollowers</td></tr>
		<tr><th>mutuals</th> <td>@nmutuals</td></tr>
		<tr><th>@timephrase</th> <td>@tweetday</td></tr>
	</table>


	<form class="actions">
		<a class="button" href="/@:xid">posts</a>
		<a class="button" href="/@:xid/arc">archive</a>
		<a class="button" href="/@:xid/media">media</a>
		<a class="button" href="/@:xid/social">associates</a>
		<hr>
		@auxbtn
	</form>
</div>







|
>
|
>
|
|
<
|
|
>
>









2
3
4
5
6
7
8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23
24
25
26
27
	<div class="banner">
		<img class="avatar" src="@:avatar">
		<div class="id">@nym</div>
		<div class="bio">
			@bio
		</div>
	</div>
	<div class="stats">
		<table>
			<tr><th>posts</th> <th>mutuals</th></tr>
			<tr><td>@nposts</td> <td>@nmutuals</td></tr>
			<tr><th>following</th> <th>followers</th></tr>
			<tr><td>@nfollows</td> <td>@nfollowers</td></tr>

			<tr><th>@timephrase</th> <td>@tweetday</td></tr>
		</table>
		<ul class="remarks">@remarks</ul>
	</div>
	<form class="actions">
		<a class="button" href="/@:xid">posts</a>
		<a class="button" href="/@:xid/arc">archive</a>
		<a class="button" href="/@:xid/media">media</a>
		<a class="button" href="/@:xid/social">associates</a>
		<hr>
		@auxbtn
	</form>
</div>