yosup/web/js/model.js
2022-12-21 16:08:11 -08:00

326 lines
8.7 KiB
JavaScript

/* model_process_event is the main point where events are post-processed from
* a relay. Additionally other side effects happen such as notification checks
* and fetching of unknown pubkey profiles.
*/
function model_process_event(model, ev) {
if (model.all_events[ev.id]) {
return;
}
model.all_events[ev.id] = ev;
ev.refs = event_get_tag_refs(ev.tags);
ev.pow = event_calculate_pow(ev);
// Process specific event needs based on it's kind. Not using a map because
// integers can't be used.
let fn;
switch(ev.kind) {
case KIND_METADATA:
fn = model_process_event_profile;
break;
case KIND_CONTACT:
fn = model_process_event_contact;
break;
case KIND_DELETE:
fn = model_process_event_deletion;
break;
case KIND_REACTION:
fn = model_process_event_reaction;
break;
}
if (fn)
fn(model, ev);
// check if the event that just came in should notify the user and is newer
// than the last recorded notification event, if it is notify
const notify_user = event_refs_pubkey(ev, model.pubkey)
const last_notified = get_local_state('last_notified_date')
ev.notified = notify_user;
if (notify_user && (last_notified == null || ((ev.created_at*1000) > last_notified))) {
set_local_state('last_notified_date', new Date().getTime());
model.notifications++;
update_title(model);
}
// If we find some unknown ids lets schedule their subscription for info
if (model_event_has_unknown_ids(model, ev))
schedule_unknown_refetch(model);
// Queue event for rendering
model.invalidated.push(ev.id);
}
/* model_process_event_profile updates the matching profile with the contents found
* in the event.
*/
function model_process_event_profile(model, ev) {
const prev_ev = model.all_events[model.profile_events[ev.pubkey]]
if (prev_ev && prev_ev.created_at > ev.created_at)
return
model.profile_events[ev.pubkey] = ev.id
model.profiles[ev.pubkey] = safe_parse_json(ev.content, "profile contents")
view_timeline_update_profiles(model, ev);
}
function model_process_event_contact(model, ev) {
contacts_process_event(model.contacts, model.pubkey, ev)
load_our_relays(model.pubkey, model.pool, ev)
}
/* model_process_event_reaction updates the reactions dictionary
*/
function model_process_event_reaction(model, ev) {
let reaction = event_parse_reaction(ev);
if (!reaction) {
return;
}
if (!model.reactions_to[reaction.e])
model.reactions_to[reaction.e] = new Set();
model.reactions_to[reaction.e].add(ev.id);
view_timeline_update_reaction(model, ev);
}
/* event_process_deletion updates the list of deleted events. Additionally
* pushes event ids onto the invalidated stack for any found.
*/
function model_process_event_deletion(model, ev) {
for (const tag of ev.tags) {
if (tag.length >= 2 && tag[0] === "e" && tag[1]) {
let evid = tag[1];
model.invalidated.push(evid);
model_remove_reaction(model, evid);
if (model.deleted[evid])
continue;
let ds = model.deletions[evid] =
(model.deletions[evid] || new Set());
ds.add(ev.id);
}
}
}
function model_remove_reaction(model, evid) {
// deleted_ev -> target_ev -> original_ev
// Here we want to find the original react event to and remove it from our
// reactions map, then we want to update the element on the page. If the
// server does not clean up events correctly the increment/decrement method
// should work fine in theory.
const target_ev = model.all_events[evid];
if (!target_ev)
return;
const reaction = event_parse_reaction(target_ev);
if (!reaction)
return;
if (model.reactions_to[reaction.e])
model.reactions_to[reaction.e].delete(target_ev.id);
view_timeline_update_reaction(model, target_ev);
}
/* model_event_has_unknown_ids checks the event if there are any referenced keys with
* unknown user profiles in the provided scope.
*/
function model_event_has_unknown_ids(damus, ev) {
// make sure this event itself is removed from unknowns
if (ev.kind === 0)
delete damus.unknown_pks[ev.pubkey]
delete damus.unknown_ids[ev.id]
let got_some = false
for (const tag of ev.tags) {
if (tag.length >= 2) {
if (tag[0] === "p") {
const pk = tag[1]
if (!model_has_profile(damus, pk) && is_valid_id(pk)) {
got_some = true
damus.unknown_pks[pk] = make_unk(tag[2], ev)
}
} else if (tag[0] === "e") {
const evid = tag[1]
if (!model_has_event(damus, evid) && is_valid_id(evid)) {
got_some = true
damus.unknown_ids[evid] = make_unk(tag[2], ev)
}
}
}
}
return got_some
}
function model_is_event_deleted(model, evid) {
// we've already know it's deleted
if (model.deleted[evid])
return model.deleted[evid]
const ev = model.all_events[evid]
if (!ev)
return false
// all deletion events
const ds = model.deletions[ev.id]
if (!ds)
return false
// find valid deletion events
for (const id of ds.keys()) {
const d_ev = model.all_events[id]
if (!d_ev)
continue
// only allow deletes from the user who created it
if (d_ev.pubkey === ev.pubkey) {
model.deleted[ev.id] = d_ev
delete model.deletions[ev.id]
return true
}
}
return false
}
function model_has_profile(model, pk) {
return pk in model.profiles
}
function model_has_event(model, evid) {
return evid in model.all_events
}
/* model_relay_update_lok returns a map of kinds found in all events based on
* the last seen event of each kind. It also updates the model's cached value.
* If the cached value is found it returns that instead
*/
function model_relay_update_lok(model, relay) {
let last_of_kind = model.last_event_of_kind[relay];
if (!last_of_kind) {
last_of_kind = model.last_event_of_kind[relay]
= calculate_last_of_kind(model.all_events);
}
return last_of_kind;
}
function model_subscribe_defaults(model, relay) {
const lok = model_relay_update_lok(model, relay);
const filters = filters_new_default_since(model, lok);
filters_subscribe(filters, model.pool, [relay]);
}
function model_events_arr(model) {
const events = model.all_events;
let arr = [];
for (const evid in events) {
const ev = events[evid];
const i = arr_bsearch_insert(arr, ev, event_cmp_created);
arr.splice(i, 0, ev);
}
return arr;
}
function test_model_events_arr() {
const arr = model_events_arr({all_events: {
"c": {name: "c", created_at: 2},
"a": {name: "a", created_at: 0},
"b": {name: "b", created_at: 1},
"e": {name: "e", created_at: 4},
"d": {name: "d", created_at: 3},
}});
let last;
while(arr.length > 0) {
let ev = arr.pop();
log_debug("test:", ev.name, ev.created_at);
if (!last) {
last = ev;
continue;
}
if (ev.created_at > last.created_at) {
log_error(`ev ${ev.name} should be before ${last.name}`);
}
last = ev;
}
}
async function model_save_events(model) {
function _events_save(ev, resolve, reject) {
const db = ev.target.result;
let tx = db.transaction("events", "readwrite");
let store = tx.objectStore("events");
for (const evid in model.all_events) {
store.put(model.all_events[evid]);
}
tx.oncomplete = (ev) => {
db.close();
resolve();
log_debug("saved events!");
};
tx.onerror = (ev) => {
db.close();
log_error("failed to save events");
reject(ev);
};
}
return dbcall(_events_save);
}
async function model_load_events(model, fn) {
function _events_load(ev, resolve, reject) {
const db = ev.target.result;
const tx = db.transaction("events", "readonly");
const store = tx.objectStore("events");
const cursor = store.openCursor();
cursor.onsuccess = (ev) => {
var cursor = ev.target.result;
if (cursor) {
fn(cursor.value);
cursor.continue();
} else {
db.close();
resolve();
log_debug("Successfully loaded events");
}
}
cursor.onerror = (ev) => {
db.close();
reject(ev);
log_error("Could not load events.");
};
}
return dbcall(_events_load);
}
function new_model() {
return {
all_events: {}, // our master list of all events
done_init: {},
notifications: 0,
max_depth: 2,
reactions_to: {},
chatrooms: {},
unknown_ids: {},
unknown_pks: {},
but_wait_theres_more: 0,
deletions: {},
deleted: {},
last_event_of_kind: {},
pow: 0, // pow difficulty target
profiles: {}, // pubkey => profile data
profile_events: {}, // pubkey => event id - use with all_events
contacts: {
event: null,
friends: new Set(),
friend_of_friends: new Set(),
},
invalidated: [], // event ids which should be added/removed
elements: {}, // map of evid > rendered element
requested_profiles: [], // an array of {relay_id, pubkey} to fetching
ids: {
comments: "comments",
explore: "explore",
refevents: "refevents",
account: "account",
home: "home",
contacts: "contacts",
notifications: "notifications",
unknowns: "unknowns",
dms: "dms",
},
};
}