-- vim: ft=terra
local m = {
timepoint = lib.osclock.time_t;
noticetype = lib.enum {
-- only add new values to the end of this list! the numerical value
-- is stored in the database and must be kept synchronized across versions
'none', 'mention', 'reply', 'like', 'rt', 'react', 'follow', 'followreq'
};
relation = lib.set {
'follow',
'sub', -- get a notification for every post
'mute', -- posts will be completely hidden at all times
'block', -- no interactions will be permitted, but posts will remain visible
'silence', -- messages will not be accepted
'collapse', -- posts will be collapsed by default
'disemvowel', -- posts will be ritually humiliated, but shown
'attenuate', -- user's retweets will not be shown
'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations
'exclude', -- own posts will not be visible to this user
};
credset = lib.set {
'pw', 'otp', 'challenge', 'trust'
};
privset = lib.set {
'post', 'edit', 'account', 'upload', 'artifact', 'moderate', 'admin', 'invite'
};
powerset = lib.set {
-- user powers -- default on
'login', -- not locked out
'visible', -- account & posts can be seen by others
'edit'; -- edit own poasts
'post', -- can do poasts
'shout', -- posts show up on local timeline
'propagate', -- posts are sent to other instances
'artifact', -- upload, claim, and manage artifacts
'account', -- configure own account
'snitch'; -- can issue badthink reports
-- admin powers -- default off
'censor', -- dispose of badthink
'herald', -- grant serverwide epithets and badges
'crier', -- can promote content to the instance page
'invite', -- *unlimited* invites
'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
'config', -- change daemon policy & config UI
'rebrand', -- modify site's brand identity
'purge' -- permanently delete users
};
prepmode = lib.enum {
'full','conf','admin'
}
}
local function setmap(set)
local map = {}
local struct pt { name:lib.mem.ptr(int8), val:set }
for i,v in ipairs(set.members) do
map[#map + 1] = quote
var ps: set ps:clear()
(ps.[v] << true)
in pt {name = [v], val = ps} end
end
return map
end
m.powmap = setmap(m.powerset)
m.privmap = setmap(m.privset)
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
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: 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.account << true)
(pow.edit << true)
(pow.snitch << true)
return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; }
end
struct m.relationship {
agent: uint64
patient: uint64
rel: m.relation -- agent → patient
recip: m.relation -- patient → agent
}
struct m.actor {
id: uint64
nym: str
handle: str
origin: uint64
bio: str
epithet: str
avatar: str
avatarid: uint64
knownsince: m.timepoint
rights: m.rights
key: lib.mem.ptr(uint8)
-- ephemera
xid: str
source: &m.source
relationship: m.relationship -- relationship to the logged-in user, if any
}
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 derkey = lib.crypt.der(false,&newkp,kbuf)
return m.actor {
id = 0; nym = nil; handle = nil;
origin = 0; bio = nil; avatar = nil;
knownsince = lib.osclock.time(nil);
rights = m.rights_default();
avatarid = 0;
epithet = nil, key = derkey;
}
end
struct m.actor_stats {
posts: intptr
follows: intptr
followers: intptr
mutuals: intptr
}
struct m.range {
mode: uint8 -- 0 == I->I, 1 == T->I, 2 == I->T, 3 == T->T
union {
from_time: m.timepoint
from_idx: uint64
}
union {
to_time: m.timepoint
to_idx: uint64
}
}
terra m.range:matrix()
if self.mode == 0 then
return self.from_time,self.to_time,0,0
elseif self.mode == 1 then
return self.from_time,0,self.to_idx,0
elseif self.mode == 2 then
return 0,self.to_time,0,self.from_idx
elseif self.mode == 3 then
return 0,0,self.to_idx,self.from_idx
else lib.bail('invalid mode on timeline range!') end
end
struct m.post {
id: uint64
uri: str
author: uint64
subject: str
body: str
acl: str
posted: m.timepoint
discovered: m.timepoint
edited: m.timepoint
chgcount: uint
mentions: lib.mem.ptr(uint64)
circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
convoheaduri: str
parent: uint64
-- ephemera
localpost: bool
accent: int16
rts: uint32
likes: uint32
rtdby: uint64 -- 0 if not rt
rtdat: m.timepoint -- 0 if not rt, time of promotion otherwise
rtact: uint64 -- 0 if not rt, id of rt action otherwise
isreply: bool
source: &m.source
-- save :: bool -> {} (defined in acl.t due to dep. hell)
}
struct m.circle {
cid: uint64
owner: uint64
name: lib.str.t
-- ephemera
memcount: intptr
}
struct m.artifact {
rid: uint64
owner: uint64
desc: str
folder: str
mime: str
url: str
}
m.user_conf_funcs = function(be,n,mem,ty,rty,rty2)
rty = rty or ty
local gt
if not mem then
if not rty2 -- what the fuck?
then gt = {&m.source, uint64, lib.str.t} -> rty;
else gt = {&m.source, uint64, lib.str.t} -> {rty, rty2};
end
else
if not rty2 -- what the fuck?
then gt = {&m.source, &lib.mem.pool, uint64, lib.str.t} -> rty;
else gt = {&m.source, &lib.mem.pool, uint64, lib.str.t} -> {rty, rty2};
end
end
for k, t in pairs {
enum = {&m.source, &lib.mem.pool, uint64, lib.str.t} -> lib.mem.ptr(rty);
get = gt;
set = {&m.source, uint64, lib.str.t, ty} -> {};
reset = {&m.source, uint64, lib.str.t} -> {};
} do
be.entries[#be.entries+1] = {
field = 'actor_conf_'..n..'_'..k, type = t
}
end
end
struct m.notice {
kind: m.noticetype.t
when: uint64
who: uint64
what: uint64
union {
reply: uint64
reaction: int8[32] -- are you shitting me, unichode
}
}
struct m.inet {
pv: uint8 -- 0 = null, 4 = ipv4, 6 = ipv6
union {
v4: uint8[4]
v6: uint8[16]
}
union {
fixbits: uint8 -- for cidr
port: uint16 -- for origin
}
}
terra m.inet:cidr_str()
if self.pv == 4 then
var maxsz = 3*4 + 3 + 1
elseif self.pv == 6 then
var bits = 128
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
kind: str
aname: str
comment: str
netmask: m.inet
privs: m.privset
blacklist: bool
}
-- backends only handle content on the local server
local pstring = lib.str.t
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
server_setup_self: {&m.source, rawstring, lib.mem.ptr(uint8)} -> {}
conf_get: {&m.source, lib.str.t} -> lib.mem.ptr(int8)
conf_set: {&m.source, lib.str.t, lib.str.t} -> {}
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_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
-- username: rawstring
actor_auth_otp: {&m.source, m.inet, rawstring, rawstring}
-> {uint64, uint64, pstr}
actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) }
-> {uint64, uint64, pstr}
-- handles password-based logins against hashed passwords
-- origin: inet
-- handle: rawstring
-- token: rawstring
actor_auth_challenge: {&m.source, m.inet, pstr, lib.mem.ptr(uint8), pstr }
-> {uint64, uint64, pstr}
-- origin: inet
-- handle: rawstring
-- response: rawstring
-- challenge token: pstring
actor_auth_tls: {&m.source, m.inet, rawstring}
-> {uint64, uint64, pstr}
-- handles implicit authentication performed as part of an TLS connection
-- origin: inet
-- fingerprint: rawstring
actor_auth_api: {&m.source, m.inet, rawstring, rawstring} -> uint64
-> {uint64, uint64, pstr}
-- handles API authentication
-- origin: inet
-- handle: rawstring
-- key: rawstring (X-API-Key)
actor_auth_record_fetch: {&m.source, uint64} -> lib.mem.ptr(m.auth)
actor_powers_fetch: {&m.source, uint64} -> m.powerset
actor_session_fetch: {&m.source, uint64, m.inet, m.timepoint} -> {lib.stat(m.auth), lib.mem.ptr(m.actor)}
-- retrieves an auth record + actor combo suitable by AID suitable
-- for determining session validity & caps
-- aid: uint64
-- 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
actor_notice_enum: {&m.source, uint64} -> lib.mem.ptr(m.notice)
actor_rel_create: {&m.source, uint16, uint64, uint64} -> {}
actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {}
actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship
auth_enum_uid: {&m.source, uint64} -> lib.mem.lstptr(m.auth)
auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
auth_fetch_aid : {&m.source, uint64} -> lib.mem.ptr(m.auth)
auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> uint64
auth_attach_rsa: {&m.source, uint64, bool, lib.mem.ptr(uint8), pstr} -> uint64
-- uid: uint64
-- reset: bool (delete other passwords?)
-- pw: pstring
-- comment: pstring
auth_privs_set: {&m.source, uint64, m.privset} -> {}
auth_destroy_aid: {&m.source, uint64} -> {}
auth_destroy_aid_uid: {&m.source, uint64, uint64} -> {}
auth_purge_pw: {&m.source, uint64, rawstring} -> {}
auth_purge_otp: {&m.source, uint64, rawstring} -> {}
auth_purge_trust: {&m.source, uint64, rawstring} -> {}
auth_sigtime_user_fetch: {&m.source, uint64} -> m.timepoint
-- authentication tokens and accounts have a property that controls
-- whether auth cookies dated to a certain point are valid. cookies
-- that are generated before the timepoint are considered invalid.
-- this is used primarily to lock out untrusted sessions.
-- uid: uint64
auth_sigtime_user_alter: {&m.source, uint64, m.timepoint} -> {}
-- 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
post_retweet: {&m.source, uint64, uint64, bool} -> {}
post_like: {&m.source, uint64, uint64, bool} -> {}
-- undo: bool
post_react: {&m.source, uint64, uint64, pstring} -> {}
-- emoji: pstring (null to delete previous reaction, otherwise adds/changes)
post_act_cancel: {&m.source, uint64} -> {}
post_liked_uid: {&m.source, uint64, uint64} -> bool
post_reacted_uid: {&m.source, uint64, uint64} -> bool
post_act_fetch_notice: {&m.source, uint64} -> m.notice
circle_search: {&m.source, &lib.mem.pool, uint64, uint64} -> lib.mem.ptr(m.circle)
circle_create: {&m.source, uint64, lib.str.t} -> uint64
circle_destroy: {&m.source, uint64, uint64} -> {}
circle_members_fetch_cid: {&m.source, &lib.mem.pool, uint64} -> lib.mem.ptr(uint64)
circle_members_fetch_name: {&m.source, &lib.mem.pool, uint64, pstring} -> lib.mem.ptr(uint64)
circle_members_add_uid: {&m.source, uint64, uint64} -> {}
circle_members_del_uid: {&m.source, uint64, uint64} -> {}
circle_memberships_uid: {&m.source, &lib.mem.pool, uint64, uint64} -> lib.mem.ptr(m.circle)
thread_top_find: {&m.source, uint64} -> uint64
-- NOTE: this won't work if conversations are broken across multiple data sources!
-- if this is a thing that's likely to happen, the overlord-side wrapper for this
-- function (srv.t) should implement a more sophisticated algorithm over all the
-- data sources, instead of just stopping when one parent is found
thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint
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.str.t, lib.str.t} -> {}
-- claims an existing artifact for the user's own collection
-- uid: uint64
-- artifact id: uint64
-- description: pstring
-- folder: pstring
artifact_claim_alter: {&m.source, uint64, uint64, lib.str.t, lib.str.t} -> {}
-- edits an existing claim to an artifact
-- ibid
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
artifact_enum_uid: {&m.source, uint64, lib.str.t} -> lib.mem.lstptr(m.artifact)
-- produces a list of artifacts claimed by a user, optionally
-- restricted by folder (empty string = new only)
artifact_fetch: {&m.source, uint64, uint64} -> lib.mem.ptr(m.artifact)
-- fetch a user's view of an artifact
-- uid: uint64
-- rid: uint64
artifact_load: {&m.source, uint64} -> {lib.mem.ptr(uint8),lib.str.t}
-- load the body of an artifact into memory (also returns mime)
artifact_folder_enum: {&m.source, uint64} -> lib.mem.ptr(lib.str.t)
-- enumerate all of a user's folders
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} -> {}
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)
timeline_circle_fetch: {&m.source, uint64, m.range} -> lib.mem.lstptr(m.post)
}
m.user_conf_funcs(m.backend, 'str', true, lib.str.t, lib.str.t)
m.user_conf_funcs(m.backend, 'int', false, intptr, intptr, bool)
struct m.source {
backend: &m.backend
id: lib.mem.ptr(int8)
handle: &opaque
string: lib.mem.ptr(int8)
}
terra m.source:free()
self.id:free()
self.string:free()
end
m.source.metamethods.__methodmissing = macro(function(meth, obj, ...)
local q = {...}
-- syntax sugar to forward unrecognized calls onto the backend
return quote var r = obj.backend.[meth](&obj, [q]) in r end
end)
return m