Skip to content

Instantly share code, notes, and snippets.

@NotArchon
Created May 4, 2023 20:23
Show Gist options
  • Save NotArchon/836e1a14b72ab6d78995024673252b16 to your computer and use it in GitHub Desktop.
Save NotArchon/836e1a14b72ab6d78995024673252b16 to your computer and use it in GitHub Desktop.
package com.neox.web.model.voting;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Projections;
import com.mongodb.client.model.Sorts;
import com.neox.web.Neox;
import com.neox.web.model.misc.ISAACCipher;
import com.neox.web.model.WrappedPage;
import com.neox.web.model.user.NeoxUser;
import com.neox.web.model.Session;
import com.neox.web.pojo.HiscoresDoc;
import com.neox.web.pojo.UserDoc;
import com.neox.web.pojo.VoteDoc;
import io.archon.misc.GsonInstance;
import io.archon.webserver.network.HttpAsyncRequest;
import io.archon.webserver.other.PageMapper;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.bson.types.ObjectId;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
@PageMapper({"/vote", "/votes", "/voting"})
public class VotePage extends WrappedPage {
private static final int[] keys = new int[]{ 293928283, -284284825, 958767281, 675849320 };
@Expose private long totalVotes; // this month
@Expose private String sitesJson;
public VotePage() {
super("ftl/vote.ftl");
}
private static final class SiteInfo {
String key;
String name;
String url;
long userVotes;
long userResetMs;
Collection<TopVoter> topVoters;
}
@RequiredArgsConstructor
private static final class TopVoter {
final long count;
String name;
}
@Override
public void load(HttpAsyncRequest req, Session session, NeoxUser user) {
if(user == null)
return;
String voterId = getVoterId(user.getId(), req.getIP());
if(voterId == null) // should never happen
return;
MongoCollection<VoteDoc> votes = Neox.getNode().getMongoDatabase()
.getCollection("votes", VoteDoc.class);
MongoCollection<HiscoresDoc> hiscores = Neox.getNode().getMongoDatabase()
.getCollection("hiscores", HiscoresDoc.class);
MongoCollection<UserDoc> users = Neox.getNode().getMongoDatabase()
.getCollection("users", UserDoc.class);
Map<String, Long> userCounts = new HashMap<>();
hiscores.find(Filters.and(
Filters.eq("user_id", user.getId()),
Filters.regex("key", "votes-.*-monthly$")
)).forEach(d -> {
String key = d.key();
key = key.substring(6, key.length() - 8); // removes "votes-" and "-monthly"
userCounts.put(key, d.primary());
});
totalVotes = userCounts.getOrDefault("all", 0L);
// todo speed this loading up its slow atm
List<SiteInfo> sites = new ArrayList<>();
for(VotingSite site : VotingSite.values()) {
if(!site.isActive())
continue;
VoteDoc lastUserVote = votes
.find(Filters.and(
Filters.eq("user_id", user.getId()),
Filters.eq("site_key", site.getSiteKey())
))
.sort(Sorts.descending("queued_at"))
.projection(Projections.include("queued_at"))
.limit(1).first();
Map<ObjectId, TopVoter> top = new LinkedHashMap<>();
hiscores
.find(Filters.eq("key", "votes-" + site.getSiteKey() + "-monthly"))
.sort(Sorts.orderBy(
Sorts.descending("primary", "secondary"),
Sorts.ascending("last_update")
))
.projection(Projections.fields(
Projections.include("user_id", "primary"),
Projections.excludeId()
))
.limit(5).forEach(d -> top.put(new ObjectId(d.userId()), new TopVoter(d.primary())));
users
.find(Filters.in("_id", top.keySet()))
.projection(Projections.include("display_name"))
.forEach(d -> top.get(d._id()).name = d.displayName());
SiteInfo info = new SiteInfo();
info.key = site.getSiteKey();
info.name = site.getName();
info.url = site.getVoteLink().replace("%voter-id%", voterId);
System.err.println("@@@ " + site.getSiteKey());
info.userVotes = userCounts.getOrDefault(site.getSiteKey(), 0L);
if(lastUserVote != null) {
long ms = System.currentTimeMillis();
long resetAtMs = lastUserVote.queuedAt() + TimeUnit.HOURS.toMillis(site.getResetHours());
info.userResetMs = Math.max(0, resetAtMs - ms);
}
info.topVoters = top.values();
sites.add(info);
}
sitesJson = GsonInstance.normal().toJson(sites);
System.err.println(new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(sites));
}
private static String getVoterId(String userId, String ip) {
long ms = System.currentTimeMillis();
byte[] userIdBytes = userId.getBytes(StandardCharsets.UTF_8);
byte[] ipBytes = ip.getBytes(StandardCharsets.UTF_8);
byte[] bytes;
ByteArrayOutputStream out;
try(DataOutputStream d = new DataOutputStream(out = new ByteArrayOutputStream(8 + userIdBytes.length + ipBytes.length))) {
d.writeLong(ms); // 8 bytes
d.write(userIdBytes); // should always be a length of 24
d.write(ipBytes);
bytes = out.toByteArray();
} catch(Exception e) {
Neox.getNode().logError(e);
return null;
}
byte[] encrypted = ISAACCipher.encrypt(bytes, keys);
return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(encrypted);
}
public static Voter decode(String voterId) {
byte[] decoded = Base64.getUrlDecoder().decode(voterId);
byte[] decrypted = ISAACCipher.decrypt(decoded, keys);
long ms;
String userId;
String ip;
try(DataInputStream in = new DataInputStream(new ByteArrayInputStream(decrypted))) {
ms = in.readLong();
byte[] userIdBytes = new byte[24];
in.read(userIdBytes);
userId = new String(userIdBytes, StandardCharsets.UTF_8);
byte[] ipBytes = new byte[in.available()];
in.read(ipBytes);
ip = new String(ipBytes, StandardCharsets.UTF_8);
} catch(Exception e) {
Neox.getNode().logError(e);
return null;
}
return new Voter(ms, userId, ip);
}
@RequiredArgsConstructor
private static final class Voter {
@Getter final long ms;
@Getter final String userId;
@Getter final String ip;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment