data endpoint for entity 90008 (aka. a website)
1import { scopeCookies } from '$lib/index.ts';
2import type { Cookies } from '@sveltejs/kit';
3import { nanoid } from 'nanoid';
4import { get, writable } from 'svelte/store';
5import { darkVisitors } from './darkvisitors.ts';
6
7export type Visitor = { visits: number[] };
8export const lastVisitors = writable<Map<string, Visitor>>(new Map());
9const VISITOR_EXPIRY_SECONDS = 60 * 60; // an hour seems reasonable
10
11export const removeLastVisitor = (id: string) => {
12 const visitors = get(lastVisitors);
13 if (visitors.has(id)) {
14 const visitor = visitors.get(id) ?? { visits: [] };
15 visitor?.visits.shift();
16 // if not enough visits remove
17 if (visitor?.visits.length === 0) {
18 visitors.delete(id);
19 } else {
20 visitors.set(id, visitor);
21 }
22 }
23 lastVisitors.set(visitors);
24};
25
26export const addLastVisitor = (request: Request, cookies: Cookies) => {
27 const { visitors, visitorId } = _addLastVisitor(get(lastVisitors), request, cookies);
28 lastVisitors.set(visitors);
29 return visitorId;
30};
31
32export const getVisitorId = (cookies: Cookies) => {
33 const scopedCookies = scopeCookies(cookies, '/');
34 // parse the last visit timestamp from cookies if it exists
35 return scopedCookies.get('visitorId');
36};
37
38// why not use this for incrementVisitCount? cuz i wanna have separate visit counts (one per hour and one per day, per hour being recent visitors)
39const _addLastVisitor = (visitors: Map<string, Visitor>, request: Request, cookies: Cookies) => {
40 const currentTime = Date.now();
41 // filter out old entries
42 visitors.forEach((visitor, id, map) => {
43 if (currentTime - visitor.visits[0] > 1000 * VISITOR_EXPIRY_SECONDS) map.delete(id);
44 else {
45 visitor.visits = visitor.visits.filter((since) => {
46 return currentTime - since < 1000 * VISITOR_EXPIRY_SECONDS;
47 });
48 map.set(id, visitor);
49 }
50 });
51 // check whether the request is from a bot or not (this doesnt need to be accurate we just want to filter out honest bots)
52 if (isBot(request)) return { visitors, visitorId: null };
53 const scopedCookies = scopeCookies(cookies, '/');
54 // parse the last visit timestamp from cookies if it exists
55 let visitorId = scopedCookies.get('visitorId') || '';
56 // if no such id exists, create one and assign it to the client
57 if (!visitors.has(visitorId)) {
58 visitorId = nanoid();
59 scopedCookies.set('visitorId', visitorId);
60 console.log(`new client visitor id ${visitorId}`);
61 }
62 // update the entry
63 const visitorEntry = visitors.get(visitorId) || { visits: [] };
64 // put new visit in the front
65 visitorEntry.visits = [currentTime].concat(visitorEntry.visits);
66 visitors.set(visitorId, visitorEntry);
67 return {
68 visitors,
69 visitorId
70 };
71};
72
73export const isBot = (request: Request) => {
74 const ua = request.headers.get('user-agent');
75 return ua
76 ? ua.toLowerCase().match(/(bot|crawl|spider|walk|fetch|scrap|proxy|image)/) !== null
77 : true;
78};
79
80export const notifyDarkVisitors = (url: URL, request: Request) => {
81 const headers = Object.fromEntries(request.headers.entries());
82 try {
83 darkVisitors.trackVisit({
84 path: url.pathname,
85 method: request.method,
86 headers: headers
87 });
88 } catch (error) {
89 console.error('failed to notify dark visitors:', error);
90 }
91};