parsav  Check-in [0324d62546]

Overview
Comment:continued iteration
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 0324d625467b68d51407a9ab6432c30e71eed46459b9cd666a5355f14b585143
User & Date: lexi on 2020-12-30 00:43:11
Other Links: manifest | tags
Context
2020-12-30
02:44
enable profile editing check-in: ac4a630ad5 user: lexi tags: trunk
00:43
continued iteration check-in: 0324d62546 user: lexi tags: trunk
2020-12-29
15:48
enable remote control of running instances check-in: f8816b0ab5 user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [0f1425913d] to [30de1dd276].

131
132
133
134
135
136
137
138

139


140
141
142
143
144

145
146
147
148
149
150
151
...
278
279
280
281
282
283
284
285
286






































































287
288
289
290
291
292
293
...
685
686
687
688
689
690
691






















692
693
694
695
696
697
698
...
905
906
907
908
909
910
911
912











913















































914
915
916
				select relatee as user from parsav_rels
					where relator = $1::bigint and kind = <follow>
			),
			followers as (
				select relator as user from parsav_rels
					where relatee = $1::bigint and kind = <follow>
			),
			mutuals as (select * from follows intersect select * from followers)




			select count(tweets.*)::bigint,
			       count(follows.*)::bigint,
				   count(followers.*)::bigint,
				   count(mutuals.*)::bigint
			from tweets, follows, followers, mutuals

		]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation[r]) end)
	};

	actor_auth_how = {
		params = {rawstring, lib.store.inet}, sql = [[
		with mts as (select a.kind from parsav_auth as a
			left join parsav_actors as u on u.id = a.uid
................................................................................
				(a.origin is null)
			order by (p.posted, p.discovered) desc
			limit case when $3::bigint = 0 then null
			           else $3::bigint end
			offset $4::bigint
		]]
	};
}
				--($5::bool = false or p.parent is null) and







































































local struct pqr {
	sz: intptr
	res: &lib.pq.PGresult
}
terra pqr:free() if self.sz > 0 then lib.pq.PQclear(self.res) end end
terra pqr:null(row: intptr, col: intptr)
................................................................................
			lib.report('successfully wiped out everything parsav-related in database')
			return true
		else
			lib.warn('backend pgsql - failed to obliterate database: \n', lib.pq.PQresultErrorMessage(res))
			return false
		end
	end];























	conf_get = [terra(src: &lib.store.source, key: rawstring)
		var r = queries.conf_get.exec(src, key)
		if r.sz == 0 then return [lib.mem.ptr(int8)] { ptr = nil, ct = 0 } else
			defer r:free()
			return r:String(0,0)
		end
................................................................................
		queries.auth_purge_type.exec(src, handle, uid, 'otp-%')
	end];

	auth_purge_trust = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
		queries.auth_purge_type.exec(src, handle, uid, 'trust')
	end];

	actor_auth_register_uid = nil; -- not necessary for view-based auth



























































}

return b







|
>
|
>
>
|
|
|
|
<
>







 







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







 







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







 







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

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



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146

147
148
149
150
151
152
153
154
...
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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
...
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
....
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
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
				select relatee as user from parsav_rels
					where relator = $1::bigint and kind = <follow>
			),
			followers as (
				select relator as user from parsav_rels
					where relatee = $1::bigint and kind = <follow>
			),
			mutuals as (
				select * from follows  intersect  select * from followers
			)

			values (
				(select count(tweets.*)::bigint from tweets),
				(select count(follows.*)::bigint from follows),
				(select count(followers.*)::bigint from followers),
				(select count(mutuals.*)::bigint from mutuals)

			)
		]]):gsub('<(%w+)>',function(r) return tostring(lib.store.relation[r]) end)
	};

	actor_auth_how = {
		params = {rawstring, lib.store.inet}, sql = [[
		with mts as (select a.kind from parsav_auth as a
			left join parsav_actors as u on u.id = a.uid
................................................................................
				(a.origin is null)
			order by (p.posted, p.discovered) desc
			limit case when $3::bigint = 0 then null
			           else $3::bigint end
			offset $4::bigint
		]]
	};

	artifact_instantiate = {
		params = {binblob, binblob, pstring}, sql = [[
			insert into parsav_artifacts (content,hash,mime) values (
				$1::bytea, $2::bytea, $3::text
			) on conflict do nothing returning id
		]];
	};
	artifact_expropriate = {
		params = {uint64, uint64, pstring}, cmd = true, sql = [[
			insert into parsav_artifact_claims (uid,rid,description,folder) values (
				$1::bigint, $2::bigint, $3::text, 'new'
			) on conflict do nothing
		]];
	};
	artifact_quicksearch = {
		params = {binblob}, sql = [[
			select id, (content is null) from parsav_artifacts where hash = $1::bytea
				limit 1
		]];
	};
	artifact_disclaim = {
		params = {uint64, uint64}, cmd = true, sql = [[
			delete from parsav_artifact_claims where
				uid = $1::bigint and
				rid = $2::bigint
		]];
	};
	artifact_excise_forget = {
		-- delete the blasted thing and pretend it never existed
		params = {uint64}, cmd=true, sql = [[
			delete from parsav_artifacts where id = $1::bigint
		]];
	};
	artifact_excise_suppress_nullify = {
		-- banish the thing into the outer darkness, preventing
		-- it from ever being admitted into our databases, and
		-- tabulate a -- list of the degenerates who befouled
		-- their accounts with such wanton and execrable filth,
		-- the better to ensure their long-overdue punishment
		params = {uint64}, cmd=true, sql = [[
			update parsav_artifacts
				set content = null
				where id = $1::bigint;
		]];
	};
	artifact_excise_suppress_breaklinks = {
	-- "ERROR:  cannot insert multiple commands into a prepared
	--  statement" are you fucking shitting me with this shit
		params = {uint64}, sql = [[
			delete from parsav_artifact_claims where
				rid = $1::bigint
			returning uid, description, birth, folder;
		]];
	};
	post_attach_ctl_ins = {
		params = {uint64, uint64}, cmd=true, sql = [[
			update parsav_posts set
				artifacts = artifacts || $2::bigint
			where id = $1::bigint and not
				artifacts @> array[$2::bigint]
		]];
	};
	post_attach_ctl_del = {
		params = {uint64, uint64}, cmd=true, sql = [[
			update parsav_posts set
				artifacts = array_remove(artifacts, $2::bigint)
			where id = $1::bigint and
				artifacts @> array[$2::bigint]
		]];
	};
}

local struct pqr {
	sz: intptr
	res: &lib.pq.PGresult
}
terra pqr:free() if self.sz > 0 then lib.pq.PQclear(self.res) end end
terra pqr:null(row: intptr, col: intptr)
................................................................................
			lib.report('successfully wiped out everything parsav-related in database')
			return true
		else
			lib.warn('backend pgsql - failed to obliterate database: \n', lib.pq.PQresultErrorMessage(res))
			return false
		end
	end];

	tx_enter = [terra(src: &lib.store.source)
		var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), 'begin')
		if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
			lib.dbg('beginning pgsql transaction')
			return true
		else
			lib.warn('backend pgsql - failed to begin transaction: \n', lib.pq.PQresultErrorMessage(res))
			return false
		end
	end];

	tx_complete = [terra(src: &lib.store.source)
		var res = lib.pq.PQexec([&lib.pq.PGconn](src.handle), 'end')
		if lib.pq.PQresultStatus(res) == lib.pq.PGRES_COMMAND_OK then
			lib.dbg('completing pgsql transaction')
			return true
		else
			lib.warn('backend pgsql - failed to complete transaction: \n', lib.pq.PQresultErrorMessage(res))
			return false
		end
	end];

	conf_get = [terra(src: &lib.store.source, key: rawstring)
		var r = queries.conf_get.exec(src, key)
		if r.sz == 0 then return [lib.mem.ptr(int8)] { ptr = nil, ct = 0 } else
			defer r:free()
			return r:String(0,0)
		end
................................................................................
		queries.auth_purge_type.exec(src, handle, uid, 'otp-%')
	end];

	auth_purge_trust = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
		queries.auth_purge_type.exec(src, handle, uid, 'trust')
	end];

	artifact_quicksearch = [terra(
		src: &lib.store.source,
		hash: binblob
	): {uint64, bool}
		var srec = queries.artifact_quicksearch.exec(src, hash)
		if srec.sz > 0 then
			defer srec:free()
			var id = srec:int(uint64,0,0)
			var ban = srec:bool(0,1)
			return id, ban
		else return 0, false end
	end];

	artifact_instantiate = [terra(
		src: &lib.store.source,
		artifact: binblob,
		mime: pstring
	): uint64
		var arthash: uint8[lib.crypt.algsz.sha256]
		if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id),
			artifact.ptr, artifact.ct, &arthash[0]) ~= 0 then
			lib.bail('could not hash artifact to be instantiated')
		end
		var hashb = binblob{ptr=&arthash[0],ct=[arthash.type.N]}

		var srec = queries.artifact_quicksearch.exec(src, hashb)
		if srec.sz > 0 then
			defer srec:free()
			var ban = srec:bool(0,1)
			if ban then
				lib.report('user attempted to instantiate forsaken artifact')
				return 0
			end
			var oldid = srec:int(uint64,0,0)
			return oldid
		else -- not in db, insert
			var nrec = queries.artifact_instantiate.exec(src, artifact, hashb, mime)
			if nrec.sz == 0 then
				lib.warn('failed to instantiate artifact -- are you running out of storage?')
				return 0
			else defer nrec:free()
				var newid = nrec:int(uint64,0,0)
				return newid
			end
		end
	end];

	post_attach_ctl = [terra(
		src: &lib.store.source,
		post: uint64,
		artifact: uint64,
		detach: bool
	): {}
		if detach
			then queries.post_attach_ctl_del.exec(src,post,artifact)
			else queries.post_attach_ctl_ins.exec(src,post,artifact)
		end
	end];

	actor_auth_register_uid = nil; -- TODO better support non-view based auth
}

return b

Modified backend/schema/pgsql-drop.sql from [e1fb43be2e] to [17a37aa5f6].

5
6
7
8
9
10
11
12

13
14
15
16
17
18
drop table if exists parsav_actors cascade;
drop table if exists parsav_rights cascade;
drop table if exists parsav_posts cascade;
drop table if exists parsav_conversations cascade;
drop table if exists parsav_rels cascade;
drop table if exists parsav_acts cascade;
drop table if exists parsav_log cascade;
drop table if exists parsav_attach cascade;

drop table if exists parsav_circles cascade;
drop table if exists parsav_rooms cascade;
drop table if exists parsav_room_members cascade;
drop table if exists parsav_invites cascade;
drop table if exists parsav_interventions cascade;
drop table if exists parsav_auth cascade;







|
>




|

5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
drop table if exists parsav_actors cascade;
drop table if exists parsav_rights cascade;
drop table if exists parsav_posts cascade;
drop table if exists parsav_conversations cascade;
drop table if exists parsav_rels cascade;
drop table if exists parsav_acts cascade;
drop table if exists parsav_log cascade;
drop table if exists parsav_artifacts cascade;
drop table if exists parsav_artifact_claims cascade;
drop table if exists parsav_circles cascade;
drop table if exists parsav_rooms cascade;
drop table if exists parsav_room_members cascade;
drop table if exists parsav_invites cascade;
drop table if exists parsav_sanctions cascade;
drop table if exists parsav_auth cascade;

Modified backend/schema/pgsql.sql from [0ef43163b5] to [cb277a93b1].

26
27
28
29
30
31
32

33
34
35
36
37
38
39
..
55
56
57
58
59
60
61
62
63
64

65
66
67
68
69
70
71
..
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
	id        bigint primary key default (1+random()*(2^63-1))::bigint,
	nym       text,
	handle    text not null, -- nym [@handle@origin] 
	origin    bigint references parsav_servers(id)
		on delete cascade, -- null origin = local actor
	knownsince timestamp,
	bio       text,

	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
	
................................................................................
	author     bigint references parsav_actors(id)
		on delete cascade,
	subject    text,
	acl        text not null default 'all', -- just store the script raw 🤷
	body       text,
	posted     timestamp not null,
	discovered timestamp not null,
	parent     bigint not null default 0,
	circles    bigint[], -- TODO at edit or creation, iterate through each circle
	mentions   bigint[], -- a user has, check if it can see her post, and if so add


	convoheaduri text
	-- only used for tracking foreign conversations and tying them to post heads;
	-- local conversations are tracked directly and mapped to URIs based on the
	-- head's ID. null if native tweet or not the first tweet in convo
);

................................................................................
	id    bigint primary key default (1+random()*(2^63-1))::bigint,
	time  timestamp not null default now(),
	actor bigint references parsav_actors(id)
		on delete cascade,
	post  bigint not null
);

create table parsav_attach (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	birth       timestamp not null default now(),
	content     bytea not null,



	mime        text, -- null if unknown, will be reported as x-octet-stream







	description text,
	parent      bigint -- post id, or userid for avatars



);


create table parsav_circles (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	owner       bigint not null references parsav_actors(id),
	name        text not null,
	members     bigint[] not null default array[]::bigint[],

	unique (owner,name)
);

create table parsav_rooms (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	origin		bigint references parsav_servers(id),
	name		text not null,
	description text not null,
	policy      smallint not null
);

create table parsav_room_members (
	room   bigint references parsav_rooms(id),
	member bigint references parsav_actors(id),
	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
	issuer bigint references parsav_actors(id),
	handle text, -- admin can lock invite to specific handle
	rank   smallint not null default 0,
	quota  integer not null  default 1000
);

create table parsav_interventions (
	id     bigint primary key default (1+random()*(2^63-1))::bigint,
	issuer bigint references parsav_actors(id) not null,
	scope  bigint, -- can be null or room for local actions
	nature smallint not null, -- silence, suspend, disemvowel, etc
	victim bigint not null, -- could potentially target group as well
	expire timestamp -- auto-expires if set



);

-- create a temporary managed auth table; we can delete this later
-- if it ends up being replaced with a view
%include pgsql-auth.sql%







>







 







|


>







 







|


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

<
>
>
>

>



|








|






|
|











|





|

|

|
|
|
>
>
>





26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
..
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
..
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
	id        bigint primary key default (1+random()*(2^63-1))::bigint,
	nym       text,
	handle    text not null, -- nym [@handle@origin] 
	origin    bigint references parsav_servers(id)
		on delete cascade, -- null origin = local actor
	knownsince timestamp,
	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
	
................................................................................
	author     bigint references parsav_actors(id)
		on delete cascade,
	subject    text,
	acl        text not null default 'all', -- just store the script raw 🤷
	body       text,
	posted     timestamp not null,
	discovered timestamp not null,
	parent     bigint not null default 0, -- if post: part of conversation; if chatroom: top-level post
	circles    bigint[], -- TODO at edit or creation, iterate through each circle
	mentions   bigint[], -- a user has, check if it can see her post, and if so add
	artifacts  bigint[],

	convoheaduri text
	-- only used for tracking foreign conversations and tying them to post heads;
	-- local conversations are tracked directly and mapped to URIs based on the
	-- head's ID. null if native tweet or not the first tweet in convo
);

................................................................................
	id    bigint primary key default (1+random()*(2^63-1))::bigint,
	time  timestamp not null default now(),
	actor bigint references parsav_actors(id)
		on delete cascade,
	post  bigint not null
);

create table parsav_artifacts (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	birth       timestamp not null default now(),
	content     bytea, -- if null, this is a "ban record" preventing content matching the hash from being re-uploaded
	hash		bytea unique not null, -- sha256 hash of content
	-- it would be cool to use a computed column for this, but i don't want
	-- to lock people into PG12 or drag in the pgcrypto extension just for this
	mime        text -- null if unknown, will be reported as x-octet-stream
);
create index on parsav_artifacts (mime);

create table parsav_artifact_claims (
	birth timestamp not null default now(),
	uid bigint references parsav_actors(id) on delete cascade,
	rid bigint references parsav_artifacts(id) on delete cascade,
	description text,

	folder text,

	unique (uid,rid)
);
create index on parsav_artifact_claims (uid);

create table parsav_circles (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	owner       bigint not null references parsav_actors(id) on delete cascade,
	name        text not null,
	members     bigint[] not null default array[]::bigint[],

	unique (owner,name)
);

create table parsav_rooms (
	id          bigint primary key default (1+random()*(2^63-1))::bigint,
	origin		bigint references parsav_servers(id) on delete cascade,
	name		text not null,
	description text not null,
	policy      smallint not null
);

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
	issuer bigint references parsav_actors(id) on delete set null,
	handle text, -- admin can lock invite to specific handle
	rank   smallint not null default 0,
	quota  integer not null  default 1000
);

create table parsav_sanctions (
	id     bigint primary key default (1+random()*(2^63-1))::bigint,
	issuer bigint references parsav_actors(id) on delete set null,
	scope  bigint, -- can be null or room for local actions
	nature smallint not null, -- silence, suspend, disemvowel, censor, noreply, etc
	victim bigint not null, -- can be user, room, or post
	expire timestamp, -- auto-expires if set
	review timestamp,  -- brings up for review at given time if set
	reason text, -- visible to victim if set
	context text -- admin-only note
);

-- create a temporary managed auth table; we can delete this later
-- if it ends up being replaced with a view
%include pgsql-auth.sql%

Modified mgtool.t from [c623f2c8c5] to [03b0c72484].

67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
...
271
272
273
274
275
276
277
278
279
280
281
282
283
284

285
286
287

288
289
290
291
292
293
294
		local fn = f.field or f[1]
		local ft = f.type or f[2]
		if fn == meth then rt = ft.type.returntype break end
	end

	return quote
		var r: rt
		if self.all
			then r=self.srv:[meth]([expr])
			elseif self.src ~= nil then r=self.src:[meth]([expr])
			else lib.bail('no data source specified')
		end
	in r end
end)

................................................................................
				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:dbsetup()
				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')

			elseif lib.str.cmp(dbmode.arglist(0),'obliterate') == 0 then
				var confirmstrs = array(
					'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa'

				)
				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







|







 







|
|
|
|
|
|
|
>


|
>







67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
...
271
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
		local fn = f.field or f[1]
		local ft = f.type or f[2]
		if fn == meth then rt = ft.type.returntype break end
	end

	return quote
		var r: rt
		if self.all or (self.srv ~= nil and self.srv.sources.ct == 1)
			then r=self.srv:[meth]([expr])
			elseif self.src ~= nil then r=self.src:[meth]([expr])
			else lib.bail('no data source specified')
		end
	in r end
end)

................................................................................
				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

Modified render/conf.t from [6e08f785f6] to [79b6da76d7].

62
63
64
65
66
67
68

69
70
71
72
73
74
		panel = panel;
	}

	var pgt = pg:tostr() defer pgt:free()
	co:stdpage([lib.srv.convo.page] {
		title = 'configure'; body = pgt;
		class = lib.str.plit 'conf';

	})

	if panel.ct ~= 0 then panel:free() end
end

return render_conf







>






62
63
64
65
66
67
68
69
70
71
72
73
74
75
		panel = panel;
	}

	var pgt = pg:tostr() defer pgt:free()
	co:stdpage([lib.srv.convo.page] {
		title = 'configure'; body = pgt;
		class = lib.str.plit 'conf';
		cache = false;
	})

	if panel.ct ~= 0 then panel:free() end
end

return render_conf

Modified render/docpage.t from [7e1168b387] to [3eda6110a6].

44
45
46
47
48
49
50

51
52
53
54
55
56
57
...
107
108
109
110
111
112
113

114
115
116
117
118
119
		parent = par;
		priv = restrict;
		title = R(t.meta.title);
		content = page {
			title = ['documentation :: ' .. t.meta.title];
			body = [ t.text ];
			class = P'doc article';

		};
	} end
end

local terra 
showpage(co: &lib.srv.convo, id: pref)
	var [pages] = array([allpages])
................................................................................
		list:lpush('</ul>')

		var bp = list:finalize()
		co:stdpage(page {
			title = 'documentation';
			body = bp;
			class = P'doc listing';

		})
		bp:free()
	else showpage(co, pg) end
end

return render_docpage







>







 







>






44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
...
108
109
110
111
112
113
114
115
116
117
118
119
120
121
		parent = par;
		priv = restrict;
		title = R(t.meta.title);
		content = page {
			title = ['documentation :: ' .. t.meta.title];
			body = [ t.text ];
			class = P'doc article';
			cache = true;
		};
	} end
end

local terra 
showpage(co: &lib.srv.convo, id: pref)
	var [pages] = array([allpages])
................................................................................
		list:lpush('</ul>')

		var bp = list:finalize()
		co:stdpage(page {
			title = 'documentation';
			body = bp;
			class = P'doc listing';
			cache = false;
		})
		bp:free()
	else showpage(co, pg) end
end

return render_docpage

Modified render/login.t from [dd5c50c3e9] to [7ea4ccf2b4].

1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17
..
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local P = lib.str.plit
local terra 
login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: pstr)
	var doc = data.view.docskel {
		instance = co.srv.cfg.instance;
		title = lib.str.plit 'instance logon';
		class = lib.str.plit 'login';
		navlinks = co.navbar;

	}

	if user == nil then
		var form = data.view.login_username {
			loginmsg = msg;
		}
		if form.loginmsg.ptr == nil then
................................................................................
		end

		doc.body = ch:tostr()
	else
		-- pick a method
	end

	var hdrs = array(
		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
	)
	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
	doc.body:free()
end

return login_form





|
<


<
>







 







|
<
<
<




1
2
3
4
5
6

7
8

9
10
11
12
13
14
15
16
..
51
52
53
54
55
56
57
58



59
60
61
62
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local P = lib.str.plit
local terra 
login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: pstr)
	var doc = [lib.srv.convo.page] {

		title = lib.str.plit 'instance logon';
		class = lib.str.plit 'login';

		cache = false;
	}

	if user == nil then
		var form = data.view.login_username {
			loginmsg = msg;
		}
		if form.loginmsg.ptr == nil then
................................................................................
		end

		doc.body = ch:tostr()
	else
		-- pick a method
	end

	co:stdpage(doc)



	doc.body:free()
end

return login_form

Modified render/nav.t from [27572ae99b] to [4a737bb7ff].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- vim: ft=terra
local terra 
render_nav(co: &lib.srv.convo)
	var t: lib.str.acc t:init(64)
	if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then
		t:lpush('<a href="/">timeline</a>')
	end
	if co.who ~= nil then
		t:lpush('<a href="/compose">compose</a> <a href="/'):push(co.who.xid,0)
		t:lpush('">profile</a> <a href="/conf">configure</a> <a href="/doc">docs</a> <a href="/logout">log out</a>')
	else
		t:lpush('<a href="/doc">docs</a> <a href="/login">log in</a>')
	end
	return t:finalize()
end
return render_nav





|


|


|




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- vim: ft=terra
local terra 
render_nav(co: &lib.srv.convo)
	var t: lib.str.acc t:init(64)
	if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then
		t:lpush(' <a href="/">timeline</a>')
	end
	if co.who ~= nil then
		t:lpush(' <a href="/compose">compose</a> <a href="/'):push(co.who.xid,0)
		t:lpush('">profile</a> <a href="/conf">configure</a> <a href="/doc">docs</a> <a href="/logout">log out</a>')
	else
		t:lpush(' <a href="/doc">docs</a> <a href="/login">log in</a>')
	end
	return t:finalize()
end
return render_nav

Modified render/nym.t from [89e574dd98] to [0d2437aadd].

1
2



3
4
5
6

7

8
9

10

11
12

13

14
15
16
17
18
19
20
21
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)




local terra 
render_nym(who: &lib.store.actor, scope: uint64)
	var n: lib.str.acc n:init(128)

	if who.nym ~= nil and who.nym[0] ~= 0 then

		n:compose('<span class="nym">',who.nym,'</span> [<span class="handle">',
			who.xid,'</span>]')

	else n:compose('<span class="handle">',who.xid,'</span>') end


	if who.epithet ~= nil then

		n:lpush(' <span class="epithet">'):push(who.epithet,0):lpush('</span>')

	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
-- 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

Modified render/timeline.t from [873aeea861] to [2afba48373].

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
	var acc: lib.str.acc acc:init(1024)
	for i = 0, posts.sz do
		lib.render.tweet(co, posts(i).ptr, &acc)
		posts(i):free()
	end
	posts:free()

	var doc = data.view.docskel {
		instance = co.srv.cfg.instance;
		title = lib.str.plit'timeline';
		body = acc:finalize();
		class = lib.str.plit'timeline';
		navlinks = co.navbar;
	}
	var hdrs = array(
		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }
	)
	doc:send(co.con,200,[lib.mem.ptr(lib.http.header)] {ct = 1, ptr = &hdrs[0]})
	doc.body:free()
end
return render_timeline







|
<



|

|
<
<
<



29
30
31
32
33
34
35
36

37
38
39
40
41
42



43
44
45
	var acc: lib.str.acc acc:init(1024)
	for i = 0, posts.sz do
		lib.render.tweet(co, posts(i).ptr, &acc)
		posts(i):free()
	end
	posts:free()

	var doc = [lib.srv.convo.page] {

		title = lib.str.plit'timeline';
		body = acc:finalize();
		class = lib.str.plit'timeline';
		cache = false;
	}
	co:stdpage(doc)



	doc.body:free()
end
return render_timeline

Modified render/userpage.t from [edce0da550] to [8e478d7a95].

26
27
28
29
30
31
32

33
34
35
36
37
38
39
	end
	posts:free()

	var bdf = acc:finalize()
	co:stdpage([lib.srv.convo.page] {
		title = tiptr; body = bdf;
		class = lib.str.plit 'profile';

	})

	tiptr:free()
	bdf:free()
end

return render_userpage







>







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
	end
	posts:free()

	var bdf = acc:finalize()
	co:stdpage([lib.srv.convo.page] {
		title = tiptr; body = bdf;
		class = lib.str.plit 'profile';
		cache = false;
	})

	tiptr:free()
	bdf:free()
end

return render_userpage

Modified route.t from [4ebb6db558] to [5e16c3f22b].

115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
			if aid == 0 then
				lib.render.login(co, nil, nil, lib.str.plit 'authentication failure')
			else
				var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
				do var p = &sesskey[0]
					p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
					p = p + lib.session.cookie_gen(co.srv.cfg.secret, aid, lib.osclock.time(nil), p)
					lib.dbg('sending cookie',&sesskey[0])
					p = lib.str.ncpy(p, '; Path=/', 9)
				end
				co:reroute_cookie('/', &sesskey[0])
			end
		end
		if act.ptr ~= nil and fakeact == false then act:free() end
	else







|







115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
			if aid == 0 then
				lib.render.login(co, nil, nil, lib.str.plit 'authentication failure')
			else
				var sesskey: int8[lib.session.maxlen + #lib.session.cookiename + #"=; Path=/" + 1]
				do var p = &sesskey[0]
					p = lib.str.ncpy(p, [lib.session.cookiename .. '='], [#lib.session.cookiename + 1])
					p = p + lib.session.cookie_gen(co.srv.cfg.secret, aid, lib.osclock.time(nil), p)
					lib.dbg('sending cookie ',{&sesskey[0],15})
					p = lib.str.ncpy(p, '; Path=/', 9)
				end
				co:reroute_cookie('/', &sesskey[0])
			end
		end
		if act.ptr ~= nil and fakeact == false then act:free() end
	else

Modified srv.t from [7727a773dd] to [4721bcd333].

156
157
158
159
160
161
162

163


164
165
166
167
168
169
170
...
194
195
196
197
198
199
200

201
202
203
204
205
206
207
208
209
210
211
212
213

214
215
216
217
218
219
220
221
222
223
		ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0)
	})
end

terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end
 
terra convo:complain(code: uint16, title: rawstring, msg: rawstring)

	var hdrs = array(lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' })



	var ti: lib.str.acc ti:compose('error :: ', title)
	var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.webp"><h1>',title,'</h1><p>',msg,'</p></div>')
	var body = data.view.docskel {
		instance = self.srv.cfg.instance;
		title = ti:finalize();
		body = bo:finalize();
................................................................................
	in ok end
end)

struct convo.page {
	title: pstring
	body: pstring
	class: pstring

}

terra convo:stdpage(pg: convo.page)
	var doc = data.view.docskel {
		instance = self.srv.cfg.instance;
		title = pg.title;
		body = pg.body;
		class = pg.class;
		navlinks = self.navbar;
	}

	var hdrs = array(
		lib.http.header { 'Content-Type', 'text/html; charset=UTF-8' }

	)

	doc:send(self.con,200,[lib.mem.ptr(lib.http.header)] {ct = [hdrs.type.N], ptr = &hdrs[0]})
end

-- CALL ONLY ONCE PER VAR
terra convo:postv(name: rawstring)
	if self.varbuf.ptr == nil then
		self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len)
		self.vbofs = self.varbuf.ptr







>
|
>
>







 







>












|
>


|







156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
...
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
		ptr = &hdrs[0], ct = [hdrs.type.N] - lib.trn(cookie == nil,1,0)
	})
end

terra convo:reroute(dest: rawstring) self:reroute_cookie(dest,nil) end
 
terra convo:complain(code: uint16, title: rawstring, msg: rawstring)
	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Cache-Control', value = 'no-store' }
	)

	var ti: lib.str.acc ti:compose('error :: ', title)
	var bo: lib.str.acc bo:compose('<div class="message"><img class="icon" src="/s/warn.webp"><h1>',title,'</h1><p>',msg,'</p></div>')
	var body = data.view.docskel {
		instance = self.srv.cfg.instance;
		title = ti:finalize();
		body = bo:finalize();
................................................................................
	in ok end
end)

struct convo.page {
	title: pstring
	body: pstring
	class: pstring
	cache: bool
}

terra convo:stdpage(pg: convo.page)
	var doc = data.view.docskel {
		instance = self.srv.cfg.instance;
		title = pg.title;
		body = pg.body;
		class = pg.class;
		navlinks = self.navbar;
	}

	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Cache-Control', value = 'no-store' }
	)

	doc:send(self.con,200,[lib.mem.ptr(lib.http.header)] {ct = [hdrs.type.N] - lib.trn(pg.cache,1,0), ptr = &hdrs[0]})
end

-- CALL ONLY ONCE PER VAR
terra convo:postv(name: rawstring)
	if self.varbuf.ptr == nil then
		self.varbuf = lib.mem.heapa(int8, self.msg.body.len + self.msg.query.len)
		self.vbofs = self.varbuf.ptr

Modified static/style.scss from [1f479ddac7] to [a08d589c48].

127
128
129
130
131
132
133

134
135
136
137
138
139
140
...
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379

380
381
382
383
384
385
386
387
...
541
542
543
544
545
546
547
548




















	tone(-50%),
	tone(-35%)
);

input[type='text'], input[type='password'], textarea {
	@extend %serif;
	padding: 0.08in 0.1in;

	border: 1px solid black;
	background: linear-gradient(to bottom, tone(-55%), tone(-40%));
	font-size: 16pt;
	color: tone(25%);
	box-shadow: inset 0 0 20px -3px tone(-55%);
	&:focus {
		color: white;
................................................................................
		> a { @extend %button; grid-column: 1 / 2; grid-row: 3/4; }
	}
}

form.compose {
	@extend %box;
	display: grid;
	grid-template-columns: 1.1in 2fr min-content 1fr;
	grid-template-rows: 1fr min-content;
	grid-gap: 2px;
	padding: 0.1in;
	> img { grid-column: 1/2; grid-row: 1/3; width: 1in; height: 1in;}
	> textarea {
		grid-column: 2/5; grid-row: 1/2; height: 3in;
		resize: vertical;
		margin-bottom: 0.08in;
	}
	> input[name="acl"] { grid-column: 2/3; grid-row: 2/3; }

	> button { grid-column: 4/5; grid-row: 2/3; }
	a.help[href] { margin-right: 0.05in }
}

a.help[href] {
	display: block;
	text-align: center;
	padding: 0.09in 0.2in;
................................................................................
			padding-top: 0.12in;
			background: linear-gradient(to right, tone(-50%), tone(-50%,-0.7));
			border: 1px solid tone(-55%);
			border-left: none;
			text-shadow: 1px 1px 0 black;
		}
	}
}



























>







 







|





|




>
|







 







|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
...
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
...
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
	tone(-50%),
	tone(-35%)
);

input[type='text'], input[type='password'], textarea {
	@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%);
	box-shadow: inset 0 0 20px -3px tone(-55%);
	&:focus {
		color: white;
................................................................................
		> a { @extend %button; grid-column: 1 / 2; grid-row: 3/4; }
	}
}

form.compose {
	@extend %box;
	display: grid;
	grid-template-columns: 1.1in 2fr min-content 1fr 1.5fr;
	grid-template-rows: 1fr min-content;
	grid-gap: 2px;
	padding: 0.1in;
	> img { grid-column: 1/2; grid-row: 1/3; width: 1in; height: 1in;}
	> textarea {
		grid-column: 2/6; grid-row: 1/2; height: 3in;
		resize: vertical;
		margin-bottom: 0.08in;
	}
	> input[name="acl"] { grid-column: 2/3; grid-row: 2/3; }
	> button[value="post"] { grid-column: 5/6; grid-row: 2/3; }
	> button[value="attach"] { grid-column: 4/5; grid-row: 2/3; }
	a.help[href] { margin-right: 0.05in }
}

a.help[href] {
	display: block;
	text-align: center;
	padding: 0.09in 0.2in;
................................................................................
			padding-top: 0.12in;
			background: linear-gradient(to right, tone(-50%), tone(-50%,-0.7));
			border: 1px solid tone(-55%);
			border-left: none;
			text-shadow: 1px 1px 0 black;
		}
	}

}

form {
	.elem {
		margin: 0.1in 0;
		label { display:block; font-weight: bold; padding: 0.03in 0; }
		.txtbox {
			@extend %serif;
			box-sizing: border-box;
			padding: 0.08in 0.1in;
			border: 1px solid black;
			background: tone(-55%);
		}
		input, textarea, .txtbox {
			display: block;
			width: 100%;
		}
		button { float: right; width: 50%; }
	}
}

Modified store.t from [763fd9ba8a] to [69369cc5c2].

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
..
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
...
191
192
193
194
195
196
197
198























199

200
201
202
203
204
205
206
...
209
210
211
212
213
214
215







216
217
218
219
220
221
222
...
273
274
275
276
277
278
279















































280
281
282
283
284
285
286
287
288
	relation = lib.enum {
		'follow', 'mute', 'block'
	};
	credset = lib.set {
		'pw', 'otp', 'challenge', 'trust'
	};
	privset = lib.set {
		'post', 'edit', 'acct', 'upload', 'censor', 'admin'
	};
	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

	};
	prepmode = lib.enum {
		'full','conf','admin'
	}
}

m.privmap = {}
................................................................................

struct m.source

struct m.rights {
	rank: uint16 -- lower = more powerful except 0 = regular user
	-- creating staff automatically assigns rank immediately below you
	quota: uint32 -- # of allowed tweets per day; 0 = no limit

	
	powers: m.powerset
}

terra m.rights_default()
	var pow: m.powerset pow:fill()
	(pow.purge << false)
	(pow.config << false)
	(pow.censor << false)
	(pow.suspend << false)
	(pow.elevate << false)
	(pow.demote << false)
	(pow.cred << false)
	(pow.rebrand << false)
	return m.rights { rank = 0, quota = 1000, powers = pow; }
end

struct m.actor {
	id: uint64
	nym: str
	handle: str
	origin: uint64
................................................................................
		var bytes = bits / 8
		var hexchs = bytes * 2
		var segs = hexchs / 4
		var seps = segs - 1
		var maxsz = hexchs + seps + 1
	else return nil end
end
























struct m.auth {

	aid: uint64
	uid: uint64
	aname: str
	netmask: m.inet
	privs: m.privset
	blacklist: bool
}
................................................................................
struct m.backend { id: rawstring
	open: &m.source -> &opaque
	close: &m.source -> {}
	dbsetup: &m.source -> bool -- creates the schema needed to call conprep (called only once per database e.g. with `parsav db init`)
	conprep: {&m.source, m.prepmode.t} -> {} -- prepares queries and similar tasks that require the schema to already be in place
	obliterate_everything: &m.source -> bool -- wipes everything parsav-related out of the database








	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_privs: {&m.source, &m.actor} -> {}
	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
................................................................................
	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
	auth_purge_trust: {&m.source, uint64, rawstring} -> {}

	post_save: {&m.source, &m.post} -> {}
	post_create: {&m.source, &m.post} -> uint64
	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))















































	convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post)
	convo_fetch_uid: {&m.source,uint64} -> lib.mem.ptr(m.post)

	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))
}

struct m.source {
	backend: &m.backend







|









|
>







 







>





|
|
|
|
|
|
|
|
|
|







 








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

>







 







>
>
>
>
>
>
>







 







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

|







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
..
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
...
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
...
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
...
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
	relation = lib.enum {
		'follow', 'mute', 'block'
	};
	credset = lib.set {
		'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 = {}
................................................................................

struct m.source

struct m.rights {
	rank: uint16 -- lower = more powerful except 0 = regular user
	-- creating staff automatically assigns rank immediately below you
	quota: uint32 -- # of allowed tweets per day; 0 = no limit
	invites: intptr -- # 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
	nym: str
	handle: str
	origin: uint64
................................................................................
		var bytes = bits / 8
		var hexchs = bytes * 2
		var segs = hexchs / 4
		var seps = segs - 1
		var maxsz = hexchs + seps + 1
	else return nil end
end

struct m.kompromat {
-- The Evidence
	id: uint64
	perp: uint64 -- whodunnit
	desc: str
	post: uint64 -- the post in question, if any
	reporter: uint64 -- 0 = originated automatically by the System itself
	resolution: str -- null for unresolved
	-- as proto: set resolution to empty string to search for resolved incidents
}

struct m.sanction {
	id: uint64
	issuer: uint64
	scope: uint64
	nature: uint16
	victim: uint64
	autoexpire: bool  expire: m.timepoint
	timedreview: bool review: m.timepoint
	reason: str
	context: str
}

struct m.auth {
-- a credential record
	aid: uint64
	uid: uint64
	aname: str
	netmask: m.inet
	privs: m.privset
	blacklist: bool
}
................................................................................
struct m.backend { id: rawstring
	open: &m.source -> &opaque
	close: &m.source -> {}
	dbsetup: &m.source -> bool -- creates the schema needed to call conprep (called only once per database e.g. with `parsav db init`)
	conprep: {&m.source, m.prepmode.t} -> {} -- prepares queries and similar tasks that require the schema to already be in place
	obliterate_everything: &m.source -> bool -- wipes everything parsav-related out of the database

	tx_enter: &m.source -> bool
	tx_complete: &m.source -> bool
	-- these two functions are special, in that they should be called
	-- directly on a specific backend, rather than passed down to the
	-- backends by the server; that is pathological behavior that will
	-- not have the desired effect

	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_privs: {&m.source, &m.actor} -> {}
	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
................................................................................
	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
	auth_purge_trust: {&m.source, uint64, rawstring} -> {}

	post_save: {&m.source, &m.post} -> {}
	post_create: {&m.source, &m.post} -> uint64
	post_enum_author_uid: {&m.source, uint64, m.range} -> 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
	artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64
		-- instantiate an artifact in the database, either installing a new
		-- artifact or returning the id of an existing artifact with the same hash
			-- artifact: bytea
			-- mime:     pstring
	artifact_quicksearch: {&m.source, lib.mem.ptr(uint8)} -> {uint64,bool}
		-- checks whether a hash is already in the database without uploading
		-- the entire file to the database server
			-- hash: bytea
				--> artifact id (0 if null), suppressed?
	artifact_expropriate: {&m.source, uint64, uint64, lib.mem.ptr(int8)} -> {}
		-- claims an existing artifact for the user's own collection
			-- uid:         uint64
			-- artifact id: uint64
			-- description: pstring
	artifact_disclaim: {&m.source, uint64, uint64} -> {}
		-- a user disclaims their ownership stake in an artifact, removing it from
		-- the database entirely if they were the only owner, and removing their
		-- description of it either way
			-- uid:         uint64
			-- artifact id: uint64
	artifact_excise: {&m.source, uint64, bool} -> {}
		-- (admin action) forcibly excise an artifact from the database, deleting
		-- all links to it and removing it from users' collections. if "blacklist,"
		-- the artifact will be banned and attempts to upload it in the future
		-- will fail, triggering a report. mainly intended for dealing with spam,
		-- IP violations, That Which Shall Not Be Named, and various other infohazards.
			-- artifact id: uint64
			-- blacklist:   bool

	nkvd_report_issue: {&m.source, &m.kompromat} -> {}
		-- an incidence of Badthink has been detected. report it immediately
		-- to the Supreme Soviet
	nkvd_reports_enum: {&m.source, &m.kompromat} -> lib.mem.ptr(m.kompromat)
		-- search through the Archives
			-- 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} -> {}

	convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post)
	convo_fetch_cid: {&m.source,uint64} -> lib.mem.ptr(m.post)

	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))
}

struct m.source {
	backend: &m.backend

Modified view/compose.tpl from [5ccb8d92d6] to [a602850007].

1
2
3
4
5

6
7
8
9
10
11
12
<form class="compose" method="post">
	<img src="/avi/@handle">
	<textarea autofocus name="post" placeholder="it was a dark and stormy night…">@!content</textarea>
	<input required autocomplete="on" type="text" name="acl" class="acl" value="@acl" list="scopes" placeholder="access control"> @?acl
	<button type="submit">commit</button>

</form>

<datalist id="scopes">
	<option>all</option>
	<option>mentioned</option>
	<option>local</option>
	<option>mutual</option>




|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
<form class="compose" method="post">
	<img src="/avi/@handle">
	<textarea autofocus name="post" placeholder="it was a dark and stormy night…">@!content</textarea>
	<input required autocomplete="on" type="text" name="acl" class="acl" value="@acl" list="scopes" placeholder="access control"> @?acl
	<button type="submit" name="act" value="post">commit</button>
	<button type="submit" name="act" value="attach">attach</button>
</form>

<datalist id="scopes">
	<option>all</option>
	<option>mentioned</option>
	<option>local</option>
	<option>mutual</option>

Modified view/conf-profile.tpl from [746111dd26] to [f1f77d7014].

1
2
3
4
5
6
<form method="post">
	<label>handle <div class="txtbox">@!handle</div></label>
	<label>display name <input type="text" name="nym" value="@:nym"></label>
	<label>bio <textarea name="bio">@!bio</textarea></label>
	<input type="submit" value="commit">
</form>

|
|
|
|

1
2
3
4
5
6
<form method="post">
	<div class="elem"><label>handle</label> <div class="txtbox">@!handle</div>
	<div class="elem"><label for="nym">display name</label> <input type="text" name="nym" id="nym" placeholder="j. random poster" value="@:nym"></div>
	<div class="elem"><label for="bio">bio</label><textarea name="bio" id="bio" placeholder="tall, dark, and mysterious">@!bio</textarea></div>
	<button>commit</button>
</form>