Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ dependencies {
implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3")
implementation("org.postgresql:postgresql:42.7.3")

// Caching
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("com.github.ben-manes.caffeine:caffeine")

runtimeOnly("org.postgresql:postgresql")

// Development tools
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/paradaux/api/ParadauxApiApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
@EnableCaching
public class ParadauxApiApplication {

public static void main(String[] args) {
Expand Down
28 changes: 0 additions & 28 deletions src/main/java/io/paradaux/api/controllers/IfumController.java

This file was deleted.

28 changes: 28 additions & 0 deletions src/main/java/io/paradaux/api/controllers/VisitsController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.paradaux.api.controllers;

import io.paradaux.api.models.VisitEntry;
import io.paradaux.api.models.annotations.ProtectedRoute;
import io.paradaux.api.services.VisitsService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/analytics/visits")
public class VisitsController {

private final VisitsService visitsService;

public VisitsController(VisitsService visitsService) {
this.visitsService = visitsService;
}

@ProtectedRoute
@PostMapping("/visit")
public void insertVisit(@RequestBody VisitEntry visit) {
visitsService.insertVisit(visit);
}

@GetMapping("/project/{project}")
public Integer getVisitCountByProject(@PathVariable String project) {
return visitsService.getVisitCount(project);
}
}
23 changes: 23 additions & 0 deletions src/main/java/io/paradaux/api/jobs/VisitCacheRefreshJob.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.paradaux.api.jobs;

import io.paradaux.api.mappers.VisitsMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class VisitCacheRefreshJob {

private final VisitsMapper visitsMapper;

public VisitCacheRefreshJob(VisitsMapper visitsMapper) {
this.visitsMapper = visitsMapper;
}

@Scheduled(fixedRate = 5 * 60 * 1000) // 5 mins
public void refreshMaterializedView() {
visitsMapper.refreshVisitCache();
log.info("Visit cache refreshed successfully.");
}
}
10 changes: 0 additions & 10 deletions src/main/java/io/paradaux/api/mappers/IfumMapper.java

This file was deleted.

11 changes: 11 additions & 0 deletions src/main/java/io/paradaux/api/mappers/VisitsMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.paradaux.api.mappers;

import io.paradaux.api.models.VisitEntry;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface VisitsMapper {
void insertVisit(VisitEntry visit);
Integer getVisitCount(String project);
void refreshVisitCache();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
import java.sql.Timestamp;

@Data
public class IfumVisit {
public class VisitEntry {
private String ipAddress;
private String userAgent;
private int id;
private Timestamp createdAt;
private String project;
}
17 changes: 0 additions & 17 deletions src/main/java/io/paradaux/api/services/IfumService.java

This file was deleted.

22 changes: 22 additions & 0 deletions src/main/java/io/paradaux/api/services/VisitsService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.paradaux.api.services;

import io.paradaux.api.models.VisitEntry;

public interface VisitsService {
/**
* Inserts a new IfumVisit record into the database.
*/
void insertVisit(VisitEntry visit);

/**
* Retrieves the total count of IfumVisit records.
*
* @return the count of IfumVisit records
*/
Integer getVisitCount(String project);

/**
* Refreshes the visit view which groups visit counts by project.
*/
void refreshVisitCache();
}
26 changes: 0 additions & 26 deletions src/main/java/io/paradaux/api/services/impl/IfumServiceImpl.java

This file was deleted.

60 changes: 60 additions & 0 deletions src/main/java/io/paradaux/api/services/impl/VisitsServiceImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.paradaux.api.services.impl;

import io.paradaux.api.mappers.VisitsMapper;
import io.paradaux.api.models.VisitEntry;
import io.paradaux.api.services.VisitsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Optional;
import java.util.TreeSet;

@Slf4j
@Service
public class VisitsServiceImpl implements VisitsService {

private static final TreeSet<String> IGNORED_USER_AGENTS = new TreeSet<>(Arrays.asList(
"bot", "crawler", "spider", "scanner", "curl", "wget",
"python", "java", "php", "unknown", "discord", "(compatible)",
"zgrab", "scrapy", "censys", "okhttp", "axios", "go-http-client",
"google", "bing", "yahoo"
));

private final VisitsMapper visitsMapper;

public VisitsServiceImpl(VisitsMapper visitsMapper) {
this.visitsMapper = visitsMapper;
}

@CacheEvict(value = "visitCounts", key = "#visit.project")
@Override
public void insertVisit(VisitEntry visit) {
String userAgent = Optional.ofNullable(visit.getUserAgent())
.map(String::toLowerCase)
.orElse("unknown");

// If the user agent matches any of the ignored partial agents, skip the insert
if (IGNORED_USER_AGENTS.stream().anyMatch(userAgent::contains)) {
log.warn("Blocked bot visit for project: {} with user agent: {}", visit.getProject(), userAgent);
return;
}

log.info("Attempting to inserting seemingly valid visit for: {}", visit.getProject());
visitsMapper.insertVisit(visit);
}

@Cacheable(value = "visitCounts", key = "#project")
@Override
public Integer getVisitCount(String project) {
return visitsMapper.getVisitCount(project);
}

@CacheEvict(value = "visitCounts", allEntries = true)
@Override
public void refreshVisitCache() {
visitsMapper.refreshVisitCache();
}
}
6 changes: 5 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
spring:
application:
name: paradaux-api
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m

cloudflare:
turnstile:
Expand All @@ -11,4 +15,4 @@ discord:

mybatis:
type-aliases-package: io.paradaux.api.models
mapper-locations: classpath:mappers/*.xml
mapper-locations: classpath:mappers/*.xml
16 changes: 16 additions & 0 deletions src/main/resources/db/2.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE SCHEMA IF NOT EXISTS analytics;
ALTER TABLE ifum.visits SET SCHEMA analytics;
ALTER TABLE analytics.visits ADD COLUMN project TEXT NOT NULL DEFAULT 'ifuckedur.mom';
ALTER TABLE analytics.visits ALTER COLUMN project DROP DEFAULT;
ALTER TABLE analytics.visits DROP CONSTRAINT IF EXISTS visits_ip_address_key;
ALTER TABLE analytics.visits ADD CONSTRAINT visits_ip_project_unique UNIQUE (ip_address, project);
CREATE MATERIALIZED VIEW analytics.visit_count_view AS
SELECT
project,
COUNT(*) AS total_count
FROM
analytics.visits
GROUP BY
project;
CREATE UNIQUE INDEX visit_count_view_project_idx ON analytics.visit_count_view(project);
DROP SCHEMA IF EXISTS ifum CASCADE;
25 changes: 0 additions & 25 deletions src/main/resources/mappers/IfumMapper.xml

This file was deleted.

24 changes: 24 additions & 0 deletions src/main/resources/mappers/VisitsMapper.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="io.paradaux.api.mappers.VisitsMapper">
<cache />

<insert id="insertVisit" parameterType="VisitEntry" flushCache="true">
INSERT INTO analytics.visits (ip_address, user_agent, project)
SELECT #{ipAddress}, #{userAgent}, #{project}
WHERE NOT EXISTS (
SELECT 1 FROM analytics.visits WHERE ip_address = #{ipAddress} AND project = #{project}
)
</insert>

<select id="getVisitCount" parameterType="String" resultType="Integer">
SELECT total_count FROM analytics.visit_count_view WHERE project = #{project}
</select>

<update id="refreshVisitCache" flushCache="true">
REFRESH MATERIALIZED VIEW CONCURRENTLY analytics.visit_count_view;
</update>
</mapper>
Loading