Created
May 4, 2023 20:23
-
-
Save NotArchon/836e1a14b72ab6d78995024673252b16 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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