From 17ce2ddd235e14a9c52da63af3a8deae2e4b70e3 Mon Sep 17 00:00:00 2001 From: s0m31 Date: Sat, 4 Jun 2022 15:59:54 +0300 Subject: [PATCH] Finish project --- pom.xml | 58 +++-- .../vezdekodbackend/LimitController.java | 26 +++ .../vezdekodbackend/MainController.java | 217 ++++++++++++++++++ .../VezdekodBackendApplication.java | 30 ++- src/main/resources/application.properties | 2 +- src/main/resources/log4j2.xml | 17 ++ .../VezdekodBackendApplicationTests.java | 13 -- 7 files changed, 333 insertions(+), 30 deletions(-) create mode 100644 src/main/java/org/nwolfhub/vezdekodbackend/LimitController.java create mode 100644 src/main/java/org/nwolfhub/vezdekodbackend/MainController.java create mode 100644 src/main/resources/log4j2.xml delete mode 100644 src/test/java/org/nwolfhub/vezdekodbackend/VezdekodBackendApplicationTests.java diff --git a/pom.xml b/pom.xml index f905028..dd75cb4 100644 --- a/pom.xml +++ b/pom.xml @@ -10,31 +10,21 @@ org.nwolfhub vezdekod-backend - 0.0.1-SNAPSHOT + 1.0.0 vezdekod-backend - Demo project for Spring Boot + 11 - - org.springframework.boot - spring-boot-starter-data-jpa - org.springframework.boot spring-boot-starter-web - - org.postgresql - postgresql - runtime - - - org.springframework.boot - spring-boot-starter-test - test + org.apache.logging.log4j + log4j-core + 2.17.2 @@ -44,6 +34,44 @@ org.springframework.boot spring-boot-maven-plugin + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + spring-boot + + org.nwolfhub.vezdekodbackend.VezdekodBackendApplication + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + + + + diff --git a/src/main/java/org/nwolfhub/vezdekodbackend/LimitController.java b/src/main/java/org/nwolfhub/vezdekodbackend/LimitController.java new file mode 100644 index 0000000..b950c39 --- /dev/null +++ b/src/main/java/org/nwolfhub/vezdekodbackend/LimitController.java @@ -0,0 +1,26 @@ +package org.nwolfhub.vezdekodbackend; + +import java.util.HashMap; + +//p.s. I prefer using another version of LimitController written on Kotlin, yet VK wants damn per-minute limit instead of hard calculations :( +public class LimitController { + public Integer maxRequests; + public HashMap requests; + public Long nextCleanup; + + public LimitController(Integer maxRequests) { + this.maxRequests = maxRequests; + this.requests = new HashMap<>(); + this.nextCleanup = System.currentTimeMillis() + 60000; + } + + public boolean addRequest(String ip) { + if(nextCleanup<=System.currentTimeMillis()) { + requests = new HashMap<>(); + nextCleanup = System.currentTimeMillis() + 60000; + } + if (!requests.containsKey(ip)) requests.put(ip, 1); + requests.replace(ip, requests.get(ip) + 1); + return maxRequests>=requests.get(ip); + } +} diff --git a/src/main/java/org/nwolfhub/vezdekodbackend/MainController.java b/src/main/java/org/nwolfhub/vezdekodbackend/MainController.java new file mode 100644 index 0000000..b03badb --- /dev/null +++ b/src/main/java/org/nwolfhub/vezdekodbackend/MainController.java @@ -0,0 +1,217 @@ +package org.nwolfhub.vezdekodbackend; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.io.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +@RestController +public class MainController { + public static List actors; + public static List votes; //VK asked not to use any database so here's your shitcode :3 + public static HashMap voteAmount; + + private static Logger logger; + private static LimitController voteController; + private static LimitController getController; + + /** + * Initializes MainController. Methods should not be called before initialization + * @param maxGets - maximum result obtaining requests from IP in 1 minute + * @param maxPosts - maximum vote requests from IP in 1 minute + */ + public static void initialize(Integer maxGets, Integer maxPosts) throws IOException { + logger = LogManager.getLogger(); + File voteFile = new File("votes.inf"); + if(!voteFile.exists()) votes = new ArrayList<>(); + else { + try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(voteFile))) { + votes = (ArrayList) in.readObject(); + logger.info("Imported" + votes.size() + " votes"); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Bad formed votes file:" + e); + } + } + voteAmount = new HashMap<>(); + for(Vote vote:votes) { + if(voteAmount.containsKey(vote.getActor())) voteAmount.replace(vote.getActor(), voteAmount.get(vote.getActor()) + 1); + else voteAmount.put(vote.getActor(), 1); + } + File cfg = new File("actors.cfg"); + if(!cfg.exists()) { + cfg.createNewFile(); + logger.error("File " + cfg.getAbsolutePath() + " was created. Fill in actors data"); + System.exit(2); + } + try (FileInputStream in = new FileInputStream(cfg)) { + String raw = new String(in.readAllBytes()).replace("\n", ""); + actors = Arrays.asList(raw.split(";")); + } + voteController = new LimitController(maxPosts); + getController = new LimitController(maxGets); + logger.info("Finished initialization"); + } + + /** + * Vote - vote for any actor + * !!!RATE LIMITED!!! + * @param phone - phone number of a user who votes for this actor + * @param actor - actor himself. Should be configured in config first + * @param ip - ip of a user. Obtained using X-Forwarded-For, make sure that web server is configured to set it + * @return Http code of a result + */ + @GetMapping("/vote") + public static ResponseEntity vote (@RequestParam(value = "phone", defaultValue = "") String phone, @RequestParam(value = "artist", defaultValue = "") String actor, @RequestHeader(value = "X-Forwarded-For", defaultValue = "none") String ip) { + HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest(); + if(ip.equals("none")) { + logger.warn("Web server is not configured to set ip using a header. You will see this warning every request without X-Forwarded-For header. For nginx, add \"proxy_set_header X-Forwarded-For $remote_addr;\" to your config inside proxy pass body"); + ip = request.getRemoteAddr(); + } + if(voteController.addRequest(ip)) { + try { + Vote vote = new Vote(phone, actor); + votes.add(vote); + if(voteAmount.containsKey(vote.getActor())) voteAmount.replace(vote.getActor(), voteAmount.get(vote.getActor()) + 1); + else voteAmount.put(vote.getActor(), 1); + return ResponseEntity.status(HttpStatus.CREATED).body("Voted"); + } catch (InvalidArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } else return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Too many requests"); + } + + public static void unloadVotes() { + File f = new File("votes.inf"); + if(!f.exists()) { + try { + f.createNewFile(); + }catch (IOException e) { + logger.error("Failed to unload votes. Cannot create votes.inf (" + e + ")"); + } + } + try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f))) { //not using buffered cuz uploading just to 1 file btw + out.writeObject(votes); + out.flush(); + out.close(); + } catch (IOException e) { + logger.error("Failed to unload votes (" + e + ")"); + } + } + + /** + * Get all votes with params + * !!!RATE LIMITED!!! + * @param fromString - start date + * @param toString - end date + * @param intervalsString - intervals. Will take me 1+ day to make my brains working so have to leave it unreleased + * @param artist - required artist + * @param ip - ip of a user. Obtained using X-Forwarded-For, make sure that web server is configured to set it + * @return As documented (excluding intervals) + */ + @GetMapping("/getVotes") + public static ResponseEntity getVotes (@RequestParam(value = "from", defaultValue = "0") String fromString, @RequestParam(value = "to", defaultValue = "ns") String toString, + @RequestParam(value = "intervals", defaultValue = "10") String intervalsString, @RequestParam(value = "artists", defaultValue = "") String artist, + @RequestHeader(value = "X-Forwarded-For", defaultValue = "none") String ip) { + Long from; + Long to; + Integer intervals = 10; + List artists; + HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest(); + if(ip.equals("none")) { + logger.warn("Web server is not configured to set ip using a header. You will see this warning every request without X-Forwarded-For header. For nginx, add \"proxy_set_header X-Forwarded-For $remote_addr;\" to your config inside proxy pass body"); + ip = request.getRemoteAddr(); + } + if(getController.addRequest(ip)) { + try { + from = Long.valueOf(fromString); + to = Long.valueOf(toString.replace("ns", String.valueOf(System.currentTimeMillis() / 1000))); + intervals = Integer.valueOf(intervalsString); + artists = artist.equals("") ? actors : Arrays.asList(artist.split(",")); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("{\"text\": \"Server returned error" + e + "\"}"); + } + Integer voteAmount = 0; + for (Vote vote : votes) { + if (artists.contains(vote.getActor())) { + if (vote.unix > from && vote.unix < to) voteAmount++; + } + } + return ResponseEntity.status(HttpStatus.OK).body("{\"data\": [{\"start\": " + from + ", \"to\": " + to + ", \"votes\": " + voteAmount + "}]}"); + } else { + return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("{\"text\": \"too many requests\"}"); + } + } + + public static class Vote implements Serializable { + public String phone; + public String actor; + public Long unix; + + public String getPhone() { + return phone; + } + + public Vote setPhone(String phone) { + this.phone = phone; + return this; + } + + public String getActor() { + return actor; + } + + public Vote setActor(String actor) { + this.actor = actor; + return this; + } + + public Long getUnix() { + return unix; + } + + public Vote setUnix(Long unix) { + this.unix = unix; + return this; + } + + public Vote() { + this.unix = System.currentTimeMillis()/1000; + } + + public Vote(String phone, String actor) { + if(!actors.contains(actor)) { + throw new InvalidArgumentException("Actor " + actor + " does not exist"); + } + this.actor = actor; + if(phone.length()==10 && phone.split("")[0].equals("9")) { + this.phone = phone; + } else throw new InvalidArgumentException(phone + " is not a legal phone number"); + this.unix = System.currentTimeMillis()/1000; + } + } + + public static class InvalidArgumentException extends RuntimeException { + public InvalidArgumentException(String text) { + super(text); + } + + public InvalidArgumentException() { + super(); + } + } +} diff --git a/src/main/java/org/nwolfhub/vezdekodbackend/VezdekodBackendApplication.java b/src/main/java/org/nwolfhub/vezdekodbackend/VezdekodBackendApplication.java index 692167a..da1638d 100644 --- a/src/main/java/org/nwolfhub/vezdekodbackend/VezdekodBackendApplication.java +++ b/src/main/java/org/nwolfhub/vezdekodbackend/VezdekodBackendApplication.java @@ -1,12 +1,40 @@ package org.nwolfhub.vezdekodbackend; +import com.sun.tools.javac.Main; +import org.apache.logging.log4j.LogManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + @SpringBootApplication public class VezdekodBackendApplication { - public static void main(String[] args) { + public static void main(String[] args) throws IOException { + String next = ""; + int get = 20; + int post = 3; + for(String arg:args) { + if(next.equals("get")) get = Integer.parseInt(arg); + else if(next.equals("post")) post = Integer.parseInt(arg); + else switch (arg) { + case "--get": + case "-g": + next = "get"; + break; + case "--post": + case "-p": + next = "post"; + break; + } + } + MainController.initialize(get, post); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + MainController.unloadVotes(); + LogManager.getLogger().info("Uploaded votes. Goodbye :)"); + })); SpringApplication.run(VezdekodBackendApplication.class, args); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..a3ac65c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1 @@ - +server.port=8080 \ No newline at end of file diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..e3721cd --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/test/java/org/nwolfhub/vezdekodbackend/VezdekodBackendApplicationTests.java b/src/test/java/org/nwolfhub/vezdekodbackend/VezdekodBackendApplicationTests.java deleted file mode 100644 index 8a0ee2e..0000000 --- a/src/test/java/org/nwolfhub/vezdekodbackend/VezdekodBackendApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.nwolfhub.vezdekodbackend; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class VezdekodBackendApplicationTests { - - @Test - void contextLoads() { - } - -}