data endpoint for entity 90008 (aka. a website)
at svelte 5.1 kB view raw
1import { env } from '$env/dynamic/private'; 2import { scopeCookies } from '$lib'; 3import type { Cookies } from '@sveltejs/kit'; 4import { nanoid } from 'nanoid'; 5import { get, writable } from 'svelte/store'; 6 7const visitCountFile = `${env.WEBSITE_DATA_DIR}/visitcount`; 8const readVisitCount = async () => { 9 try { 10 return parseInt(await Deno.readTextFile(visitCountFile)); 11 } catch { 12 return 0; 13 } 14}; 15export const visitCount = writable(await readVisitCount()); 16 17export type Visitor = { visits: number[] }; 18export const lastVisitors = writable<Map<string, Visitor>>(new Map()); 19const VISITOR_EXPIRY_SECONDS = 60 * 60; // an hour seems reasonable 20 21export const decrementVisitCount = () => { 22 visitCount.set(get(visitCount) - 1); 23}; 24 25export const incrementVisitCount = async (request: Request, cookies: Cookies) => { 26 let currentVisitCount = get(visitCount); 27 // check whether the request is from a bot or not (this doesnt need to be accurate we just want to filter out honest bots) 28 if (isBot(request)) return false; 29 const scopedCookies = scopeCookies(cookies, '/'); 30 // parse the last visit timestamp from cookies if it exists 31 const visitedTimestamp = parseInt(scopedCookies.get('visitedTimestamp') || '0'); 32 // get unix timestamp 33 const currentTime = Date.now(); 34 const timeSinceVisit = currentTime - visitedTimestamp; 35 // check if this is the first time a client is visiting or if an hour has passed since they last visited 36 if (visitedTimestamp === 0 || timeSinceVisit > 1000 * 60 * 60 * 24) { 37 // increment current and write to the store 38 currentVisitCount += 1; 39 visitCount.set(currentVisitCount); 40 // update the cookie with the current timestamp 41 scopedCookies.set('visitedTimestamp', currentTime.toString()); 42 // write the visit count to a file so we can load it later again 43 await Deno.writeTextFile(visitCountFile, currentVisitCount.toString()); 44 } 45 return true; 46}; 47 48export const removeLastVisitor = (id: string) => { 49 const visitors = get(lastVisitors); 50 if (visitors.has(id)) { 51 const visitor = visitors.get(id) ?? { visits: [] }; 52 visitor?.visits.shift(); 53 // if not enough visits remove 54 if (visitor?.visits.length === 0) { 55 visitors.delete(id); 56 } else { 57 visitors.set(id, visitor); 58 } 59 } 60 lastVisitors.set(visitors); 61}; 62 63export const addLastVisitor = (request: Request, cookies: Cookies) => { 64 const { visitors, visitorId } = _addLastVisitor(get(lastVisitors), request, cookies); 65 lastVisitors.set(visitors); 66 return visitorId; 67}; 68 69export const getVisitorId = (cookies: Cookies) => { 70 const scopedCookies = scopeCookies(cookies, '/'); 71 // parse the last visit timestamp from cookies if it exists 72 return scopedCookies.get('visitorId'); 73}; 74 75// 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) 76const _addLastVisitor = (visitors: Map<string, Visitor>, request: Request, cookies: Cookies) => { 77 const currentTime = Date.now(); 78 // filter out old entries 79 visitors.forEach((visitor, id, map) => { 80 if (currentTime - visitor.visits[0] > 1000 * VISITOR_EXPIRY_SECONDS) map.delete(id); 81 else { 82 visitor.visits = visitor.visits.filter((since) => { 83 return currentTime - since < 1000 * VISITOR_EXPIRY_SECONDS; 84 }); 85 map.set(id, visitor); 86 } 87 }); 88 // check whether the request is from a bot or not (this doesnt need to be accurate we just want to filter out honest bots) 89 if (isBot(request)) return { visitors, visitorId: null }; 90 const scopedCookies = scopeCookies(cookies, '/'); 91 // parse the last visit timestamp from cookies if it exists 92 let visitorId = scopedCookies.get('visitorId') || ''; 93 // if no such id exists, create one and assign it to the client 94 if (!visitors.has(visitorId)) { 95 visitorId = nanoid(); 96 scopedCookies.set('visitorId', visitorId); 97 console.log(`new client visitor id ${visitorId}`); 98 } 99 // update the entry 100 const visitorEntry = visitors.get(visitorId) || { visits: [] }; 101 // put new visit in the front 102 visitorEntry.visits = [currentTime].concat(visitorEntry.visits); 103 visitors.set(visitorId, visitorEntry); 104 return { 105 visitors, 106 visitorId 107 }; 108}; 109 110export const isBot = (request: Request) => { 111 const ua = request.headers.get('user-agent'); 112 return ua 113 ? ua.toLowerCase().match(/(bot|crawl|spider|walk|fetch|scrap|proxy|image)/) !== null 114 : true; 115}; 116 117export const notifyDarkVisitors = (url: URL, request: Request) => { 118 fetch('https://api.darkvisitors.com/visits', { 119 method: 'POST', 120 headers: { 121 authorization: `Bearer ${env.DARK_VISITORS_TOKEN}`, 122 'content-type': 'application/json' 123 }, 124 body: JSON.stringify({ 125 request_path: url.pathname, 126 request_method: request.method, 127 request_headers: request.headers 128 }) 129 }) 130 .catch((why) => { 131 console.log('failed sending dark visitors analytics:', why); 132 return null; 133 }) 134 .then((resp) => { 135 if (resp !== null) { 136 const host = `(${request.headers.get('host')}|${request.headers.get('x-real-ip')}|${request.headers.get('user-agent')})`; 137 console.log(`sent visitor analytic to dark visitors: ${resp.statusText}; ${host}`); 138 } 139 }); 140};