implements GameChatResponseQueue {
18 |
19 | RemoteResponseQueue(ObjectMapper mapper, Channel channel, String exchange, String queue) {
20 | super(mapper, channel, exchange, queue, log, EventReponsePair.class, null);
21 | }
22 |
23 | @Override
24 | public void onResponse(GameChatResponse response, GameChatEvent event) {
25 | event.getMeta().setMdc(MdcUtils.getSnapshot());
26 | send(new EventReponsePair(event, response));
27 | }
28 |
29 | @Override
30 | public int size() {
31 | return super.size();
32 | }
33 |
34 | @Data
35 | @NoArgsConstructor
36 | @AllArgsConstructor
37 | public static class EventReponsePair {
38 | GameChatEvent event;
39 | GameChatResponse response;
40 | }
41 | }
--------------------------------------------------------------------------------
/tillerinobot-rabbit/src/test/java/org/tillerino/ppaddict/rabbit/RabbitMqContainerConnection.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.rabbit;
2 |
3 | import java.util.concurrent.ExecutorService;
4 |
5 | import org.junit.rules.ExternalResource;
6 |
7 | import com.rabbitmq.client.Connection;
8 | import com.rabbitmq.client.ConnectionFactory;
9 |
10 | import lombok.Getter;
11 | import lombok.RequiredArgsConstructor;
12 |
13 | @RequiredArgsConstructor
14 | public class RabbitMqContainerConnection extends ExternalResource {
15 | @Getter
16 | private Connection connection;
17 |
18 | private final ExecutorService sharedExecutorService;
19 |
20 | @Override
21 | protected void before() throws Throwable {
22 | ConnectionFactory connectionFactory = RabbitMqConfiguration.connectionFactory(
23 | RabbitMqContainer.getHost(), RabbitMqContainer.getAmqpPort(), RabbitMqContainer.getVirtualHost());
24 | if (sharedExecutorService != null) {
25 | connectionFactory.setSharedExecutor(sharedExecutorService);
26 | }
27 |
28 | connection = connectionFactory.newConnection("test");
29 | }
30 |
31 | @Override
32 | protected void after() {
33 | if (connection != null) {
34 | try {
35 | connection.close();
36 | } catch (Exception e) {
37 | // we don't care
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/tillerinobot-rabbit/src/test/java/org/tillerino/ppaddict/rabbit/RemoteEventQueueTest.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.rabbit;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import org.junit.Test;
6 | import org.tillerino.ppaddict.chat.GameChatEvent;
7 | import org.tillerino.ppaddict.chat.Joined;
8 | import org.tillerino.ppaddict.chat.PrivateAction;
9 | import org.tillerino.ppaddict.chat.PrivateMessage;
10 | import org.tillerino.ppaddict.chat.Sighted;
11 | import org.tillerino.ppaddict.util.MdcUtils;
12 | import org.tillerino.ppaddict.util.MdcUtils.MdcAttributes;
13 |
14 | import com.fasterxml.jackson.core.JsonProcessingException;
15 | import com.fasterxml.jackson.databind.JsonMappingException;
16 | import com.fasterxml.jackson.databind.ObjectMapper;
17 |
18 | public class RemoteEventQueueTest {
19 | private static final ObjectMapper OBJECT_MAPPER = RabbitMqConfiguration.mapper();
20 |
21 | @Test
22 | public void testSerializations() throws Exception {
23 | roundTrip(new PrivateMessage(123, "n", 456, "m"));
24 | roundTrip(new PrivateAction(123, "n", 456, "a"));
25 | roundTrip(new Sighted(123, "n", 456));
26 | roundTrip(new Joined(123, "n", 456));
27 | }
28 |
29 | private void roundTrip(GameChatEvent message) throws JsonProcessingException, JsonMappingException {
30 | try (MdcAttributes with = MdcUtils.with("mdck", "mdcv")) {
31 | message.getMeta().setMdc(MdcUtils.getSnapshot());
32 | message.getMeta().setRateLimiterBlockedTime(234);
33 | String serialized = OBJECT_MAPPER.writerFor(GameChatEvent.class).writeValueAsString(message);
34 | GameChatEvent deserialized = OBJECT_MAPPER.readValue(serialized, GameChatEvent.class);
35 | assertThat(deserialized).isEqualTo(message);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tillerinobot-rabbit/src/test/java/org/tillerino/ppaddict/rabbit/RemoteResponseQueueTest.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.rabbit;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import org.junit.Test;
6 | import org.tillerino.ppaddict.chat.GameChatResponse;
7 |
8 | import com.fasterxml.jackson.core.JsonProcessingException;
9 | import com.fasterxml.jackson.databind.JsonMappingException;
10 | import com.fasterxml.jackson.databind.ObjectMapper;
11 |
12 | public class RemoteResponseQueueTest {
13 | private static final ObjectMapper OBJECT_MAPPER = RabbitMqConfiguration.mapper();
14 |
15 | @Test
16 | public void testSerializations() throws Exception {
17 | roundTrip(new GameChatResponse.Success("abc"));
18 | roundTrip(new GameChatResponse.Message("abc"));
19 | roundTrip(new GameChatResponse.Action("abc"));
20 | roundTrip(new GameChatResponse.Action("abc").then(new GameChatResponse.Success("def")));
21 | roundTrip(GameChatResponse.none());
22 | }
23 |
24 | private void roundTrip(GameChatResponse message) throws JsonProcessingException, JsonMappingException {
25 | String serialized = OBJECT_MAPPER.writerFor(GameChatResponse.class).writeValueAsString(message);
26 | GameChatResponse deserialized = OBJECT_MAPPER.readValue(serialized, GameChatResponse.class);
27 | assertThat(deserialized).isEqualTo(message);
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/tillerinobot-rabbit/src/test/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tillerinobot-tests/src/test/java/org/tillerino/ppaddict/MessageQueueTest.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict;
2 |
3 | import static org.awaitility.Awaitility.await;
4 | import static org.hamcrest.CoreMatchers.equalTo;
5 |
6 | import java.util.concurrent.atomic.AtomicInteger;
7 |
8 | import org.junit.ClassRule;
9 | import org.junit.Test;
10 | import org.slf4j.MDC;
11 | import org.tillerino.ppaddict.chat.Sighted;
12 | import org.tillerino.ppaddict.rabbit.RabbitMqConfiguration;
13 | import org.tillerino.ppaddict.rabbit.RabbitMqContainerConnection;
14 | import org.tillerino.ppaddict.rabbit.RemoteEventQueue;
15 |
16 | public class MessageQueueTest {
17 | @ClassRule
18 | public static final RabbitMqContainerConnection rabbit = new RabbitMqContainerConnection(null);
19 |
20 | /**
21 | * The largest burst of messages is when the bot gets an overview of all online players.
22 | * We need to make sure that this works reasonably fast.
23 | */
24 | @Test
25 | public void speed() throws Exception {
26 | RemoteEventQueue eventQueue = RabbitMqConfiguration.internalEventQueue(rabbit.getConnection());
27 | eventQueue.setup();
28 |
29 | AtomicInteger received = new AtomicInteger();
30 | eventQueue.subscribe(x -> received.incrementAndGet());
31 |
32 | for (long i = 0, event = System.currentTimeMillis(); i < 15000; i++) {
33 | MDC.put("eventId", "" + event++);
34 | MDC.put("pircbotx.id", "1");
35 | MDC.put("pircbotx.server", "irc.ppy.sh");
36 | MDC.put("pircbotx.port", "3306");
37 | eventQueue.onEvent(new Sighted(event, "nickname", System.currentTimeMillis()));
38 | }
39 |
40 | await().untilAtomic(received, equalTo(15000));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tillerinobot-tests/src/test/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/mormon/Column.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.mormon;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | @Retention(RetentionPolicy.RUNTIME)
9 | @Target(ElementType.FIELD)
10 | public @interface Column {
11 | String value();
12 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/mormon/KeyColumn.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.mormon;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | /**
9 | * Declares the key columns of a persisted class.
10 | * Since the order of fields is not constant at runtime, it is of no use to us
11 | * to annotate fields with a key-annotation in the case of compound keys.
12 | * Instead, we use this annotation at the class level.
13 | *
14 | *
15 | * Whenever we load data from the database without specifying a query,
16 | * a query is automatically constructed from this annotation.
17 | * The placeholders in that query are then filled from the given object array.
18 | * The order of the values in that array must match the order of the columns in this annotation.
19 | */
20 | @Retention(RetentionPolicy.RUNTIME)
21 | @Target(ElementType.TYPE)
22 | public @interface KeyColumn {
23 | String[] value();
24 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/mormon/Persister.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.mormon;
2 |
3 | import java.sql.PreparedStatement;
4 | import java.sql.SQLException;
5 |
6 | import javax.annotation.Nonnull;
7 |
8 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
9 | import lombok.NonNull;
10 |
11 | /**
12 | * Wraps a {@link PreparedStatement} to persist Java objects to the database.
13 | * Depending on the chosen {@link Action}, this can be used to both insert and update rows.
14 | *
15 | *
16 | * This class will close the underlying {@link PreparedStatement} when closed.
17 | * It implements {@link AutoCloseable}, so it is best used in a try-with block.
18 | */
19 | public class Persister implements AutoCloseable {
20 | public enum Action {
21 | INSERT("INSERT INTO"),
22 | INSERT_IGNORE("INSERT IGNORE INTO"),
23 | INSERT_DELAYED("INSERT DELAYED INTO"),
24 | REPLACE("REPLACE INTO"),
25 | ;
26 | private String command;
27 |
28 | Action(String command) {
29 | this.command = command;
30 | }
31 | }
32 |
33 | private final PreparedStatement statement;
34 |
35 | private final Mapping mapping;
36 |
37 | private int batched = 0;
38 |
39 | @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
40 | Persister(Database database, Class cls, Action a) throws SQLException {
41 | mapping = Mapping.getOrCreateMapping(cls);
42 |
43 | statement = database.prepare(a.command + " `" + mapping.table() + "` (" + mapping.fields() + ") values (" + mapping.questionMarks() + ")");
44 | }
45 |
46 | public int persist(@Nonnull @NonNull T obj) throws SQLException {
47 | return persist(obj, 0);
48 | }
49 |
50 | public int persist(@Nonnull @NonNull T obj, int batchUpTo) throws SQLException {
51 | if(batched > 1 && batched >= batchUpTo) {
52 | statement.executeBatch();
53 | batched = 0;
54 | }
55 | mapping.set(obj, statement);
56 | if(batchUpTo <= 1) {
57 | return statement.executeUpdate();
58 | }
59 | statement.addBatch();
60 | batched++;
61 | if(batched >= batchUpTo) {
62 | statement.executeBatch();
63 | batched = 0;
64 | }
65 | return 0;
66 | }
67 |
68 | @Override
69 | public void close() throws SQLException {
70 | if(batched > 0) {
71 | statement.executeBatch();
72 | batched = 0;
73 | }
74 | statement.close();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/mormon/ResultSetIterator.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.mormon;
2 |
3 | import java.sql.ResultSet;
4 | import java.sql.SQLException;
5 | import java.util.Iterator;
6 |
7 | class ResultSetIterator implements Iterator {
8 | private final ResultSet set;
9 | private final Mapping extends T> mapping;
10 | private boolean hasNext = false;
11 | private boolean consumed = true;
12 |
13 | ResultSetIterator(ResultSet set, Mapping extends T> mapping) {
14 | this.set = set;
15 | this.mapping = mapping;
16 | }
17 |
18 | @Override
19 | public boolean hasNext() {
20 | if (consumed) {
21 | try {
22 | hasNext = set.next();
23 | consumed = false;
24 | } catch (SQLException e) {
25 | throw new RuntimeException(e);
26 | }
27 | }
28 | return hasNext;
29 | }
30 |
31 | @Override
32 | public T next() {
33 | consumed = true;
34 | try {
35 | T instance = mapping.constructor().newInstance();
36 |
37 | mapping.get(instance, set);
38 |
39 | return instance;
40 | } catch (ReflectiveOperationException | SQLException e) {
41 | throw new RuntimeException(e);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/mormon/Table.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.mormon;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | @Retention(RetentionPolicy.RUNTIME)
9 | @Target(ElementType.TYPE)
10 | public @interface Table {
11 | String value();
12 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/mormon/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * mORMon = minimal ORM Mysql-ONly.
3 | *
4 | *
5 | * A tiny ORM that only takes care of the basics:
6 | * mapping between Java objects and {@link ResultSet}/{@link PreparedStatement}
7 | * with some convenience stuff like streaming and batching sprinkled on top.
8 | *
9 | *
10 | * The "where" part of queries is written in plain SQL.
11 | * Everything is built for MySQL (e.g. how to escape column names, streaming, and batching).
12 | *
13 | *
14 | * Start by creating a {@link Database} instance.
15 | * For convenience, {@link DatabaseManager} implements a pool for {@link Database} instances.
16 | */
17 | package org.tillerino.mormon;
18 |
19 | import java.sql.PreparedStatement;
20 | import java.sql.ResultSet;
21 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/ppaddict/chat/impl/ProcessorsModule.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.chat.impl;
2 |
3 | import org.tillerino.ppaddict.chat.GameChatEventConsumer;
4 | import org.tillerino.ppaddict.chat.GameChatResponseConsumer;
5 |
6 | import com.google.inject.AbstractModule;
7 | import com.google.inject.name.Names;
8 |
9 | /**
10 | * Binds {@link MessagePreprocessor} and {@link ResponsePostprocessor}.
11 | */
12 | public class ProcessorsModule extends AbstractModule {
13 | @Override
14 | protected void configure() {
15 | bind(GameChatEventConsumer.class).annotatedWith(Names.named("messagePreprocessor")).to(MessagePreprocessor.class);
16 | bind(GameChatResponseConsumer.class).annotatedWith(Names.named("responsePostprocessor"))
17 | .to(ResponsePostprocessor.class);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/ppaddict/chat/local/LocalGameChatMetrics.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.chat.local;
2 |
3 | import javax.inject.Singleton;
4 |
5 | import org.mapstruct.MappingTarget;
6 | import org.mapstruct.factory.Mappers;
7 | import org.tillerino.ppaddict.chat.GameChatClientMetrics;
8 |
9 | import lombok.Data;
10 | import lombok.EqualsAndHashCode;
11 | import lombok.ToString;
12 |
13 | @Singleton
14 | @Data
15 | @ToString(callSuper = true)
16 | @EqualsAndHashCode(callSuper = true)
17 | public class LocalGameChatMetrics extends GameChatClientMetrics {
18 | private long lastSentMessage;
19 | private long lastRecommendation;
20 | private long responseQueueSize;
21 | private long eventQueueSize;
22 |
23 | @org.mapstruct.Mapper
24 | public interface Mapper {
25 | static final Mapper INSTANCE = Mappers.getMapper(Mapper.class);
26 |
27 | void loadFromBot(GameChatClientMetrics source, @MappingTarget GameChatClientMetrics target);
28 |
29 | LocalGameChatMetrics copy(LocalGameChatMetrics l);
30 | }
31 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/ppaddict/config/CachedDatabaseConfigServiceModule.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.config;
2 |
3 | import com.google.inject.AbstractModule;
4 | import com.google.inject.name.Names;
5 |
6 | public class CachedDatabaseConfigServiceModule extends AbstractModule {
7 | @Override
8 | protected void configure() {
9 | bind(ConfigService.class).to(CachingConfigService.class);
10 | bind(ConfigService.class).annotatedWith(Names.named("uncached")).to(DatabaseConfigService.class);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/ppaddict/config/CachingConfigService.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.config;
2 |
3 | import java.util.Optional;
4 | import java.util.concurrent.TimeUnit;
5 |
6 | import javax.inject.Inject;
7 | import javax.inject.Named;
8 | import javax.inject.Singleton;
9 |
10 | import com.google.common.cache.CacheBuilder;
11 | import com.google.common.cache.CacheLoader;
12 | import com.google.common.cache.LoadingCache;
13 | import com.google.common.util.concurrent.UncheckedExecutionException;
14 | import org.apache.commons.lang3.function.Failable;
15 |
16 | @Singleton
17 | public class CachingConfigService implements ConfigService {
18 | private final LoadingCache> cache;
19 |
20 | @Inject
21 | public CachingConfigService(@Named("uncached") ConfigService delegate) {
22 | this.cache = CacheBuilder.newBuilder()
23 | .expireAfterAccess(1, TimeUnit.SECONDS)
24 | .build(CacheLoader.from(delegate::config));
25 | }
26 |
27 | @Override
28 | public Optional config(String key) {
29 | try {
30 | return cache.getUnchecked(key);
31 | } catch (UncheckedExecutionException e) {
32 | throw Failable.rethrow(e.getCause());
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/ppaddict/config/DatabaseConfigService.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.config;
2 |
3 | import java.sql.SQLException;
4 | import java.util.Optional;
5 |
6 | import javax.inject.Inject;
7 | import javax.inject.Singleton;
8 |
9 | import org.apache.commons.lang3.exception.ContextedRuntimeException;
10 | import org.tillerino.mormon.Database;
11 | import org.tillerino.mormon.DatabaseManager;
12 |
13 | import lombok.RequiredArgsConstructor;
14 | import tillerino.tillerinobot.data.BotConfig;
15 |
16 | @Singleton
17 | @RequiredArgsConstructor(onConstructor = @__(@Inject))
18 | public class DatabaseConfigService implements ConfigService {
19 | private final DatabaseManager dbm;
20 |
21 | @Override
22 | public Optional config(String key) {
23 | try (Database db = dbm.getDatabase()) {
24 | return db.selectUnique(BotConfig.class)."where path = \{key}".map(BotConfig::getValue);
25 | } catch (SQLException e) {
26 | throw new ContextedRuntimeException("Unable to load config", e)
27 | .addContextValue("key", key);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/ppaddict/rest/AuthenticationService.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.rest;
2 |
3 | import jakarta.ws.rs.ForbiddenException;
4 | import jakarta.ws.rs.GET;
5 | import jakarta.ws.rs.HeaderParam;
6 | import jakarta.ws.rs.NotFoundException;
7 | import jakarta.ws.rs.POST;
8 | import jakarta.ws.rs.Path;
9 | import jakarta.ws.rs.Produces;
10 | import jakarta.ws.rs.QueryParam;
11 | import jakarta.ws.rs.core.MediaType;
12 |
13 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
14 |
15 | import lombok.AllArgsConstructor;
16 | import lombok.Data;
17 | import lombok.NoArgsConstructor;
18 |
19 | /**
20 | * Service for API key authentication.
21 | */
22 | public interface AuthenticationService {
23 | @Data
24 | @NoArgsConstructor
25 | @AllArgsConstructor
26 | @JsonIgnoreProperties(ignoreUnknown = true)
27 | static class Authorization {
28 | private boolean admin;
29 | }
30 |
31 | /**
32 | * Finds the authorization for a given API key.
33 | *
34 | * @throws NotFoundException if there is no such key.
35 | */
36 | @GET
37 | @Path("/authorization")
38 | @Produces(MediaType.APPLICATION_JSON)
39 | Authorization getAuthorization(@HeaderParam("api-key") String apiKey) throws NotFoundException;
40 |
41 | /**
42 | * Creates a new API key for an osu user.
43 | *
44 | * @param adminKey admin key of the application. This key must be authorized for key creation.
45 | * @param osuUserId id of the user to create an API key for. Any existing key is revoked.
46 | * @return the new API key
47 | */
48 | @POST
49 | @Path("/authentication")
50 | String createKey(@HeaderParam("api-key") String apiKey, @QueryParam("osu-user-id") int osuUserId) throws NotFoundException, ForbiddenException;
51 | }
52 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/ppaddict/rest/AuthenticationServiceImpl.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.rest;
2 |
3 | import javax.inject.Inject;
4 | import javax.inject.Named;
5 |
6 | import jakarta.ws.rs.ForbiddenException;
7 | import jakarta.ws.rs.InternalServerErrorException;
8 | import jakarta.ws.rs.NotFoundException;
9 | import jakarta.ws.rs.client.ResponseProcessingException;
10 |
11 | import org.glassfish.jersey.client.JerseyClientBuilder;
12 | import org.glassfish.jersey.client.proxy.WebResourceFactory;
13 |
14 | import com.google.inject.AbstractModule;
15 |
16 | import lombok.extern.slf4j.Slf4j;
17 |
18 | /**
19 | * Implements the {@link AuthenticationService} against an internal HTTP API.
20 | */
21 | @Slf4j
22 | public class AuthenticationServiceImpl implements AuthenticationService {
23 | private final AuthenticationService remoteService;
24 |
25 | @Inject
26 | public AuthenticationServiceImpl(@Named("ppaddict.auth.url") String authServiceBaseUrl) {
27 | remoteService = WebResourceFactory.newResource(AuthenticationService.class,
28 | JerseyClientBuilder.createClient().target(authServiceBaseUrl));
29 | }
30 |
31 | @Override
32 | public Authorization getAuthorization(String key) {
33 | try {
34 | return remoteService.getAuthorization(key);
35 | } catch (ResponseProcessingException e) {
36 | log.error("Error getting authorization", e);
37 | throw new InternalServerErrorException();
38 | }
39 | }
40 |
41 | @Override
42 | public String createKey(String adminKey, int osuUserId) throws NotFoundException, ForbiddenException {
43 | return remoteService.createKey(adminKey, osuUserId);
44 | }
45 |
46 | public static class RemoteAuthenticationModule extends AbstractModule {
47 | @Override
48 | protected void configure() {
49 | bind(AuthenticationService.class).to(AuthenticationServiceImpl.class);
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/ppaddict/util/LoopingRunnable.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.util;
2 |
3 | import org.slf4j.MDC;
4 |
5 | import lombok.extern.slf4j.Slf4j;
6 |
7 | /**
8 | * Implements a runnable by calling a loop body over and over. The loop body is
9 | * implemented in {@link #loop()}.
10 | */
11 | @Slf4j
12 | public abstract class LoopingRunnable implements Runnable {
13 | @Override
14 | public final void run() {
15 | for (;;) {
16 | try {
17 | MDC.clear();
18 | loop();
19 | } catch (InterruptedException e) {
20 | log.info("Interrupted. Stopping loop.");
21 | Thread.currentThread().interrupt();
22 | return;
23 | } catch (Throwable e) {
24 | log.error("Error", e);
25 | throw e;
26 | }
27 | }
28 | }
29 |
30 | /**
31 | * Is repeatedly called from {@link #run()} until an
32 | * {@link InterruptedException} is thrown. All other exceptions are not
33 | * caught. This method is always called with an empty {@link MDC}.
34 | *
35 | * @throws InterruptedException to end the loop gracefully.
36 | */
37 | protected abstract void loop() throws InterruptedException;
38 | }
39 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/ppaddict/web/data/HasLinkedOsuId.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.web.data;
2 |
3 | import javax.annotation.CheckForNull;
4 |
5 | import org.tillerino.osuApiModel.types.UserId;
6 |
7 | public interface HasLinkedOsuId {
8 | @CheckForNull
9 | @UserId Integer getLinkedOsuId();
10 |
11 | void setLinkedOsuId(@UserId Integer osuId);
12 | }
13 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/ppaddict/web/data/PpaddictLinkKey.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.web.data;
2 |
3 | import org.tillerino.mormon.KeyColumn;
4 | import org.tillerino.mormon.Table;
5 | import org.tillerino.ppaddict.web.types.PpaddictId;
6 |
7 | import lombok.AllArgsConstructor;
8 | import lombok.Data;
9 | import lombok.NoArgsConstructor;
10 |
11 | @Table("ppaddictlinkkeys")
12 | @KeyColumn("linkKey")
13 | @Data
14 | @NoArgsConstructor
15 | @AllArgsConstructor
16 | public class PpaddictLinkKey {
17 | private @PpaddictId String identifier;
18 |
19 | private String displayName;
20 |
21 | private String linkKey;
22 |
23 | private long expires;
24 | }
25 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/ppaddict/web/data/PpaddictUser.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.web.data;
2 |
3 | import javax.annotation.CheckForNull;
4 |
5 | import org.tillerino.mormon.KeyColumn;
6 | import org.tillerino.mormon.Table;
7 | import org.tillerino.ppaddict.web.types.PpaddictId;
8 |
9 | import lombok.AllArgsConstructor;
10 | import lombok.Data;
11 | import lombok.NoArgsConstructor;
12 |
13 | @Table("ppaddictusers")
14 | @KeyColumn("identifier")
15 | @Data
16 | @NoArgsConstructor
17 | @AllArgsConstructor
18 | public class PpaddictUser {
19 | @PpaddictId
20 | private String identifier;
21 |
22 | @CheckForNull
23 | private String data;
24 |
25 | @PpaddictId
26 | private String forward;
27 | }
28 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/org/tillerino/ppaddict/web/types/PpaddictId.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.web.types;
2 |
3 | import javax.annotation.meta.TypeQualifier;
4 |
5 | @TypeQualifier
6 | public @interface PpaddictId {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/RateLimitingOsuApiDownloader.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot;
2 |
3 | import java.io.IOException;
4 | import java.net.URL;
5 |
6 | import javax.inject.Inject;
7 | import javax.inject.Named;
8 |
9 | import jakarta.ws.rs.ServiceUnavailableException;
10 |
11 | import org.tillerino.osuApiModel.Downloader;
12 | import org.tillerino.ppaddict.util.MdcUtils;
13 |
14 | import com.fasterxml.jackson.databind.JsonNode;
15 |
16 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
17 |
18 | public class RateLimitingOsuApiDownloader extends Downloader {
19 | private final RateLimiter limiter;
20 |
21 | @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Injection")
22 | @Inject
23 | public RateLimitingOsuApiDownloader(@Named("osuapi.url") URL baseUrl,
24 | @Named("osuapi.key") String key, RateLimiter limiter) {
25 | super(baseUrl, key);
26 | this.limiter = limiter;
27 | }
28 |
29 | @Override
30 | public JsonNode get(String command, String... parameters) throws IOException {
31 | try {
32 | limiter.limitRate();
33 | MdcUtils.incrementCounter(MdcUtils.MDC_EXTERNAL_API_CALLS);
34 | } catch (InterruptedException e) {
35 | Thread.currentThread().interrupt();
36 | throw new ServiceUnavailableException();
37 | }
38 | return super.get(command, parameters);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/UserException.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot;
2 |
3 | import lombok.NonNull;
4 |
5 | /**
6 | * This type of exception will be displayed to the user.
7 | *
8 | * @author Tillerino
9 | */
10 | public class UserException extends Exception {
11 | private static final long serialVersionUID = 1L;
12 |
13 | private static final String ERROR_MESSAGE = "%s must be between %s and %s but was %s";
14 |
15 | public UserException(String message) {
16 | super(message);
17 | }
18 |
19 | public static void validateInclusiveBetween(long floor, long ceil, long actual, @NonNull String desc) throws UserException {
20 | if (actual < floor || actual > ceil) {
21 | throw new UserException(String.format(ERROR_MESSAGE, desc, floor, ceil, actual));
22 | }
23 | }
24 |
25 | public static void validateInclusiveBetween(double floor, double ceil, double actual, @NonNull String desc) throws UserException {
26 | if (actual < floor || actual > ceil) {
27 | throw new UserException(String.format(ERROR_MESSAGE, desc, floor, ceil, actual));
28 | }
29 | }
30 |
31 | /**
32 | * This type of exception is extremely rare in a sense that it won't occur
33 | * again if the causing action is repeated.
34 | *
35 | * @author Tillerino
36 | */
37 | public static class RareUserException extends UserException {
38 | private static final long serialVersionUID = 1L;
39 |
40 | public RareUserException(String message) {
41 | super(message);
42 | }
43 |
44 | }
45 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/data/BotConfig.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.data;
2 |
3 | import org.tillerino.mormon.KeyColumn;
4 | import org.tillerino.mormon.Table;
5 |
6 | import lombok.Data;
7 |
8 | /**
9 | * Configuration that can be changed while the bot runs.
10 | * This is stored as simple key-value in the database.
11 | */
12 | @Data
13 | @Table("botconfig")
14 | @KeyColumn("path")
15 | public class BotConfig {
16 | private String path;
17 |
18 | private String value;
19 | }
20 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/data/BotUserData.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.data;
2 |
3 | import javax.annotation.Nonnull;
4 |
5 | import org.tillerino.mormon.KeyColumn;
6 | import org.tillerino.mormon.Table;
7 |
8 | import lombok.Data;
9 |
10 | @Table("userdata")
11 | @KeyColumn("userId")
12 | @Data
13 | public class BotUserData {
14 | @Nonnull
15 | private Integer userId = 0;
16 |
17 | /*
18 | * At the time of this writing, the maximum length of data was 1440.
19 | */
20 | private String userdata;
21 | }
22 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/data/GivenRecommendation.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.data;
2 |
3 | import org.tillerino.mormon.KeyColumn;
4 | import org.tillerino.mormon.Table;
5 | import org.tillerino.osuApiModel.types.BeatmapId;
6 | import org.tillerino.osuApiModel.types.BitwiseMods;
7 | import org.tillerino.osuApiModel.types.UserId;
8 |
9 | import lombok.Data;
10 |
11 | @Table("givenrecommendations")
12 | @KeyColumn("id")
13 | @Data
14 | public class GivenRecommendation {
15 | public GivenRecommendation(@UserId int userid, @BeatmapId int beatmapid,
16 | long date, @BitwiseMods long mods) {
17 | super();
18 | this.userid = userid;
19 | this.beatmapid = beatmapid;
20 | this.date = date;
21 | this.mods = mods;
22 | }
23 |
24 | public GivenRecommendation() {
25 |
26 | }
27 |
28 | private Integer id;
29 |
30 | @UserId
31 | private int userid;
32 | @BeatmapId
33 | private int beatmapid;
34 | private long date;
35 | @BitwiseMods
36 | public long mods;
37 |
38 | /**
39 | * If true, this won't be taken into consideration when generating
40 | * recommendations.
41 | */
42 | private boolean forgotten = false;
43 | /**
44 | * If true, this won't be displayed in the recommendations list in ppaddict
45 | * anymore.
46 | */
47 | private boolean hidden = false;
48 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/data/UserNameMapping.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.data;
2 |
3 |
4 | import org.tillerino.mormon.KeyColumn;
5 | import org.tillerino.mormon.Table;
6 | import org.tillerino.osuApiModel.types.UserId;
7 | import org.tillerino.ppaddict.chat.IRCName;
8 |
9 | import lombok.Data;
10 |
11 | @Table("usernames")
12 | @KeyColumn("userName")
13 | @Data
14 | public class UserNameMapping {
15 | @IRCName
16 | private String userName;
17 |
18 | @UserId
19 | private int userid;
20 |
21 | private long resolved;
22 |
23 | private long firstresolveattempt;
24 | }
25 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/diff/.gitignore:
--------------------------------------------------------------------------------
1 | /OsuScore.cpp
2 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/diff/BeatmapImpl.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.diff;
2 |
3 | import org.tillerino.osuApiModel.types.BitwiseMods;
4 |
5 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
6 | import lombok.Builder;
7 |
8 | /**
9 | * This class implements {@link Beatmap} which is the interface that the
10 | * translated pp calculation uses to get info about the beatmap.
11 | */
12 | @Builder
13 | // suppress warning about case-insensitive field collision, because we cannot change the names in CBeatmap
14 | @SuppressWarnings("squid:S1845")
15 | @SuppressFBWarnings("NM")
16 | public record BeatmapImpl(
17 | @BitwiseMods long modsUsed,
18 | float starDiff,
19 | float aim,
20 | float speed,
21 | float overallDifficulty,
22 | float approachRate,
23 | int maxCombo,
24 | float sliderFactor,
25 | float flashlight,
26 | float speedNoteCount,
27 | int circleCount,
28 | int spinnerCount,
29 | int sliderCount) implements Beatmap {
30 |
31 |
32 | @Override
33 | public float DifficultyAttribute(long mods, int kind) {
34 | if (Beatmap.getDiffMods(mods) != modsUsed) {
35 | throw new IllegalArgumentException("Unexpected mods " + mods + ". Was loaded with " + modsUsed);
36 | }
37 |
38 | return switch (kind) {
39 | case Beatmap.Aim -> aim;
40 | case Beatmap.Speed -> speed;
41 | case Beatmap.OD -> overallDifficulty;
42 | case Beatmap.AR -> approachRate;
43 | case Beatmap.MaxCombo -> maxCombo;
44 | case Beatmap.SliderFactor -> sliderFactor;
45 | case Beatmap.Flashlight -> flashlight;
46 | case Beatmap.SpeedNoteCount -> speedNoteCount;
47 | default -> throw new IllegalArgumentException("Unexpected kind: " + kind);
48 | };
49 | }
50 |
51 | @Override
52 | public int NumHitCircles() {
53 | return circleCount;
54 | }
55 |
56 | @Override
57 | public int NumSpinners() {
58 | return spinnerCount;
59 | }
60 |
61 | @Override
62 | public int NumSliders() {
63 | return sliderCount;
64 | }
65 |
66 | public int getObjectCount() {
67 | return circleCount + sliderCount + spinnerCount;
68 | }
69 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/diff/MathHelper.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.diff;
2 |
3 | import static java.lang.Math.max;
4 | import static java.lang.Math.min;
5 |
6 | @SuppressWarnings( "squid:S00100" )
7 | public final class MathHelper {
8 | private MathHelper() {
9 | // utility class
10 | }
11 |
12 | static float static_cast_f32(int x) {
13 | return x;
14 | }
15 |
16 | static float static_cast_f32(double x) {
17 | return (float) x;
18 | }
19 |
20 | static int static_cast_s32(double x) {
21 | return (int) x;
22 | }
23 |
24 | static float std_pow(float b, float e) {
25 | return (float) Math.pow(b, e);
26 | }
27 |
28 | static float pow(float b, float e) {
29 | return (float) Math.pow(b, e);
30 | }
31 |
32 | static float Clamp(float x, float min, float max) {
33 | return max(min, min(max, x));
34 | }
35 |
36 | static float log10(float x) {
37 | return (float) Math.log10(x);
38 | }
39 |
40 | static float std_max(float x, float y) {
41 | return Math.max(x, y);
42 | }
43 |
44 | static float std_min(float x, float y) {
45 | return Math.min(x, y);
46 | }
47 |
48 | static int std_max(int x, int y) {
49 | return Math.max(x, y);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/diff/PercentageEstimates.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.diff;
2 |
3 | import org.tillerino.osuApiModel.types.BitwiseMods;
4 |
5 | import tillerino.tillerinobot.UserException;
6 |
7 | /**
8 | * Provides pp values for a beatmap played with a specified mod.
9 | * "PercentageEstimates" refers to the fact that the accuracy is specified and
10 | * that the value is estimated although the value is usually quite accurate.
11 | */
12 | public interface PercentageEstimates {
13 | public double getPP(double acc);
14 |
15 | public double getPP(double acc, int combo, int misses) throws UserException;
16 |
17 | public double getPP(int x100, int x50, int combo, int misses);
18 |
19 | @BitwiseMods
20 | long getMods();
21 |
22 | double getStarDiff();
23 |
24 | double getApproachRate();
25 |
26 | double getOverallDifficulty();
27 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/diff/PercentageEstimatesImpl.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.diff;
2 |
3 | import org.tillerino.osuApiModel.types.BitwiseMods;
4 |
5 | import lombok.Getter;
6 | import tillerino.tillerinobot.UserException;
7 |
8 | public class PercentageEstimatesImpl implements PercentageEstimates {
9 | private final BeatmapImpl beatmap;
10 |
11 | @Getter
12 | private final @BitwiseMods long mods;
13 |
14 | public PercentageEstimatesImpl(BeatmapImpl beatmap, @BitwiseMods long mods) {
15 | this.beatmap = beatmap;
16 | this.mods = mods;
17 | }
18 |
19 | @Override
20 | public double getPP(double acc) {
21 | AccuracyDistribution dist;
22 | try {
23 | dist = AccuracyDistribution.model(beatmap.getObjectCount(), 0, acc);
24 | } catch (UserException e) {
25 | // this should have been allowed to get here.
26 | throw new RuntimeException(e);
27 | }
28 |
29 | OsuScore score = new OsuScore((int) beatmap.DifficultyAttribute(getMods(), Beatmap.MaxCombo),
30 | dist.getX300(), dist.getX100(), dist.getX50(), dist.getMiss(), getMods());
31 |
32 | return score.getPP(beatmap);
33 | }
34 |
35 | @Override
36 | public double getPP(double acc, int combo, int misses) throws UserException {
37 | AccuracyDistribution dist = AccuracyDistribution.model(beatmap.getObjectCount(), misses, acc);
38 |
39 | OsuScore score = new OsuScore(combo, dist.getX300(), dist.getX100(), dist.getX50(), dist.getMiss(),
40 | getMods());
41 |
42 | return score.getPP(beatmap);
43 | }
44 |
45 | @Override
46 | public double getPP(int x100, int x50, int combo, int misses) {
47 | int x300 = beatmap.getObjectCount() - x50 - x100;
48 | OsuScore score = new OsuScore(combo, x300, x100, x50, misses, getMods());
49 |
50 | return score.getPP(beatmap);
51 | }
52 |
53 | @Override
54 | public double getStarDiff() {
55 | return beatmap.starDiff();
56 | }
57 |
58 | @Override
59 | public double getApproachRate() {
60 | return beatmap.approachRate();
61 | }
62 |
63 | @Override
64 | public double getOverallDifficulty() {
65 | return beatmap.overallDifficulty();
66 | }
67 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/diff/sandoku/SanDokuError.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.diff.sandoku;
2 |
3 | import java.util.List;
4 | import java.util.Map;
5 |
6 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
7 |
8 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
9 |
10 | @SuppressFBWarnings(value = { "EI_EXPOSE_REP", "EI_EXPOSE_REP2" },
11 | justification = "Yes, but also this is wayyy too annoying to do correctly. It's a DTO, relax.")
12 | @JsonIgnoreProperties(ignoreUnknown = true)
13 | public record SanDokuError(
14 | String title,
15 | Map> errors
16 | ) {
17 | }
18 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/diff/sandoku/SanDokuResponse.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.diff.sandoku;
2 |
3 | import org.tillerino.osuApiModel.types.BitwiseMods;
4 | import org.tillerino.osuApiModel.types.GameMode;
5 |
6 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
7 |
8 | import lombok.Builder;
9 | import tillerino.tillerinobot.diff.BeatmapImpl;
10 |
11 | @Builder(toBuilder = true)
12 | @JsonIgnoreProperties(ignoreUnknown = true)
13 | public record SanDokuResponse(
14 | @GameMode int beatmapGameMode,
15 | @GameMode int gameModeUsed,
16 | @BitwiseMods long modsUsed,
17 | SanDokuDiffCalcResult diffCalcResult) {
18 |
19 | @Builder(toBuilder = true)
20 | @JsonIgnoreProperties(ignoreUnknown = true)
21 | public static record SanDokuDiffCalcResult(
22 | // only declare fields which are needed for std calc
23 | int maxCombo,
24 | double starRating,
25 | double aim,
26 | double speed,
27 | double overallDifficulty,
28 | double approachRate,
29 | double flashlight,
30 | double sliderFactor,
31 | double speedNoteCount,
32 | int hitCircleCount,
33 | int sliderCount,
34 | int spinnerCount) {
35 | }
36 |
37 | public BeatmapImpl toBeatmap() {
38 | return BeatmapImpl.builder()
39 | .modsUsed(modsUsed)
40 | .starDiff((float) diffCalcResult.starRating)
41 | .aim((float) diffCalcResult.aim)
42 | .speed((float) diffCalcResult.speed)
43 | .overallDifficulty((float) diffCalcResult.overallDifficulty)
44 | .approachRate((float) diffCalcResult.approachRate)
45 | .maxCombo(diffCalcResult.maxCombo)
46 | .sliderFactor((float) diffCalcResult.sliderFactor)
47 | .flashlight((float) diffCalcResult.flashlight)
48 | .speedNoteCount((float) diffCalcResult.speedNoteCount)
49 | .circleCount(diffCalcResult.hitCircleCount)
50 | .spinnerCount(diffCalcResult.spinnerCount)
51 | .sliderCount(diffCalcResult.sliderCount)
52 | .build();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/handlers/ComplaintHandler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers;
2 |
3 | import static org.apache.commons.lang3.StringUtils.getLevenshteinDistance;
4 |
5 | import java.io.IOException;
6 | import java.sql.SQLException;
7 | import java.util.Arrays;
8 |
9 | import javax.inject.Inject;
10 |
11 | import org.apache.commons.lang3.ArrayUtils;
12 | import org.tillerino.osuApiModel.OsuApiUser;
13 | import org.tillerino.ppaddict.chat.GameChatResponse;
14 | import org.tillerino.ppaddict.chat.GameChatResponse.Success;
15 |
16 | import lombok.RequiredArgsConstructor;
17 | import lombok.extern.slf4j.Slf4j;
18 | import tillerino.tillerinobot.CommandHandler;
19 | import tillerino.tillerinobot.UserDataManager.UserData;
20 | import tillerino.tillerinobot.UserException;
21 | import tillerino.tillerinobot.lang.Language;
22 | import tillerino.tillerinobot.recommendations.Recommendation;
23 | import tillerino.tillerinobot.recommendations.RecommendationsManager;
24 |
25 | @Slf4j
26 | @RequiredArgsConstructor(onConstructor = @__(@Inject))
27 | public class ComplaintHandler implements CommandHandler {
28 | private final RecommendationsManager manager;
29 |
30 | @Override
31 | public GameChatResponse handle(String command, OsuApiUser apiUser, UserData userData, Language lang)
32 | throws UserException, IOException, SQLException,
33 | InterruptedException {
34 | if(getLevenshteinDistance(command.toLowerCase().substring(0, Math.min("complain".length(), command.length())), "complain") <= 2) {
35 | Recommendation lastRecommendation = manager
36 | .getLastRecommendation(apiUser.getUserId());
37 | if(lastRecommendation != null && lastRecommendation.beatmap != null) {
38 | log.debug("COMPLAINT: " + lastRecommendation.beatmap.getBeatmap().getBeatmapId() + " mods: " + lastRecommendation.bareRecommendation.mods() + ". Recommendation source: " + Arrays.asList(ArrayUtils.toObject(lastRecommendation.bareRecommendation.causes())));
39 | return new Success(lang.complaint());
40 | }
41 | }
42 | return null;
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/handlers/FixIDHandler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers;
2 |
3 | import java.io.IOException;
4 | import java.sql.SQLException;
5 |
6 | import javax.inject.Inject;
7 |
8 | import org.tillerino.osuApiModel.OsuApiUser;
9 | import org.tillerino.osuApiModel.types.UserId;
10 | import org.tillerino.ppaddict.chat.GameChatResponse;
11 | import org.tillerino.ppaddict.chat.GameChatResponse.Message;
12 |
13 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
14 | import lombok.RequiredArgsConstructor;
15 | import tillerino.tillerinobot.CommandHandler;
16 | import tillerino.tillerinobot.IrcNameResolver;
17 | import tillerino.tillerinobot.UserDataManager.UserData;
18 | import tillerino.tillerinobot.UserException;
19 | import tillerino.tillerinobot.lang.Language;
20 |
21 | @RequiredArgsConstructor(onConstructor = @__(@Inject))
22 | public class FixIDHandler implements CommandHandler {
23 | private static final String COMMAND = "!fixid";
24 | private final IrcNameResolver resolver;
25 |
26 | @Override
27 | public GameChatResponse handle(String command, OsuApiUser apiUser, UserData userData, Language lang)
28 | throws UserException, IOException, SQLException {
29 |
30 | if (!command.toLowerCase().startsWith(COMMAND)) {
31 | return null;
32 | }
33 |
34 | String idStr = command.substring(COMMAND.length()).trim();
35 | int id = parseId(idStr);
36 |
37 | OsuApiUser user = resolver.resolveManually(id);
38 | if(user == null) {
39 | throw new UserException("That user-id does not exist :(");
40 | } else {
41 | return new Message("User '" + user.getUserName() + "' is now resolvable to user-id " + user.getUserId());
42 | }
43 | }
44 |
45 | @SuppressFBWarnings("TQ")
46 | @UserId
47 | private int parseId(String idStr) throws UserException {
48 | try {
49 | return Integer.parseInt(idStr);
50 | } catch (NumberFormatException e) {
51 | throw new UserException("Invalid user-id :(");
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/handlers/HelpHandler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers;
2 |
3 | import static org.apache.commons.lang3.StringUtils.getLevenshteinDistance;
4 |
5 | import java.io.IOException;
6 | import java.sql.SQLException;
7 |
8 | import org.tillerino.osuApiModel.OsuApiUser;
9 | import org.tillerino.ppaddict.chat.GameChatResponse;
10 | import org.tillerino.ppaddict.chat.GameChatResponse.Success;
11 |
12 | import tillerino.tillerinobot.CommandHandler;
13 | import tillerino.tillerinobot.UserDataManager.UserData;
14 | import tillerino.tillerinobot.UserException;
15 | import tillerino.tillerinobot.lang.Language;
16 |
17 | public class HelpHandler implements CommandHandler {
18 |
19 | @Override
20 | public GameChatResponse handle(String command, OsuApiUser apiUser, UserData userData, Language lang)
21 | throws UserException, IOException, SQLException,
22 | InterruptedException {
23 | if (getLevenshteinDistance(command.toLowerCase(), "help") <= 1) {
24 | return new Success(lang.help());
25 | } else if (getLevenshteinDistance(command.toLowerCase(), "faq") <= 1) {
26 | return new Success(lang.faq());
27 | }
28 | return null;
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/handlers/LinkPpaddictHandler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers;
2 |
3 | import java.io.IOException;
4 | import java.math.BigInteger;
5 | import java.security.SecureRandom;
6 | import java.sql.SQLException;
7 | import java.util.regex.Pattern;
8 |
9 | import org.apache.commons.lang3.StringUtils;
10 | import org.tillerino.osuApiModel.OsuApiUser;
11 | import org.tillerino.ppaddict.chat.GameChatResponse;
12 | import org.tillerino.ppaddict.chat.GameChatResponse.Success;
13 |
14 | import lombok.RequiredArgsConstructor;
15 | import tillerino.tillerinobot.BotBackend;
16 | import tillerino.tillerinobot.CommandHandler;
17 | import tillerino.tillerinobot.UserDataManager.UserData;
18 | import tillerino.tillerinobot.UserException;
19 | import tillerino.tillerinobot.lang.Language;
20 |
21 | @RequiredArgsConstructor
22 | public class LinkPpaddictHandler implements CommandHandler {
23 | public static final Pattern TOKEN_PATTERN = Pattern.compile("[0-9a-z]{32}");
24 | private static final SecureRandom random = new SecureRandom();
25 |
26 | private final BotBackend backend;
27 |
28 | @Override
29 | public GameChatResponse handle(String command, OsuApiUser apiUser, UserData userData, Language lang)
30 | throws UserException, IOException, SQLException {
31 | if(!TOKEN_PATTERN.matcher(command).matches()) {
32 | return null;
33 | }
34 | String linkedName = backend.tryLinkToPatreon(command, apiUser);
35 | if(linkedName == null)
36 | throw new UserException("nothing happened.");
37 | else
38 | return new Success("linked to " + linkedName);
39 | }
40 |
41 | public static synchronized String newKey() {
42 | return StringUtils.leftPad(new BigInteger(165, LinkPpaddictHandler.random).toString(36), 32, '0');
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/handlers/RecentHandler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers;
2 |
3 | import java.io.IOException;
4 | import java.sql.SQLException;
5 | import java.util.List;
6 |
7 | import org.tillerino.osuApiModel.OsuApiScore;
8 | import org.tillerino.osuApiModel.OsuApiUser;
9 | import org.tillerino.ppaddict.chat.GameChatResponse;
10 | import org.tillerino.ppaddict.chat.GameChatResponse.Success;
11 |
12 | import lombok.Value;
13 | import tillerino.tillerinobot.BeatmapMeta;
14 | import tillerino.tillerinobot.BotBackend;
15 | import tillerino.tillerinobot.CommandHandler;
16 | import tillerino.tillerinobot.UserDataManager.UserData;
17 | import tillerino.tillerinobot.UserException;
18 | import tillerino.tillerinobot.lang.Language;
19 |
20 | @Value
21 | public class RecentHandler implements CommandHandler {
22 | BotBackend backend;
23 |
24 | @Override
25 | public GameChatResponse handle(String command, OsuApiUser apiUser, UserData userData, Language language)
26 | throws UserException, IOException, SQLException, InterruptedException {
27 | if (!command.equalsIgnoreCase("now")) {
28 | return null;
29 | }
30 |
31 | if(userData.getHearts() <= 0) {
32 | return null;
33 | }
34 |
35 | List recentPlays = backend.getRecentPlays(apiUser.getUserId());
36 | if (recentPlays.isEmpty()) {
37 | throw new UserException(language.noRecentPlays());
38 | }
39 |
40 | OsuApiScore score = recentPlays.get(0);
41 |
42 | final BeatmapMeta estimates = backend.loadBeatmap(score.getBeatmapId(), score.getMods(), language);
43 |
44 | if (estimates == null) {
45 | throw new UserException(language.unknownBeatmap());
46 | }
47 | if (estimates.getMods() != score.getMods()) {
48 | throw new UserException(language.noInformationForMods());
49 | }
50 |
51 | userData.setLastSongInfo(estimates.getBeatmapWithMods());
52 | return new Success(estimates.formInfoMessage(false, true, null,
53 | userData.getHearts(), score.getAccuracy(), null, null));
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/handlers/ResetHandler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers;
2 |
3 | import java.io.IOException;
4 | import java.sql.SQLException;
5 |
6 | import javax.inject.Inject;
7 |
8 | import org.tillerino.osuApiModel.OsuApiUser;
9 | import org.tillerino.ppaddict.chat.GameChatResponse;
10 |
11 | import tillerino.tillerinobot.CommandHandler;
12 | import tillerino.tillerinobot.UserDataManager.UserData;
13 | import tillerino.tillerinobot.UserException;
14 | import tillerino.tillerinobot.lang.Language;
15 | import tillerino.tillerinobot.recommendations.RecommendationsManager;
16 |
17 | public class ResetHandler implements CommandHandler {
18 | RecommendationsManager backend;
19 |
20 | @Inject
21 | public ResetHandler(RecommendationsManager backend) {
22 | super();
23 | this.backend = backend;
24 | }
25 |
26 | @Override
27 | public GameChatResponse handle(String command, OsuApiUser apiUser, UserData userData, Language lang)
28 | throws UserException, IOException, SQLException {
29 | if (!command.equalsIgnoreCase("reset"))
30 | return null;
31 |
32 | backend.forgetRecommendations(apiUser.getUserId());
33 |
34 | return GameChatResponse.none();
35 | }
36 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/handlers/options/BooleanOptionHandler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers.options;
2 |
3 | import org.tillerino.osuApiModel.OsuApiUser;
4 | import org.tillerino.ppaddict.chat.GameChatResponse;
5 |
6 | import tillerino.tillerinobot.UserDataManager.UserData;
7 | import tillerino.tillerinobot.UserException;
8 | import tillerino.tillerinobot.lang.Language;
9 |
10 | import javax.annotation.Nonnull;
11 | import javax.annotation.Nullable;
12 |
13 | public abstract class BooleanOptionHandler extends OptionHandler {
14 | protected BooleanOptionHandler(@Nonnull String description, @Nonnull String optionName, @Nullable String shortOptionName, int minHearts) {
15 | super(description, optionName, shortOptionName, minHearts);
16 | }
17 |
18 | @Override
19 | protected void handleSet(String value, UserData userData, OsuApiUser apiUser, Language lang) throws UserException {
20 | handleSetBoolean(parseBoolean(value, lang), userData);
21 | }
22 |
23 | @Nonnull
24 | @Override
25 | protected String getCurrentValue(UserData userData) {
26 | return getCurrentBooleanValue(userData) ? "ON" : "OFF";
27 | }
28 |
29 | protected abstract void handleSetBoolean(boolean value, UserData userData);
30 |
31 | protected abstract boolean getCurrentBooleanValue(UserData userData);
32 |
33 | public static boolean parseBoolean(final @Nonnull String original, Language lang) throws UserException {
34 | String s = original.toLowerCase();
35 | if (s.equals("on") || s.equals("true") || s.equals("yes") || s.equals("1")) {
36 | return true;
37 | }
38 | if (s.equals("off") || s.equals("false") || s.equals("no") || s.equals("0")) {
39 | return false;
40 | }
41 | throw new UserException(lang.invalidChoice(original, "on|true|yes|1|off|false|no|0"));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/handlers/options/DefaultOptionHandler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers.options;
2 |
3 | import org.apache.commons.lang3.StringUtils;
4 | import org.tillerino.osuApiModel.OsuApiUser;
5 | import tillerino.tillerinobot.UserDataManager;
6 | import tillerino.tillerinobot.UserException;
7 | import tillerino.tillerinobot.lang.Language;
8 | import tillerino.tillerinobot.recommendations.RecommendationRequestParser;
9 |
10 | import javax.annotation.Nonnull;
11 | import java.io.IOException;
12 | import java.sql.SQLException;
13 |
14 | public class DefaultOptionHandler extends OptionHandler {
15 | private final RecommendationRequestParser requestParser;
16 |
17 | public DefaultOptionHandler(RecommendationRequestParser requestParser) {
18 | super("Default recommendation settings", "default", null, 0);
19 | this.requestParser = requestParser;
20 | }
21 |
22 | @Override
23 | protected void handleSet(String value, UserDataManager.UserData userData, OsuApiUser apiUser, Language lang) throws UserException, SQLException, IOException {
24 | if (value.isEmpty()) {
25 | userData.setDefaultRecommendationOptions(null);
26 | } else {
27 | requestParser.parseSamplerSettings(apiUser, value, lang);
28 | userData.setDefaultRecommendationOptions(value);
29 | }
30 | }
31 |
32 | @Nonnull
33 | @Override
34 | protected String getCurrentValue(UserDataManager.UserData userData) {
35 | return StringUtils.defaultString(userData.getDefaultRecommendationOptions(), "-");
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/handlers/options/LangOptionHandler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers.options;
2 |
3 | import org.tillerino.osuApiModel.OsuApiUser;
4 | import org.tillerino.ppaddict.chat.GameChatResponse;
5 | import tillerino.tillerinobot.UserDataManager.UserData;
6 | import tillerino.tillerinobot.UserException;
7 | import tillerino.tillerinobot.handlers.OptionsHandler;
8 | import tillerino.tillerinobot.lang.Language;
9 | import tillerino.tillerinobot.lang.LanguageIdentifier;
10 |
11 | import javax.annotation.Nonnull;
12 | import java.util.stream.Stream;
13 |
14 | import static java.util.stream.Collectors.joining;
15 |
16 | public class LangOptionHandler extends OptionHandler {
17 | public LangOptionHandler() {
18 | super("Language", "language", "lang", 0);
19 | }
20 |
21 | @Override
22 | protected void handleSet(String value, UserData userData, OsuApiUser apiUser, Language lang) throws UserException {
23 | LanguageIdentifier ident;
24 | try {
25 | ident = OptionsHandler.find(LanguageIdentifier.values(), i -> i.token, value);
26 | } catch (IllegalArgumentException e) {
27 | String choices = Stream.of(LanguageIdentifier.values())
28 | .map(i -> i.token)
29 | .sorted()
30 | .collect(joining(", "));
31 | throw new UserException(lang.invalidChoice(value, choices));
32 | }
33 |
34 | userData.setLanguage(ident);
35 | }
36 |
37 | @Override
38 | protected GameChatResponse responseAfterSet(UserData userData, OsuApiUser apiUser) {
39 | return userData.usingLanguage(lang -> lang.optionalCommentOnLanguage(apiUser));
40 | }
41 |
42 | @Override
43 | @Nonnull
44 | protected String getCurrentValue(UserData userData) {
45 | return userData.getLanguageIdentifier().token;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/handlers/options/MapMetaDataOptionHandler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers.options;
2 |
3 | import tillerino.tillerinobot.UserDataManager.UserData;
4 |
5 | public class MapMetaDataOptionHandler extends BooleanOptionHandler{
6 | public MapMetaDataOptionHandler() {
7 | super("Show map metadata on recommendations", "r-metadata", null, 0);
8 | }
9 |
10 | @Override
11 | protected void handleSetBoolean(boolean value, UserData userData) {
12 | userData.setShowMapMetaDataOnRecommendation(value);
13 | }
14 |
15 | @Override
16 | protected boolean getCurrentBooleanValue(UserData userData) {
17 | return userData.isShowMapMetaDataOnRecommendation();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/handlers/options/OsutrackWelcomeOptionHandler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers.options;
2 |
3 | import tillerino.tillerinobot.UserDataManager.UserData;
4 |
5 | public class OsutrackWelcomeOptionHandler extends BooleanOptionHandler {
6 | public OsutrackWelcomeOptionHandler() {
7 | super("osu!track on welcome", "osutrack-welcome", null, 1);
8 | }
9 |
10 | @Override
11 | protected void handleSetBoolean(boolean value, UserData userData) {
12 | userData.setOsuTrackWelcomeEnabled(value);
13 | }
14 |
15 | @Override
16 | protected boolean getCurrentBooleanValue(UserData userData) {
17 | return userData.isOsuTrackWelcomeEnabled();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/handlers/options/WelcomeOptionHandler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers.options;
2 |
3 | import tillerino.tillerinobot.UserDataManager.UserData;
4 |
5 | public class WelcomeOptionHandler extends BooleanOptionHandler{
6 | public WelcomeOptionHandler() {
7 | super("Welcome Message", "welcome", null, 1);
8 | }
9 |
10 | @Override
11 | protected void handleSetBoolean(boolean value, UserData userData) {
12 | userData.setShowWelcomeMessage(value);
13 | }
14 |
15 | @Override
16 | protected boolean getCurrentBooleanValue(UserData userData) {
17 | return userData.isShowWelcomeMessage();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/lang/AbstractMutableLanguage.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.lang;
2 |
3 | import tillerino.tillerinobot.util.IsMutable;
4 |
5 | public abstract class AbstractMutableLanguage implements Language, IsMutable {
6 | private transient boolean modified;
7 |
8 | @Override
9 | public boolean isModified() {
10 | return modified;
11 | }
12 |
13 | @Override
14 | public void clearModified() {
15 | modified = false;
16 | }
17 |
18 | /**
19 | * After this method has been called, calls to {@link #isModified()} will return
20 | * true until {@link #clearModified()} is called.
21 | */
22 | protected void registerModification() {
23 | modified = true;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/lang/LanguageIdentifier.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.lang;
2 |
3 | // suppress warning about enum constant naming pattern
4 | @SuppressWarnings("squid:S00115")
5 | public enum LanguageIdentifier {
6 | Default(Default.class),
7 | English(Default.class),
8 | Tsundere(TsundereEnglish.class),
9 | TsundereGerman(TsundereGerman.class),
10 | Italiano(Italiano.class),
11 | Français(Francais.class),
12 | Polski(Polish.class),
13 | Nederlands(Nederlands.class),
14 | עברית(Hebrew.class),
15 | Farsi(Farsi.class),
16 | Português_BR(Portuguese.class),
17 | Deutsch(Deutsch.class),
18 | Čeština(Czech.class),
19 | Magyar(Hungarian.class),
20 | 한국어(Korean.class),
21 | Dansk(Dansk.class),
22 | Türkçe(Turkish.class),
23 | 日本語(Japanese.class),
24 | Español(Spanish.class),
25 | Ελληνικά(Greek.class),
26 | Русский(Russian.class),
27 | Lietuvių(Lithuanian.class),
28 | Português_PT(PortuguesePortugal.class),
29 | Svenska(Svenska.class),
30 | Romana(Romana.class),
31 | 繁體中文(ChineseTraditional.class),
32 | български(Bulgarian.class),
33 | Norsk(Norwegian.class),
34 | Indonesian(Indonesian.class),
35 | 简体中文(ChineseSimple.class),
36 | Català(Catalan.class),
37 | Slovenščina(Slovenian.class),
38 | Schwiizerdütsch(Swissgerman.class),
39 | Slovenčina(Slovak.class),
40 | Vietnamese(Vietnamese.class, "Tiếng Việt"),
41 | ; // please end identifier entries with a comma and leave this semicolon here
42 |
43 | public final Class extends Language> cls;
44 |
45 | public final String token;
46 |
47 | private LanguageIdentifier(Class extends Language> cls) {
48 | this.cls = cls;
49 | this.token = name();
50 | }
51 |
52 | private LanguageIdentifier(Class extends Language> cls, String token) {
53 | this.cls = cls;
54 | this.token = token;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/lang/StringShuffler.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.lang;
2 |
3 | import java.util.Arrays;
4 | import java.util.Collections;
5 | import java.util.Random;
6 |
7 | import com.fasterxml.jackson.annotation.JsonCreator;
8 |
9 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
10 | import lombok.AllArgsConstructor;
11 | import lombok.Getter;
12 |
13 | /*
14 | * 20 bytes
15 | */
16 | /**
17 | * Device to permanently shuffle an array of Strings while the size of this
18 | * object is independent of the number of strings. This is accomplished by
19 | * saving the seed for shuffling the array and shuffling it every time a String
20 | * is requested instead of shuffling it once.
21 | */
22 | @AllArgsConstructor(onConstructor = @__(@JsonCreator))
23 | @Getter
24 | public class StringShuffler {
25 | /*
26 | * 8 bytes;
27 | */
28 | private final long seed;
29 |
30 | @SuppressFBWarnings(value = "DMI_RANDOM_USED_ONLY_ONCE", justification = "false positive")
31 | public StringShuffler(Random globalRandom) {
32 | seed = globalRandom.nextLong();
33 | }
34 |
35 | /*
36 | * 4 bytes
37 | */
38 | private int index = 0;
39 |
40 | public String get(String... strings) {
41 | Random random = new Random(seed);
42 |
43 | String[] forShuffling = strings.clone();
44 |
45 | Collections.shuffle(Arrays.asList(forShuffling), random);
46 |
47 | return forShuffling[(index++) % forShuffling.length];
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/osutrack/Highscore.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.osutrack;
2 |
3 | import lombok.Data;
4 | import lombok.EqualsAndHashCode;
5 | import lombok.ToString;
6 | import org.tillerino.osuApiModel.OsuApiScore;
7 |
8 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
9 |
10 | @JsonIgnoreProperties(ignoreUnknown = true)
11 | @Data
12 | @ToString(callSuper = true)
13 | @EqualsAndHashCode(callSuper = true)
14 | public class Highscore extends OsuApiScore {
15 | private int ranking;
16 | }
17 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/osutrack/OsutrackApi.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.osutrack;
2 |
3 | import jakarta.ws.rs.POST;
4 | import jakarta.ws.rs.Path;
5 | import jakarta.ws.rs.Produces;
6 | import jakarta.ws.rs.QueryParam;
7 | import jakarta.ws.rs.core.MediaType;
8 |
9 | public interface OsutrackApi {
10 | @POST
11 | @Path("update")
12 | @Produces(MediaType.APPLICATION_JSON)
13 | UpdateResult getUpdate(@QueryParam("user") int osuUserId);
14 | }
15 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/osutrack/OsutrackDownloader.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.osutrack;
2 |
3 | import jakarta.ws.rs.client.ClientBuilder;
4 | import org.glassfish.jersey.client.proxy.WebResourceFactory;
5 |
6 | public class OsutrackDownloader {
7 | // __________________________________________________________________________________________
8 | // /\ \
9 | // \_| If you're reading this and thinking, hey, I wanna use that osutrack API as well: |
10 | // | --> PLEASE ask FIRST for permission from ameo (https://ameobea.me/osutrack/) <-- |
11 | // | _____________________________________________________________________________________|_
12 | // \_/_______________________________________________________________________________________/
13 | private static final String OSUTRACK_API_BASE = "https://osutrack-api.ameo.dev/";
14 | private static final OsutrackApi OSUTRACK_API = WebResourceFactory.newResource(OsutrackApi.class, ClientBuilder
15 | .newClient()
16 | .target(OSUTRACK_API_BASE)
17 | .queryParam("mode", 0));
18 |
19 | protected void completeUpdateObject(UpdateResult updateResult) {
20 | for (Highscore highscore : updateResult.getNewHighscores()) {
21 | highscore.setMode(0);
22 | }
23 | }
24 |
25 | public UpdateResult getUpdate(int osuUserId) {
26 | UpdateResult updateResult = OSUTRACK_API.getUpdate(osuUserId);
27 | completeUpdateObject(updateResult);
28 | return updateResult;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/osutrack/UpdateResult.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.osutrack;
2 |
3 | import java.util.List;
4 |
5 | import org.tillerino.osuApiModel.types.GameMode;
6 |
7 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
8 | import com.fasterxml.jackson.annotation.JsonProperty;
9 |
10 | import lombok.Data;
11 |
12 | @JsonIgnoreProperties(ignoreUnknown = true)
13 | @Data
14 | public class UpdateResult {
15 | private String username;
16 |
17 | @GameMode
18 | private int mode;
19 |
20 | @JsonProperty("playcount")
21 | private int playCount;
22 |
23 | @JsonProperty("pp_rank")
24 | private int ppRank;
25 |
26 | @JsonProperty("pp_raw")
27 | private float ppRaw;
28 |
29 | private float accuracy;
30 |
31 | @JsonProperty("total_score")
32 | private long totalScore;
33 |
34 | @JsonProperty("ranked_score")
35 | private long rankedScore;
36 |
37 | private int count300;
38 |
39 | private int count100;
40 |
41 | private int count50;
42 |
43 | private float level;
44 |
45 | @JsonProperty("count_rank_a")
46 | private int countRankA;
47 |
48 | @JsonProperty("count_rank_s")
49 | private int countRankS;
50 |
51 | @JsonProperty("count_rank_ss")
52 | private int countRankSS;
53 |
54 | @JsonProperty("levelup")
55 | private boolean levelUp;
56 |
57 | private boolean first;
58 |
59 | private boolean exists;
60 |
61 | @JsonProperty("newhs")
62 | private List newHighscores;
63 | }
64 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/predicates/ApproachRate.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import org.tillerino.osuApiModel.OsuApiBeatmap;
4 |
5 | import lombok.EqualsAndHashCode;
6 |
7 | @EqualsAndHashCode
8 | public class ApproachRate implements NumericBeatmapProperty {
9 | @Override
10 | public String getName() {
11 | return "AR";
12 | }
13 |
14 | @Override
15 | public double getValue(OsuApiBeatmap beatmap, long mods) {
16 | return beatmap.getApproachRate(mods);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/predicates/BeatsPerMinute.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import lombok.EqualsAndHashCode;
4 |
5 | import org.tillerino.osuApiModel.OsuApiBeatmap;
6 |
7 | @EqualsAndHashCode
8 | public class BeatsPerMinute implements NumericBeatmapProperty {
9 |
10 | @Override
11 | public String getName() {
12 | return "BPM";
13 | }
14 |
15 | @Override
16 | public double getValue(OsuApiBeatmap beatmap, long mods) {
17 | return beatmap.getBpm(mods);
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/predicates/CircleSize.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import lombok.EqualsAndHashCode;
4 | import org.tillerino.osuApiModel.OsuApiBeatmap;
5 |
6 | @EqualsAndHashCode
7 | public class CircleSize implements NumericBeatmapProperty {
8 |
9 | @Override
10 | public String getName() {
11 | return "CS";
12 | }
13 |
14 | @Override
15 | public double getValue(OsuApiBeatmap beatmap, long mods) {
16 | return beatmap.getCircleSize(mods);
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/predicates/ExcludeMod.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import lombok.Value;
4 |
5 | import java.util.Optional;
6 |
7 | import org.tillerino.osuApiModel.Mods;
8 | import org.tillerino.osuApiModel.OsuApiBeatmap;
9 |
10 | import tillerino.tillerinobot.UserException;
11 | import tillerino.tillerinobot.lang.Language;
12 | import tillerino.tillerinobot.predicates.PredicateParser.PredicateBuilder;
13 | import tillerino.tillerinobot.recommendations.BareRecommendation;
14 | import tillerino.tillerinobot.recommendations.RecommendationRequest;
15 |
16 | @Value
17 | public class ExcludeMod implements RecommendationPredicate {
18 | Mods mod;
19 |
20 | @Override
21 | public boolean test(BareRecommendation r, OsuApiBeatmap beatmap) {
22 | return !mod.is(r.mods());
23 | }
24 |
25 | @Override
26 | public boolean contradicts(RecommendationPredicate otherPredicate) {
27 | return false;
28 | }
29 |
30 | @Override
31 | public String getOriginalArgument() {
32 | return "-" + mod.getShortName();
33 | }
34 |
35 | public static class Builder implements PredicateBuilder {
36 | @Override
37 | public ExcludeMod build(String argument, Language lang) throws UserException {
38 | if (!argument.startsWith("-")) {
39 | return null;
40 | }
41 | try {
42 | Mods mod = Mods.fromShortName(argument.substring(1).toUpperCase());
43 | if (mod == null) {
44 | return null;
45 | }
46 | return new ExcludeMod(mod);
47 | } catch (IllegalArgumentException e) {
48 | return null;
49 | }
50 | }
51 |
52 | }
53 |
54 | @Override
55 | public Optional findNonPredicateContradiction(RecommendationRequest request) {
56 | if (mod.is(request.requestedMods())) {
57 | return Optional.of(String.format("%s -%s", mod.getShortName(), mod.getShortName()));
58 | }
59 | return Optional.empty();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/predicates/MapLength.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import lombok.EqualsAndHashCode;
4 |
5 | import org.tillerino.osuApiModel.OsuApiBeatmap;
6 |
7 | @EqualsAndHashCode
8 | public class MapLength implements NumericBeatmapProperty {
9 |
10 | @Override
11 | public String getName() {
12 | return "LEN";
13 | }
14 |
15 | @Override
16 | public double getValue(OsuApiBeatmap beatmap, long mods) {
17 | return beatmap.getTotalLength(mods);
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/predicates/NumericBeatmapProperty.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import java.util.Optional;
4 |
5 | import org.tillerino.osuApiModel.OsuApiBeatmap;
6 | import org.tillerino.osuApiModel.types.BitwiseMods;
7 |
8 | import tillerino.tillerinobot.recommendations.RecommendationRequest;
9 |
10 | public interface NumericBeatmapProperty {
11 | String getName();
12 |
13 | double getValue(OsuApiBeatmap beatmap, @BitwiseMods long mods);
14 |
15 | /**
16 | * see
17 | * {@link RecommendationPredicate#findNonPredicateContradiction(RecommendationRequest)}
18 | *
19 | * @param value the parsed value for this property
20 | */
21 | default Optional findNonPredicateContradiction(RecommendationRequest request, NumericPropertyPredicate> value) {
22 | return Optional.empty();
23 | }
24 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/predicates/NumericPropertyPredicate.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import lombok.Value;
4 | import tillerino.tillerinobot.recommendations.BareRecommendation;
5 | import tillerino.tillerinobot.recommendations.RecommendationRequest;
6 |
7 | import java.util.Optional;
8 |
9 | import org.tillerino.osuApiModel.OsuApiBeatmap;
10 |
11 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
12 |
13 | @Value
14 | public class NumericPropertyPredicate
15 | implements RecommendationPredicate {
16 | String originalArgument;
17 | T property;
18 | double min;
19 | boolean includeMin;
20 | double max;
21 | boolean includeMax;
22 |
23 | @Override
24 | public boolean test(BareRecommendation r, OsuApiBeatmap beatmap) {
25 | double value = property.getValue(beatmap, r.mods());
26 |
27 | if(value < min) {
28 | return false;
29 | }
30 | if(value <= min && !includeMin) {
31 | return false;
32 | }
33 | if(value > max) {
34 | return false;
35 | }
36 | return value < max || includeMax;
37 | }
38 |
39 | @Override
40 | @SuppressFBWarnings(value = "SA_LOCAL_SELF_COMPARISON", justification = "Looks like a bug")
41 | public boolean contradicts(RecommendationPredicate otherPredicate) {
42 | if (otherPredicate instanceof NumericPropertyPredicate> predicate
43 | && predicate.property.getClass() == property.getClass()) {
44 | if (predicate.min > max || min > predicate.max) {
45 | return true;
46 | }
47 | if(predicate.min >= max && predicate.includeMin != includeMax) {
48 | return true;
49 | }
50 | if(min >= predicate.max && includeMin != predicate.includeMax) {
51 | return true;
52 | }
53 | }
54 |
55 | return false;
56 | }
57 |
58 | @Override
59 | public Optional findNonPredicateContradiction(RecommendationRequest request) {
60 | return property.findNonPredicateContradiction(request, this);
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/predicates/OverallDifficulty.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import lombok.EqualsAndHashCode;
4 |
5 | import org.tillerino.osuApiModel.OsuApiBeatmap;
6 |
7 | @EqualsAndHashCode
8 | public class OverallDifficulty implements NumericBeatmapProperty {
9 |
10 | @Override
11 | public String getName() {
12 | return "OD";
13 | }
14 |
15 | @Override
16 | public double getValue(OsuApiBeatmap beatmap, long mods) {
17 | return beatmap.getOverallDifficulty(mods);
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/predicates/PredicateParser.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Arrays;
5 | import java.util.List;
6 |
7 | import javax.annotation.CheckForNull;
8 |
9 | import tillerino.tillerinobot.UserException;
10 | import tillerino.tillerinobot.lang.Language;
11 |
12 | public class PredicateParser {
13 | public interface PredicateBuilder {
14 | /**
15 | * Parses the given string.
16 | *
17 | * @param argument
18 | * doesn't contain spaces
19 | * @param lang
20 | * for error messages
21 | * @return null if the argument cannot be parsed
22 | */
23 | T build(String argument, Language lang) throws UserException;
24 | }
25 |
26 | List> builders = new ArrayList<>();
27 |
28 | public PredicateParser() {
29 | List properties = Arrays.asList(new ApproachRate(), new BeatsPerMinute(), new OverallDifficulty(), new MapLength(), new CircleSize(), new StarDiff());
30 |
31 | for (NumericBeatmapProperty property : properties) {
32 | builders.add(new NumericPredicateBuilder<>(property));
33 | }
34 |
35 | builders.add(new ExcludeMod.Builder());
36 | }
37 |
38 | public @CheckForNull RecommendationPredicate tryParse(String argument, Language lang) throws UserException {
39 | for (PredicateBuilder> predicateBuilder : builders) {
40 | RecommendationPredicate predicate = predicateBuilder.build(argument, lang);
41 | if (predicate != null)
42 | return predicate;
43 | }
44 | return null;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/predicates/RecommendationPredicate.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import java.util.Optional;
4 |
5 | import org.tillerino.osuApiModel.OsuApiBeatmap;
6 |
7 | import tillerino.tillerinobot.recommendations.BareRecommendation;
8 | import tillerino.tillerinobot.recommendations.RecommendationRequest;
9 |
10 | public interface RecommendationPredicate {
11 | boolean test(BareRecommendation r, OsuApiBeatmap beatmap);
12 |
13 | /**
14 | * Checks if this predicate contradicts the given predicate.
15 | */
16 | boolean contradicts(RecommendationPredicate otherPredicate);
17 |
18 | /**
19 | * Checks if this predicate contradicts any settings in the request beside other
20 | * predicates.
21 | *
22 | * @return If there is a contradiction, a string describing the contradiction,
23 | * an empty optional otherwise.
24 | */
25 | Optional findNonPredicateContradiction(RecommendationRequest request);
26 |
27 | String getOriginalArgument();
28 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/predicates/StarDiff.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import java.util.Optional;
4 |
5 | import org.tillerino.osuApiModel.Mods;
6 | import org.tillerino.osuApiModel.OsuApiBeatmap;
7 |
8 | import lombok.EqualsAndHashCode;
9 | import tillerino.tillerinobot.recommendations.RecommendationRequest;
10 |
11 | @EqualsAndHashCode
12 | public class StarDiff implements NumericBeatmapProperty {
13 | @Override
14 | public String getName() {
15 | return "STAR";
16 | }
17 |
18 | @Override
19 | public double getValue(OsuApiBeatmap beatmap, long mods) {
20 | return beatmap.getStarDifficulty();
21 | }
22 |
23 | @Override
24 | public Optional findNonPredicateContradiction(RecommendationRequest request, NumericPropertyPredicate> value) {
25 | if (request.requestedMods() != 0L) {
26 | return Optional.of(String.format("%s %s", Mods.toShortNamesContinuous(Mods.getMods(request.requestedMods())), value.getOriginalArgument()));
27 | }
28 | return Optional.empty();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/AllRecommenders.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.recommendations;
2 |
3 | import java.io.IOException;
4 | import java.sql.SQLException;
5 | import java.util.Collection;
6 | import java.util.List;
7 |
8 | import javax.inject.Inject;
9 | import javax.inject.Named;
10 |
11 | import org.tillerino.ppaddict.util.MaintenanceException;
12 |
13 | import lombok.RequiredArgsConstructor;
14 | import tillerino.tillerinobot.UserException;
15 |
16 | @RequiredArgsConstructor(onConstructor = @__(@Inject))
17 | public class AllRecommenders implements Recommender {
18 | @Named("standard")
19 | private final Recommender standard;
20 |
21 | private final NamePendingApprovalRecommender nap;
22 |
23 | @Override
24 | public List loadTopPlays(int userId) throws SQLException, MaintenanceException, IOException {
25 | return standard.loadTopPlays(userId);
26 | }
27 |
28 | @Override
29 | public Collection loadRecommendations(List topPlays, Collection exclude,
30 | Model model, boolean nomod, long requestMods) throws SQLException, IOException, UserException {
31 | Recommender delegate = switch (model) {
32 | case NAP -> nap;
33 | default -> standard;
34 | };
35 |
36 | return delegate.loadRecommendations(topPlays, exclude, model, nomod, requestMods);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/BareRecommendation.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.recommendations;
2 |
3 | import org.tillerino.osuApiModel.types.BeatmapId;
4 | import org.tillerino.osuApiModel.types.BitwiseMods;
5 |
6 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
7 |
8 | /**
9 | * Recommendation as returned by the backend. Needs to be enriched before being displayed.
10 | *
11 | * @author Tillerino
12 | */
13 | @SuppressFBWarnings({"EI_EXPOSE_REP", "EI_EXPOSE_REP2"})
14 | public record BareRecommendation(
15 | @BeatmapId int beatmapId,
16 |
17 | /**
18 | * mods for this recommendation
19 | *
20 | * @return 0 for no mods, -1 for unknown mods, any other long for mods according
21 | * to {@link Mods}
22 | */
23 | @BitwiseMods long mods,
24 |
25 | long[] causes,
26 |
27 | /**
28 | * returns a guess at how much pp the player could achieve for this
29 | * recommendation
30 | *
31 | * @return null if no personal pp were calculated
32 | */
33 | Integer personalPP,
34 |
35 | /**
36 | * @return this is not normed, so the sum of all probabilities can be greater
37 | * than 1 and this must be accounted for!
38 | */
39 | double probability) {
40 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/Model.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.recommendations;
2 |
3 | import lombok.Getter;
4 | import lombok.RequiredArgsConstructor;
5 |
6 | /**
7 | * The type of recommendation model that the player has chosen.
8 | *
9 | * @author Tillerino
10 | */
11 | @RequiredArgsConstructor
12 | public enum Model {
13 | ALPHA(false),
14 | BETA(false),
15 | GAMMA8(true),
16 | GAMMA9(true),
17 | GAMMA10(true),
18 | /**
19 | * External model made by NamePendingApproval
20 | */
21 | NAP(true)
22 | ;
23 |
24 | @Getter
25 | private final boolean modsCapable;
26 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/Recommendation.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.recommendations;
2 |
3 | import lombok.RequiredArgsConstructor;
4 | import tillerino.tillerinobot.BeatmapMeta;
5 |
6 | /**
7 | * Enriched Recommendation.
8 | *
9 | * @author Tillerino
10 | */
11 | @RequiredArgsConstructor
12 | public class Recommendation {
13 | public final BeatmapMeta beatmap;
14 |
15 | public final BareRecommendation bareRecommendation;
16 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/RecommendationRequest.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.recommendations;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Collections;
5 | import java.util.List;
6 |
7 | import org.tillerino.osuApiModel.types.BitwiseMods;
8 |
9 | import lombok.Builder;
10 | import lombok.Getter;
11 | import tillerino.tillerinobot.predicates.RecommendationPredicate;
12 |
13 | @Builder
14 | public record RecommendationRequest(
15 | boolean nomod,
16 | Model model,
17 | @BitwiseMods long requestedMods,
18 | List predicates,
19 | Shift difficultyShift
20 | ) {
21 | public RecommendationRequest {
22 | predicates = new ArrayList<>(predicates);
23 | }
24 |
25 | public List predicates() {
26 | return Collections.unmodifiableList(predicates);
27 | }
28 |
29 | public static class RecommendationRequestBuilder {
30 | @Getter
31 | @BitwiseMods
32 | private long requestedMods = 0L;
33 |
34 | private List predicates = new ArrayList<>();
35 |
36 | private Shift difficultyShift = Shift.NONE;
37 |
38 | public RecommendationRequestBuilder requestedMods(@BitwiseMods long requestedMods) {
39 | this.requestedMods = requestedMods;
40 | return this;
41 | }
42 |
43 | public RecommendationRequestBuilder predicate(RecommendationPredicate predicate) {
44 | predicates.add(predicate);
45 | return this;
46 | }
47 |
48 | public Model getModel() {
49 | return model;
50 | }
51 |
52 | public List getPredicates() {
53 | return Collections.unmodifiableList(predicates);
54 | }
55 | }
56 |
57 | /**
58 | * Modifies the difficulty of recommendations.
59 | */
60 | static enum Shift {
61 | /**
62 | * Regular strength.
63 | */
64 | NONE,
65 | /**
66 | * The player is weak compared to their top scores. Recommendations are easier.
67 | */
68 | SUCC,
69 | /**
70 | * Even weaker than {@link #SUCC}
71 | */
72 | SUCCER,
73 | /**
74 | * Even weaker than {@link #SUCCERBERG}
75 | */
76 | SUCCERBERG
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/Recommender.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.recommendations;
2 |
3 | import java.io.IOException;
4 | import java.sql.SQLException;
5 | import java.util.Collection;
6 | import java.util.List;
7 |
8 | import javax.annotation.Nonnull;
9 |
10 | import org.tillerino.osuApiModel.types.BitwiseMods;
11 | import org.tillerino.osuApiModel.types.UserId;
12 | import org.tillerino.ppaddict.util.MaintenanceException;
13 |
14 | import tillerino.tillerinobot.UserException;
15 |
16 | public interface Recommender {
17 | List loadTopPlays(@UserId int userId) throws SQLException, MaintenanceException, IOException;
18 |
19 | /**
20 | * @param topPlays base for the recommendation
21 | * @param exclude these maps will be excluded (give top50 and previously given recommendations)
22 | * @param model selected model
23 | * @param nomod don't recommend mods
24 | * @param requestMods request specific mods (these will be included, but this won't exclude other mods)
25 | */
26 | public Collection loadRecommendations(List topPlays, @Nonnull Collection exclude,
27 | @Nonnull Model model, boolean nomod, @BitwiseMods long requestMods) throws SQLException, IOException, UserException;
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/recommendations/TopPlay.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.recommendations;
2 |
3 | import org.tillerino.osuApiModel.types.BeatmapId;
4 | import org.tillerino.osuApiModel.types.BitwiseMods;
5 | import org.tillerino.osuApiModel.types.UserId;
6 |
7 | import lombok.AllArgsConstructor;
8 | import lombok.Data;
9 | import lombok.NoArgsConstructor;
10 | import tillerino.tillerinobot.UserDataManager.UserData.BeatmapWithMods;
11 |
12 | @Data
13 | @NoArgsConstructor
14 | @AllArgsConstructor
15 | public class TopPlay {
16 | @UserId
17 | private int userid;
18 | private int place;
19 |
20 | @BeatmapId
21 | private int beatmapid;
22 | @BitwiseMods
23 | private long mods;
24 |
25 | private double pp;
26 |
27 | public BeatmapWithMods idAndMods() {
28 | return new BeatmapWithMods(beatmapid, mods);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/rest/AuthenticationFilter.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import java.io.IOException;
4 | import java.util.Optional;
5 |
6 | import javax.annotation.Priority;
7 | import javax.inject.Inject;
8 |
9 | import jakarta.ws.rs.NotFoundException;
10 | import jakarta.ws.rs.Priorities;
11 | import jakarta.ws.rs.WebApplicationException;
12 | import jakarta.ws.rs.container.ContainerRequestContext;
13 | import jakarta.ws.rs.container.ContainerRequestFilter;
14 | import jakarta.ws.rs.core.Response.Status;
15 |
16 | import org.apache.commons.lang3.StringUtils;
17 | import org.slf4j.MDC;
18 | import org.tillerino.ppaddict.rest.AuthenticationService;
19 | import org.tillerino.ppaddict.util.MdcUtils;
20 |
21 | import lombok.RequiredArgsConstructor;
22 |
23 | @KeyRequired
24 | @Priority(Priorities.AUTHENTICATION)
25 | @RequiredArgsConstructor(onConstructor = @__(@Inject))
26 | public class AuthenticationFilter implements ContainerRequestFilter {
27 | private final AuthenticationService authentication;
28 |
29 | @Override
30 | public void filter(ContainerRequestContext requestContext) throws IOException {
31 | String apiKey = Optional.ofNullable(requestContext.getUriInfo().getQueryParameters().get("k"))
32 | .flatMap(l -> l.stream().findFirst()).orElse(requestContext.getHeaderString("api-key"));
33 |
34 | try {
35 | if (apiKey == null) {
36 | throw new NotFoundException();
37 | }
38 | authentication.getAuthorization(apiKey);
39 | } catch (NotFoundException e) {
40 | throw new WebApplicationException("Unknown API key", Status.UNAUTHORIZED);
41 | }
42 | // abbreviate. never log credentials. keys are made to be unique in the first 8 characters.
43 | MDC.put(MdcUtils.MDC_API_KEY, StringUtils.substring(apiKey, 0, 8));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/rest/BeatmapDifficulties.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import java.util.List;
4 |
5 | import jakarta.ws.rs.DefaultValue;
6 | import jakarta.ws.rs.GET;
7 | import jakarta.ws.rs.Path;
8 | import jakarta.ws.rs.Produces;
9 | import jakarta.ws.rs.QueryParam;
10 | import jakarta.ws.rs.core.MediaType;
11 |
12 | import org.tillerino.osuApiModel.types.BeatmapId;
13 | import org.tillerino.osuApiModel.types.BitwiseMods;
14 |
15 | import tillerino.tillerinobot.rest.BeatmapInfoService.BeatmapInfo;
16 |
17 | @Path("/beatmapinfo")
18 | public interface BeatmapDifficulties {
19 | @KeyRequired
20 | @GET
21 | @Produces(MediaType.APPLICATION_JSON)
22 | BeatmapInfo getBeatmapInfo(@QueryParam("beatmapid") @BeatmapId int beatmapid,
23 | @QueryParam("mods") @BitwiseMods long mods,
24 | @QueryParam("acc") List requestedAccs,
25 | @QueryParam("wait") @DefaultValue("1000") long wait) throws Throwable;
26 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/rest/BeatmapResource.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import jakarta.ws.rs.Consumes;
4 | import jakarta.ws.rs.GET;
5 | import jakarta.ws.rs.PUT;
6 | import jakarta.ws.rs.Path;
7 | import jakarta.ws.rs.Produces;
8 | import jakarta.ws.rs.core.MediaType;
9 |
10 | import org.tillerino.osuApiModel.OsuApiBeatmap;
11 |
12 | import io.swagger.annotations.Api;
13 | import io.swagger.annotations.ApiOperation;
14 | import io.swagger.annotations.ApiResponse;
15 | import io.swagger.annotations.ApiResponses;
16 | import io.swagger.annotations.Authorization;
17 |
18 | @Path("")
19 | @Api(hidden = true)
20 | @Produces(MediaType.APPLICATION_JSON)
21 | @KeyRequired
22 | public interface BeatmapResource {
23 | @ApiOperation(value = "Get a beatmap object", tags = "public", authorizations = @Authorization("api_key"))
24 | @GET
25 | OsuApiBeatmap get();
26 |
27 | @ApiOperation(value = "Get a beatmap file", tags = "public", authorizations = @Authorization("api_key"))
28 | @Path("/file")
29 | @GET
30 | @Produces(MediaType.TEXT_PLAIN)
31 | String getFile();
32 |
33 | @ApiOperation(value = "Update a beatmap file", tags = "public", authorizations = @Authorization("api_key"))
34 | @ApiResponses({
35 | @ApiResponse(code = 403, message = "The supplied beatmap file did not have the required hash value"),
36 | @ApiResponse(code = 204, message = "Beatmap file saved")
37 | })
38 | @Path("/file")
39 | @PUT
40 | @Consumes(MediaType.TEXT_PLAIN)
41 | void setFile(String content);
42 | }
43 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/rest/BeatmapsService.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import jakarta.ws.rs.Path;
4 | import jakarta.ws.rs.PathParam;
5 |
6 | import org.tillerino.osuApiModel.types.BeatmapId;
7 |
8 | import io.swagger.annotations.Api;
9 | import io.swagger.annotations.ApiOperation;
10 |
11 | @Api
12 | @Path("/beatmaps")
13 | public interface BeatmapsService {
14 | @Path("/byId/{id}")
15 | @ApiOperation("")
16 | BeatmapResource byId(@PathParam("id") @BeatmapId int id);
17 |
18 | @Path("byHash/{hash}")
19 | @ApiOperation("")
20 | BeatmapResource byHash(@PathParam("hash") String hash);
21 | }
22 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/rest/BotApiDefinition.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import java.util.Collections;
4 | import java.util.HashSet;
5 | import java.util.List;
6 | import java.util.Set;
7 |
8 | import javax.inject.Inject;
9 |
10 | import jakarta.ws.rs.container.ContainerResponseFilter;
11 | import jakarta.ws.rs.core.Application;
12 |
13 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
14 |
15 | /**
16 | * @author Tillerino
17 | */
18 | public class BotApiDefinition extends Application {
19 | Set resourceInstances = new HashSet<>();
20 |
21 | @Inject
22 | public BotApiDefinition(BotInfoService botInfo, BeatmapInfoService beatmapInfo,
23 | UserByIdService userById, BeatmapsService beatmaps,
24 | AuthenticationFilter authentication, ApiLoggingFeature logging) {
25 | super();
26 |
27 | resourceInstances.add(botInfo);
28 | resourceInstances.add(beatmapInfo);
29 | resourceInstances.add(userById);
30 | resourceInstances.add(new DelegatingBeatmapsService(beatmaps));
31 | resourceInstances.add(authentication);
32 | resourceInstances.add(logging);
33 | resourceInstances.add((ContainerResponseFilter) (requestContext, responseContext) -> {
34 | // allow requests from github page, e.g. swagger UI.
35 | List origin = requestContext.getHeaders().getOrDefault("Origin", Collections.emptyList());
36 | if (origin.stream().allMatch(x -> x.startsWith("https://tillerino.github.io"))) {
37 | origin.forEach(o -> responseContext.getHeaders().add("Access-Control-Allow-Origin", o));
38 | responseContext.getHeaders().add("Access-Control-Allow-Headers", "api-key");
39 | }
40 | });
41 | }
42 |
43 | @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "We intentionally modify this externally. Sorry :D")
44 | @Override
45 | public Set getSingletons() {
46 | return resourceInstances;
47 | }
48 |
49 | @Override
50 | public Set> getClasses() {
51 | return Collections.singleton(PrintMessageExceptionMapper.class);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/rest/BotInfoService.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import javax.inject.Inject;
4 | import javax.inject.Singleton;
5 |
6 | import jakarta.ws.rs.NotFoundException;
7 |
8 | import org.tillerino.ppaddict.chat.GameChatClient;
9 | import org.tillerino.ppaddict.chat.GameChatClientMetrics;
10 | import org.tillerino.ppaddict.chat.local.LocalGameChatMetrics;
11 | import org.tillerino.ppaddict.util.Clock;
12 |
13 | import lombok.RequiredArgsConstructor;
14 |
15 | @Singleton
16 | @RequiredArgsConstructor(onConstructor = @__(@Inject))
17 | public class BotInfoService implements BotStatus {
18 | private final GameChatClient bot;
19 |
20 | private final LocalGameChatMetrics botInfo;
21 |
22 | private final Clock clock;
23 |
24 | @Override
25 | public LocalGameChatMetrics botinfo() {
26 | GameChatClientMetrics botMetrics = bot.getMetrics().unwrapOrElse(__ -> {
27 | GameChatClientMetrics metrics = new GameChatClientMetrics();
28 | metrics.setLastInteraction(-1); // as a marker
29 | return metrics;
30 | });
31 | LocalGameChatMetrics.Mapper.INSTANCE.loadFromBot(botMetrics, botInfo);
32 | return LocalGameChatMetrics.Mapper.INSTANCE.copy(botInfo);
33 | }
34 |
35 | /*
36 | * The following are endpoints for automated health checks, so they don't return anything
37 | * valuable other than a 200 or 404.
38 | */
39 | @Override
40 | public boolean isReceiving() {
41 | // set remotely, so we need to call botinfo()
42 | if (botinfo().getLastReceivedMessage() < clock.currentTimeMillis() - 10000) {
43 | throw new NotFoundException();
44 | }
45 | return true;
46 | }
47 |
48 | @Override
49 | public boolean isSending() {
50 | // set locally
51 | if (botInfo.getLastSentMessage() < clock.currentTimeMillis() - 60000) {
52 | throw new NotFoundException();
53 | }
54 | return true;
55 | }
56 |
57 | @Override
58 | public boolean isRecommending() {
59 | // set locally
60 | if (botInfo.getLastRecommendation() < clock.currentTimeMillis() - 60000) {
61 | throw new NotFoundException();
62 | }
63 | return true;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/rest/BotStatus.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import org.tillerino.ppaddict.chat.local.LocalGameChatMetrics;
4 |
5 | import jakarta.ws.rs.GET;
6 | import jakarta.ws.rs.Path;
7 | import jakarta.ws.rs.Produces;
8 | import jakarta.ws.rs.core.MediaType;
9 |
10 | @Path("/botinfo")
11 | public interface BotStatus {
12 | @GET
13 | @Produces(MediaType.APPLICATION_JSON)
14 | LocalGameChatMetrics botinfo();
15 |
16 | @GET
17 | @Produces(MediaType.TEXT_PLAIN)
18 | @Path("/isReceiving")
19 | boolean isReceiving();
20 |
21 | @GET
22 | @Produces(MediaType.TEXT_PLAIN)
23 | @Path("/isSending")
24 | boolean isSending();
25 |
26 | @GET
27 | @Produces(MediaType.TEXT_PLAIN)
28 | @Path("/isRecommending")
29 | boolean isRecommending();
30 | }
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/rest/DelegatingBeatmapsService.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import javax.inject.Inject;
4 |
5 | import lombok.RequiredArgsConstructor;
6 |
7 | @RequiredArgsConstructor(onConstructor = @__(@Inject))
8 | public class DelegatingBeatmapsService implements BeatmapsService {
9 | private final BeatmapsService delegate;
10 |
11 | @Override
12 | public BeatmapResource byId(int id) {
13 | return delegate.byId(id);
14 | }
15 |
16 | @Override
17 | public BeatmapResource byHash(String hash) {
18 | return delegate.byHash(hash);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/rest/KeyRequired.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import static java.lang.annotation.ElementType.METHOD;
4 | import static java.lang.annotation.ElementType.TYPE;
5 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
6 |
7 | import java.lang.annotation.Retention;
8 | import java.lang.annotation.Target;
9 |
10 | import jakarta.ws.rs.NameBinding;
11 |
12 | /**
13 | * Marks classes and methods which require a general key to be present in the API
14 | */
15 | @NameBinding
16 | @Retention(RUNTIME)
17 | @Target({ TYPE, METHOD })
18 | public @interface KeyRequired {
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/rest/PrintMessageExceptionMapper.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import jakarta.ws.rs.WebApplicationException;
4 | import jakarta.ws.rs.core.MediaType;
5 | import jakarta.ws.rs.core.Response;
6 | import jakarta.ws.rs.ext.ExceptionMapper;
7 |
8 | public class PrintMessageExceptionMapper implements ExceptionMapper {
9 | @Override
10 | public Response toResponse(WebApplicationException exception) {
11 | Response response = exception.getResponse();
12 | if (!response.hasEntity() && exception.getMessage() != null) {
13 | return Response.fromResponse(response).entity(exception.getMessage()).type(MediaType.TEXT_PLAIN).build();
14 | }
15 | return response;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/rest/RestUtils.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import java.io.IOException;
4 |
5 | import jakarta.ws.rs.WebApplicationException;
6 | import jakarta.ws.rs.core.Response;
7 | import jakarta.ws.rs.core.Response.Status;
8 |
9 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
10 |
11 | public final class RestUtils {
12 | private RestUtils() {
13 | // utils class
14 | }
15 |
16 | public static WebApplicationException getBadGateway(IOException e) {
17 | return new WebApplicationException(e != null ? e.getMessage() : "Communication with the osu API server failed.", Status.fromStatusCode(502));
18 | }
19 |
20 | public static WebApplicationException getInterrupted() {
21 | return new WebApplicationException("The server is being shutdown for maintenance", Status.SERVICE_UNAVAILABLE);
22 | }
23 |
24 | @SuppressFBWarnings(value = "SA_LOCAL_SELF_COMPARISON", justification = "Looks like a bug")
25 | public static Throwable refreshWebApplicationException(Throwable t) {
26 | if (t instanceof WebApplicationException web) {
27 | return new WebApplicationException(t.getCause(), Response.fromResponse(web.getResponse()).build());
28 | }
29 | return t;
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/rest/UserByIdService.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import java.io.IOException;
4 | import java.sql.SQLException;
5 |
6 | import javax.inject.Inject;
7 | import javax.inject.Singleton;
8 |
9 | import jakarta.ws.rs.GET;
10 | import jakarta.ws.rs.NotFoundException;
11 | import jakarta.ws.rs.Path;
12 | import jakarta.ws.rs.Produces;
13 | import jakarta.ws.rs.QueryParam;
14 | import jakarta.ws.rs.core.MediaType;
15 |
16 | import org.tillerino.osuApiModel.OsuApiUser;
17 | import org.tillerino.osuApiModel.types.UserId;
18 |
19 | import lombok.RequiredArgsConstructor;
20 | import tillerino.tillerinobot.IrcNameResolver;
21 |
22 | @Singleton
23 | @Path("/userbyid")
24 | @RequiredArgsConstructor(onConstructor = @__(@Inject))
25 | public class UserByIdService {
26 | private final IrcNameResolver resolver;
27 |
28 | @KeyRequired
29 | @GET
30 | @Produces(MediaType.APPLICATION_JSON)
31 | public OsuApiUser getUserById(@QueryParam("id") @UserId int id) throws SQLException {
32 | try {
33 | OsuApiUser user = resolver.resolveManually(id);
34 | if (user == null) {
35 | throw new NotFoundException("user with that id does not exist");
36 | } else {
37 | return user;
38 | }
39 | } catch (IOException e) {
40 | throw RestUtils.getBadGateway(null);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/java/tillerino/tillerinobot/util/IsMutable.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.util;
2 |
3 | public interface IsMutable {
4 | /**
5 | * Checks whether this object has been modified.
6 | */
7 | boolean isModified();
8 |
9 | /**
10 | * After this method has been called, calls to {@link #isModified()} will return
11 | * false
until the next modification of this object.
12 | */
13 | void clearModified();
14 | }
15 |
--------------------------------------------------------------------------------
/tillerinobot/src/main/resources/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tillerino/Tillerinobot/7c59b2557fe9be2a227ca5fb7b476d06c5c181d3/tillerinobot/src/main/resources/.gitignore
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/org/tillerino/mormon/LoaderTest.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.mormon;
2 |
3 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
4 |
5 | import javax.inject.Inject;
6 |
7 | import lombok.NoArgsConstructor;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 | import org.tillerino.ppaddict.util.InjectionRunner;
11 | import org.tillerino.ppaddict.util.TestModule;
12 |
13 | import tillerino.tillerinobot.AbstractDatabaseTest.DockeredMysqlModule;
14 |
15 | @RunWith(InjectionRunner.class)
16 | @TestModule(value = { DockeredMysqlModule.class })
17 | public class LoaderTest {
18 | @Inject
19 | DatabaseManager dbm;
20 |
21 | @Table("does_not_exist")
22 | @NoArgsConstructor
23 | static class DoesNotExist {
24 |
25 | }
26 |
27 | @Test
28 | public void wrongParameterCount() throws Exception {
29 | try(Database db = dbm.getDatabase();
30 | Loader loader = db.loader(DoesNotExist.class, "where field_name = ?")) {
31 | assertThatThrownBy(() -> loader.query(1, 2)).hasMessage("Expected 1 parameters but received 2");
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/org/tillerino/mormon/LoaderTestManual.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.mormon;
2 |
3 | import javax.inject.Inject;
4 |
5 | import org.junit.Test;
6 | import org.junit.runner.RunWith;
7 | import org.tillerino.mormon.Persister.Action;
8 | import org.tillerino.ppaddict.util.InjectionRunner;
9 | import org.tillerino.ppaddict.util.TestModule;
10 |
11 | import tillerino.tillerinobot.AbstractDatabaseTest.DockeredMysqlModule;
12 |
13 | /**
14 | * Check if streaming works.
15 | */
16 | @RunWith(InjectionRunner.class)
17 | @TestModule(value = { DockeredMysqlModule.class })
18 | public class LoaderTestManual {
19 | @Inject
20 | DatabaseManager dbm;
21 |
22 | @Table("byteArrays")
23 | public static class ByteArrays {
24 | public byte[] payload;
25 | }
26 |
27 | @Test
28 | public void testStreaming() throws Exception {
29 | Database db = dbm.getDatabase();
30 | db.connection().createStatement().execute("CREATE TABLE `byteArrays` (`payload` longblob NOT NULL)");
31 | for (int i = 0; i < 1024; i++) {
32 | ByteArrays b = new ByteArrays();
33 | b.payload = new byte[500 * 1024];
34 | db.persister(ByteArrays.class, Action.INSERT)
35 | .persist(b);
36 | System.out.println(i);
37 | }
38 | System.out.println();
39 | for (ByteArrays b : db.streamingLoader(ByteArrays.class, "").query()) {
40 | // put a breakpoint here, check memory consumption after GC
41 | System.out.println("x");
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/org/tillerino/ppaddict/chat/impl/BouncerTest.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.chat.impl;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.junit.Assert.*;
5 |
6 | import org.junit.Test;
7 | import org.tillerino.ppaddict.chat.impl.Bouncer.SemaphorePayload;
8 | import org.tillerino.ppaddict.util.Clock;
9 | import org.tillerino.ppaddict.util.TestClock;
10 |
11 | public class BouncerTest {
12 | Clock clock = new TestClock();
13 |
14 | Bouncer bouncer = new Bouncer(clock);
15 |
16 | @Test
17 | public void testEnter() throws Exception {
18 | assertTrue(bouncer.tryEnter("nick", 1));
19 | assertThat(bouncer.get("nick")).contains(new SemaphorePayload(1, 0, 0, false));
20 | }
21 |
22 | @Test
23 | public void testRepeatedDenied() throws Exception {
24 | assertTrue(bouncer.tryEnter("it's a me", 1));
25 | assertFalse(bouncer.tryEnter("it's a me", 2));
26 | assertThat(bouncer.get("it's a me").get()).hasFieldOrPropertyWithValue("attemptsSinceEntered", 1);
27 | }
28 |
29 | @Test
30 | public void testOkAfterLeaving() throws Exception {
31 | assertTrue(bouncer.tryEnter("it's a me", 1));
32 | assertTrue(bouncer.exit("it's a me", 1));
33 | assertTrue(bouncer.tryEnter("it's a me", 2));
34 | }
35 |
36 | @Test
37 | public void testFalseExit() throws Exception {
38 | assertFalse(bouncer.exit("it's a me", 1));
39 | assertTrue(bouncer.tryEnter("it's a me", 1));
40 | assertFalse(bouncer.exit("it's a me", 2));
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/org/tillerino/ppaddict/chat/local/InMemoryQueuesModule.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.chat.local;
2 |
3 | import org.tillerino.ppaddict.chat.GameChatEventQueue;
4 | import org.tillerino.ppaddict.chat.GameChatResponseQueue;
5 |
6 | import com.google.inject.AbstractModule;
7 |
8 | public class InMemoryQueuesModule extends AbstractModule {
9 | @Override
10 | protected void configure() {
11 | bind(GameChatEventQueue.class).to(LocalGameChatEventQueue.class);
12 | bind(GameChatResponseQueue.class).to(LocalGameChatResponseQueue.class);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/org/tillerino/ppaddict/chat/local/LocalGameChatEventQueue.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.chat.local;
2 |
3 | import java.util.concurrent.BlockingQueue;
4 | import java.util.concurrent.LinkedBlockingQueue;
5 |
6 | import javax.inject.Inject;
7 | import javax.inject.Singleton;
8 |
9 | import org.tillerino.ppaddict.chat.GameChatEvent;
10 | import org.tillerino.ppaddict.chat.GameChatEventQueue;
11 | import org.tillerino.ppaddict.chat.impl.MessageHandlerScheduler;
12 | import org.tillerino.ppaddict.util.LoopingRunnable;
13 | import org.tillerino.ppaddict.util.MdcUtils;
14 |
15 | import lombok.RequiredArgsConstructor;
16 |
17 | @Singleton
18 | @RequiredArgsConstructor(onConstructor = @__(@Inject))
19 | public class LocalGameChatEventQueue extends LoopingRunnable implements GameChatEventQueue {
20 | private final BlockingQueue queue = new LinkedBlockingQueue<>();
21 |
22 | private final MessageHandlerScheduler scheduler;
23 |
24 | private final LocalGameChatMetrics botInfo;
25 |
26 | @Override
27 | public void onEvent(GameChatEvent event) throws InterruptedException {
28 | event.getMeta().setMdc(MdcUtils.getSnapshot());
29 | queue.put(event);
30 | botInfo.setEventQueueSize(size());
31 | }
32 |
33 | @Override
34 | protected void loop() throws InterruptedException {
35 | scheduler.onEvent(queue.take());
36 | botInfo.setEventQueueSize(size());
37 | }
38 |
39 | @Override
40 | public int size() {
41 | return queue.size();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/org/tillerino/ppaddict/chat/local/LocalGameChatResponseQueue.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.chat.local;
2 |
3 | import java.util.concurrent.BlockingQueue;
4 | import java.util.concurrent.LinkedBlockingQueue;
5 |
6 | import javax.inject.Inject;
7 | import javax.inject.Named;
8 | import javax.inject.Singleton;
9 |
10 | import org.apache.commons.lang3.tuple.Pair;
11 | import org.tillerino.ppaddict.chat.*;
12 | import org.tillerino.ppaddict.util.LoopingRunnable;
13 | import org.tillerino.ppaddict.util.MdcUtils;
14 | import org.tillerino.ppaddict.util.MdcUtils.MdcAttributes;
15 |
16 | import lombok.RequiredArgsConstructor;
17 | import lombok.extern.slf4j.Slf4j;
18 |
19 | /**
20 | * Implements {@link GameChatResponseQueue} with a simple local, in-memory
21 | * version. If {@link #run()} is executed, the queue feeds the queued responses
22 | * synchronously downstream.
23 | */
24 | @Slf4j
25 | @Singleton
26 | @RequiredArgsConstructor(onConstructor = @__(@Inject))
27 | public class LocalGameChatResponseQueue extends LoopingRunnable implements GameChatResponseQueue {
28 | private final @Named("responsePostprocessor") GameChatResponseConsumer downstream;
29 |
30 | private final BlockingQueue> queue = new LinkedBlockingQueue<>();
31 |
32 | private final LocalGameChatMetrics botInfo;
33 |
34 | @Override
35 | public void onResponse(GameChatResponse response, GameChatEvent event) throws InterruptedException {
36 | event.getMeta().setMdc(MdcUtils.getSnapshot());
37 | queue.put(Pair.of(response, event));
38 | botInfo.setResponseQueueSize(queue.size());
39 | }
40 |
41 | @Override
42 | protected void loop() throws InterruptedException {
43 | Pair response = queue.take();
44 | botInfo.setResponseQueueSize(queue.size());
45 | try (MdcAttributes mdc = response.getRight().getMeta().getMdc().apply()) {
46 | downstream.onResponse(response.getLeft(), response.getRight());
47 | } catch (InterruptedException e) {
48 | throw e;
49 | } catch (Throwable e) {
50 | log.error("Exception while handling response", e);
51 | }
52 | }
53 |
54 | @Override
55 | public int size() {
56 | return queue.size();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/org/tillerino/ppaddict/config/DatabaseConfigServiceTest.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.config;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import javax.inject.Inject;
6 |
7 | import org.junit.Test;
8 | import org.tillerino.mormon.Persister.Action;
9 | import org.tillerino.ppaddict.util.TestModule;
10 |
11 | import tillerino.tillerinobot.AbstractDatabaseTest;
12 | import tillerino.tillerinobot.data.BotConfig;
13 |
14 | @TestModule(CachedDatabaseConfigServiceModule.class)
15 | public class DatabaseConfigServiceTest extends AbstractDatabaseTest {
16 | @Inject
17 | ConfigService config;
18 |
19 | @Test
20 | public void noConfigIsDefault() throws Exception {
21 | assertThat(config.scoresMaintenance()).isFalse();
22 | }
23 |
24 | @Test
25 | public void falsee() throws Exception {
26 | BotConfig botConfig = new BotConfig();
27 | botConfig.setPath("api-scores-maintenance");
28 | botConfig.setValue("false");
29 | db.persist(botConfig, Action.INSERT);
30 | assertThat(config.scoresMaintenance()).isFalse();
31 | }
32 |
33 | @Test
34 | public void truee() throws Exception {
35 | BotConfig botConfig = new BotConfig();
36 | botConfig.setPath("api-scores-maintenance");
37 | botConfig.setValue("true");
38 | db.persist(botConfig, Action.INSERT);
39 | assertThat(config.scoresMaintenance()).isTrue();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/org/tillerino/ppaddict/rest/AuthenticationServiceImplIT.java:
--------------------------------------------------------------------------------
1 | package org.tillerino.ppaddict.rest;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
5 |
6 | import javax.inject.Inject;
7 |
8 | import jakarta.ws.rs.NotAuthorizedException;
9 |
10 | import org.junit.Before;
11 | import org.junit.Rule;
12 | import org.junit.Test;
13 | import org.junit.runner.RunWith;
14 | import org.tillerino.MockServerRule;
15 | import org.tillerino.ppaddict.util.InjectionRunner;
16 | import org.tillerino.ppaddict.util.TestModule;
17 |
18 | @RunWith(InjectionRunner.class)
19 | @TestModule({ MockServerRule.MockServerModule.class, AuthenticationServiceImpl.RemoteAuthenticationModule.class })
20 | public class AuthenticationServiceImplIT{
21 | @Rule
22 | public MockServerRule mockServer = new MockServerRule();
23 |
24 | @Inject
25 | AuthenticationService auth;
26 |
27 | @Before
28 | public void before() {
29 | mockServer.mockJsonGet("/auth/authorization", "{ \"unknownProperty\": true }", "api-key", "regular");
30 | mockServer.mockJsonGet("/auth/authorization", "{ \"admin\": true }", "api-key", "adminKey");
31 | mockServer.mockStatusCodeGet("/auth/authorization", 401, "api-key", "garbage");
32 | }
33 |
34 | @Test
35 | public void testPositive() throws Exception {
36 | assertThat(auth.getAuthorization("regular")).hasFieldOrPropertyWithValue("admin", false);
37 | }
38 |
39 | @Test
40 | public void testNegative() throws Exception {
41 | assertThatThrownBy(() -> auth.getAuthorization("garbage")).isInstanceOf(NotAuthorizedException.class);
42 | }
43 |
44 | @Test
45 | public void testAdmin() throws Exception {
46 | assertThat(auth.getAuthorization("adminKey")).hasFieldOrPropertyWithValue("admin", true);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/AbstractDatabaseTest.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot;
2 |
3 | import static tillerino.tillerinobot.MysqlContainer.mysql;
4 |
5 | import java.sql.SQLException;
6 | import java.util.Properties;
7 |
8 | import javax.inject.Inject;
9 | import javax.inject.Named;
10 |
11 | import org.junit.After;
12 | import org.junit.Before;
13 | import org.junit.Rule;
14 | import org.junit.rules.TestRule;
15 | import org.junit.runner.RunWith;
16 | import org.tillerino.mormon.Database;
17 | import org.tillerino.mormon.DatabaseManager;
18 | import org.tillerino.ppaddict.util.InjectionRunner;
19 | import org.tillerino.ppaddict.util.TestModule;
20 |
21 | import com.google.inject.AbstractModule;
22 | import com.google.inject.Provides;
23 |
24 | import tillerino.tillerinobot.MysqlContainer.MysqlDatabaseLifecycle;
25 |
26 | /**
27 | * Creates a MySQL instance in running in Docker.
28 | */
29 | @TestModule(AbstractDatabaseTest.DockeredMysqlModule.class)
30 | @RunWith(InjectionRunner.class)
31 | public abstract class AbstractDatabaseTest {
32 | public static class DockeredMysqlModule extends AbstractModule {
33 | @Provides
34 | @Named("mysql")
35 | Properties myqslProperties() {
36 | Properties props = new Properties();
37 | props.put("host", mysql().getHost());
38 | props.put("port", "" + mysql().getMappedPort(3306));
39 | props.put("user", mysql().getUsername());
40 | props.put("password", mysql().getPassword());
41 | props.put("database", mysql().getDatabaseName());
42 | return props;
43 | }
44 | }
45 |
46 | @Rule
47 | public TestRule resetMysql = new MysqlDatabaseLifecycle();
48 |
49 | @Inject
50 | protected DatabaseManager dbm;
51 | protected Database db;
52 |
53 | @Before
54 | public void createEntityManager() {
55 | db = dbm.getDatabase();
56 | }
57 |
58 | @After
59 | public void closeEntityManager() throws SQLException {
60 | db.close();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/CommandHandlerTest.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot;
2 | import static org.junit.Assert.assertEquals;
3 | import static org.junit.Assert.assertFalse;
4 | import static org.junit.Assert.assertNull;
5 | import static org.junit.Assert.assertTrue;
6 |
7 | import org.junit.Before;
8 | import org.junit.Test;
9 | import org.tillerino.ppaddict.chat.GameChatResponse;
10 |
11 | import tillerino.tillerinobot.UserDataManager.UserData;
12 | import tillerino.tillerinobot.lang.Default;
13 | import tillerino.tillerinobot.lang.LanguageIdentifier;
14 |
15 | public class CommandHandlerTest {
16 | UserData userData = new UserData();
17 |
18 | boolean called_b, called_c;
19 |
20 | @Before
21 | public void setUp() {
22 | userData.setLanguage(LanguageIdentifier.Default);
23 | }
24 |
25 | CommandHandler handler = CommandHandler.handling(
26 | "A ",
27 | CommandHandler.alwaysHandling("B", (a, c, d, lang) -> { called_b = true; return GameChatResponse.none(); })
28 | .or(CommandHandler.alwaysHandling("C",
29 | (a, c, d, lang) -> { called_c = true; return GameChatResponse.none(); })));
30 |
31 | @Test
32 | public void testNestedChoices() throws Exception {
33 | assertEquals("A (B|C)", handler.getChoices());
34 | }
35 |
36 | @Test
37 | public void testPass() throws Exception {
38 | assertNull(handler.handle("X", null, null, null));
39 | }
40 |
41 | @Test(expected = UserException.class)
42 | public void testNoNestedChoice() throws Exception {
43 | handler.handle("A X", null, userData, new Default());
44 | }
45 |
46 | @Test
47 | public void testB() throws Exception {
48 | handler.handle("A B", null, userData, null);
49 | assertTrue(called_b);
50 | assertFalse(called_c);
51 | }
52 |
53 | @Test
54 | public void testC() throws Exception {
55 | handler.handle("A C", null, userData, null);
56 | assertTrue(called_c);
57 | assertFalse(called_b);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/FakeAuthenticationService.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot;
2 |
3 | import java.util.UUID;
4 |
5 | import jakarta.ws.rs.ForbiddenException;
6 | import jakarta.ws.rs.NotFoundException;
7 |
8 | import org.tillerino.ppaddict.rest.AuthenticationService;
9 |
10 | public class FakeAuthenticationService implements AuthenticationService {
11 | @Override
12 | public Authorization getAuthorization(String key) throws NotFoundException {
13 | if (key.equals("testKey") || key.equals("valid-key")) {
14 | return new Authorization(false);
15 | }
16 | throw new NotFoundException();
17 | }
18 |
19 | @Override
20 | public String createKey(String adminKey, int osuUserId) throws NotFoundException, ForbiddenException {
21 | return UUID.randomUUID().toString(); // not quite the usual format, but meh
22 | }
23 | }
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/IrcNameResolverTest.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.junit.Assert.assertNotNull;
5 | import static org.junit.Assert.assertNull;
6 |
7 | import javax.inject.Inject;
8 |
9 | import org.junit.Test;
10 | import org.tillerino.ppaddict.util.TestModule;
11 |
12 | import tillerino.tillerinobot.data.UserNameMapping;
13 |
14 | @TestModule(value = TestBackend.Module.class, cache = false)
15 | public class IrcNameResolverTest extends AbstractDatabaseTest {
16 | @Inject
17 | TestBackend backend;
18 |
19 | @Inject
20 | IrcNameResolver resolver;
21 |
22 | @Test
23 | public void testBasic() throws Exception {
24 | assertNull(resolver.resolveIRCName("anybody"));
25 |
26 | db.truncate(UserNameMapping.class);
27 | backend.hintUser("anybody", false, 1000, 1000);
28 | assertNotNull(resolver.resolveIRCName("anybody"));
29 |
30 | assertThat(db.selectUnique(UserNameMapping.class)."where userName = \{"anybody"}")
31 | .hasValueSatisfying(m -> assertThat(m.getUserid()).isEqualTo(1));
32 | }
33 |
34 | @Test
35 | public void testFix() throws Exception {
36 | backend.hintUser("this_underscore space_bullshit", false, 1000, 1000);
37 | assertNull(resolver.resolveIRCName("this_underscore_space_bullshit"));
38 | resolver.resolveManually(backend.downloadUser("this_underscore space_bullshit").getUserId());
39 | assertNotNull(resolver.resolveIRCName("this_underscore_space_bullshit"));
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/UserDataManagerTest.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.junit.Assert.assertFalse;
5 | import static org.junit.Assert.assertTrue;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | import javax.inject.Inject;
11 |
12 | import org.junit.Test;
13 | import org.tillerino.ppaddict.util.TestModule;
14 |
15 | import tillerino.tillerinobot.UserDataManager.UserData;
16 | import tillerino.tillerinobot.UserDataManager.UserData.BeatmapWithMods;
17 |
18 | @TestModule(TestBackend.Module.class)
19 | public class UserDataManagerTest extends AbstractDatabaseTest {
20 | @Inject
21 | UserDataManager manager;
22 |
23 | @Test
24 | public void testSaveLoad() throws Exception {
25 | UserData data = manager.loadUserData(534678);
26 | assertFalse(data.isAllowedToDebug());
27 | data.setAllowedToDebug(true);
28 | data.setLastSongInfo(new BeatmapWithMods(123, 456));
29 | data.close();
30 |
31 | reloadManager();
32 |
33 | data = manager.loadUserData(534678);
34 | assertTrue(data.isAllowedToDebug());
35 | assertThat(data.getLastSongInfo()).hasFieldOrPropertyWithValue("beatmap", 123);
36 | }
37 |
38 | private void reloadManager() {
39 | manager = new UserDataManager(null, dbm);
40 | }
41 |
42 | @Test
43 | public void testLanguageMutability() throws Exception {
44 | UserDataManager manager = new UserDataManager(null, dbm);
45 | List answers = new ArrayList<>();
46 | try(UserData data = manager.loadUserData(534678)) {
47 | data.usingLanguage(language -> {
48 | answers.add(language.apiTimeoutException());
49 | for (;;) {
50 | String answer = language.apiTimeoutException();
51 | if (answer.equals(answers.get(0))) {
52 | assertThat(answers).size().as("number of responses to API timeout").isGreaterThan(1);
53 | break;
54 | }
55 | answers.add(answer);
56 | }
57 | return null;
58 | });
59 | }
60 |
61 | // at this point we got the first answer again. Time go serialize, deserialize and check if we get the second answer next.
62 | reloadManager();
63 | try(UserData data = manager.loadUserData(534678)) {
64 | data.usingLanguage(lang -> {
65 | assertThat(lang.apiTimeoutException()).as("API timeout message after reload").isEqualTo(answers.get(1));
66 | return null;
67 | });
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/data/ApiBeatmapTest.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.data;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.junit.Assert.*;
5 | import static org.mockito.Mockito.spy;
6 |
7 | import org.junit.Test;
8 | import org.tillerino.mormon.Persister.Action;
9 | import org.tillerino.osuApiModel.Downloader;
10 | import tillerino.tillerinobot.AbstractDatabaseTest;
11 |
12 | public class ApiBeatmapTest extends AbstractDatabaseTest {
13 |
14 | protected static Downloader downloader = spy(Downloader.createTestDownloader(AbstractDatabaseTest.class));
15 |
16 | @Test
17 | public void testSchema() throws Exception {
18 | assertNotNull(ApiBeatmap.loadOrDownload(db, 53, 0L, 0, downloader));
19 | }
20 |
21 | @Test
22 | public void testStoring() throws Exception {
23 | ApiBeatmap original = newApiBeatmap();
24 | db.persister(ApiBeatmap.class, Action.INSERT).persist(original);
25 | assertThat(db.loader(ApiBeatmap.class, "").queryUnique()).hasValueSatisfying(original::equals);
26 | }
27 |
28 | public static ApiBeatmap newApiBeatmap() {
29 | ApiBeatmap apiBeatmap = new ApiBeatmap();
30 | apiBeatmap.setArtist("no artist");
31 | apiBeatmap.setTitle("no title");
32 | apiBeatmap.setVersion("no version");
33 | apiBeatmap.setCreator("no creator");
34 | apiBeatmap.setSource("no source");
35 | apiBeatmap.setFileMd5("no md5");
36 | return apiBeatmap;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/handlers/DebugHandlerTest.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers;
2 |
3 | import static org.junit.Assert.assertNotNull;
4 |
5 | import org.junit.Before;
6 | import org.junit.Test;
7 | import org.mockito.Mock;
8 | import org.mockito.MockitoAnnotations;
9 |
10 | import tillerino.tillerinobot.BotBackend;
11 | import tillerino.tillerinobot.IrcNameResolver;
12 | import tillerino.tillerinobot.UserDataManager.UserData;
13 |
14 | public class DebugHandlerTest {
15 | @Mock
16 | BotBackend backend;
17 |
18 | @Mock
19 | IrcNameResolver resolver;
20 |
21 | DebugHandler handler;
22 |
23 | UserData userData = new UserData();
24 |
25 | @Before
26 | public void initMocks() {
27 | MockitoAnnotations.initMocks(this);
28 |
29 | handler = new DebugHandler(backend, resolver);
30 | userData.setAllowedToDebug(true);
31 | }
32 |
33 | @Test
34 | public void testIfHandles() throws Exception {
35 | assertNotNull(handler.handle("debug resolve bla", null, userData, null));
36 | assertNotNull(handler.handle("debug getUserByIdFresh 1", null, userData, null));
37 | assertNotNull(handler.handle("debug getUserByIdCached 1", null, userData, null));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/handlers/RecommendHandlerTest.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.handlers;
2 |
3 | import static org.mockito.Mockito.any;
4 | import static org.mockito.Mockito.eq;
5 | import static org.mockito.Mockito.mock;
6 | import static org.mockito.Mockito.verify;
7 | import static org.mockito.Mockito.when;
8 |
9 | import org.junit.Test;
10 | import org.tillerino.osuApiModel.OsuApiBeatmap;
11 | import org.tillerino.osuApiModel.OsuApiUser;
12 | import org.tillerino.ppaddict.chat.LiveActivity;
13 |
14 | import tillerino.tillerinobot.BeatmapMeta;
15 | import tillerino.tillerinobot.UserDataManager.UserData;
16 | import tillerino.tillerinobot.diff.PercentageEstimates;
17 | import tillerino.tillerinobot.lang.Default;
18 | import tillerino.tillerinobot.recommendations.BareRecommendation;
19 | import tillerino.tillerinobot.recommendations.Recommendation;
20 | import tillerino.tillerinobot.recommendations.RecommendationsManager;
21 |
22 | public class RecommendHandlerTest {
23 | @Test
24 | public void testDefaultSettings() throws Exception {
25 | RecommendationsManager manager = mock(RecommendationsManager.class);
26 | OsuApiBeatmap beatmap = new OsuApiBeatmap();
27 | beatmap.setMaxCombo(100);
28 | when(manager.getRecommendation(any(), any(), any())).thenReturn(
29 | new Recommendation(new BeatmapMeta(beatmap, null, mock(PercentageEstimates.class)), new BareRecommendation(0, 0, null, null, 0)));
30 | UserData userData = mock(UserData.class);
31 |
32 | when(userData.getDefaultRecommendationOptions()).thenReturn("dt");
33 | new RecommendHandler(manager, mock(LiveActivity.class)).handle("r", mock(OsuApiUser.class), userData, new Default());
34 | verify(manager).getRecommendation(any(), eq("dt"), any());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/lang/StringShufflerTest.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.lang;
2 |
3 | import static org.junit.Assert.*;
4 |
5 | import java.util.HashMap;
6 | import java.util.Map;
7 | import java.util.Random;
8 |
9 | import org.junit.Test;
10 |
11 |
12 | public class StringShufflerTest {
13 | @Test
14 | public void testShuffling() {
15 | Random rnd = new Random();
16 |
17 | StringShuffler shuffler = new StringShuffler(rnd);
18 |
19 | Map map = new HashMap();
20 |
21 | for(int i = 1; i <= 100; i++) {
22 | String[] strings = { "a", "b", "c", "d", "e" };
23 |
24 | for (int j = 0; j < strings.length; j++) {
25 | String s = shuffler.get(strings);
26 |
27 | Integer x = map.get(s);
28 | if(x == null) {
29 | x = 0;
30 | }
31 |
32 | x++;
33 |
34 | map.put(s, x);
35 | }
36 |
37 | for (Integer count : map.values()) {
38 | assertEquals(i, (int) count);
39 | }
40 | }
41 | }
42 |
43 | @Test
44 | public void testInTsundere() {
45 | TsundereEnglish tsundere = new TsundereEnglish();
46 |
47 | Map map = new HashMap();
48 |
49 | for(int i = 1; i <= 100; i++) {
50 | for (int j = 0; j < 3; j++) {
51 | String s = tsundere.unknownBeatmap();
52 |
53 | Integer x = map.get(s);
54 | if(x == null) {
55 | x = 0;
56 | }
57 |
58 | x++;
59 |
60 | map.put(s, x);
61 | }
62 |
63 | for (Integer count : map.values()) {
64 | assertEquals(i, (int) count);
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/lang/TsundereTest.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.lang;
2 |
3 | import static org.junit.Assert.fail;
4 | import static org.mockito.ArgumentMatchers.anyString;
5 | import static org.mockito.Mockito.mock;
6 | import static org.mockito.Mockito.spy;
7 | import static org.mockito.Mockito.times;
8 | import static org.mockito.Mockito.verify;
9 |
10 | import org.junit.Test;
11 | import org.tillerino.osuApiModel.OsuApiUser;
12 | import org.tillerino.ppaddict.chat.LiveActivity;
13 |
14 | import tillerino.tillerinobot.BotBackend;
15 | import tillerino.tillerinobot.TestBackend;
16 | import tillerino.tillerinobot.UserException;
17 | import tillerino.tillerinobot.handlers.RecommendHandler;
18 | import tillerino.tillerinobot.recommendations.RecommendationRequestParser;
19 | import tillerino.tillerinobot.recommendations.RecommendationsManager;
20 | import tillerino.tillerinobot.recommendations.Recommender;
21 |
22 | public class TsundereTest {
23 |
24 | @Test
25 | public void testInvalidChoice() throws Exception {
26 | // spy on a fresh tsundere object
27 | TsundereEnglish tsundere = spy(new TsundereEnglish());
28 |
29 | // mock backend and create RecommendationsManager and RecommendHandler based on mocked backend
30 | BotBackend backend = mock(BotBackend.class);
31 | RecommendHandler handler = new RecommendHandler(new RecommendationsManager(backend, null,
32 | new RecommendationRequestParser(backend),
33 | new TestBackend.TestBeatmapsLoader(), mock(Recommender.class)),
34 | mock(LiveActivity.class));
35 |
36 | // make a bullshit call to the handler four times
37 | for (int i = 0; i < 4; i++) {
38 | try {
39 | handler.handle("r bullshit", mock(OsuApiUser.class), null, tsundere);
40 | // we should not get this far because we're expecting an exception
41 | fail("there should be an exception");
42 | } catch (UserException e) {
43 | // good, we're expecting this
44 | }
45 | }
46 |
47 | // invalid choice should have been called all four times
48 | verify(tsundere, times(4)).invalidChoice(anyString(), anyString());
49 | // three of those times, unknownRecommendationParameter should have been called as well
50 | verify(tsundere, times(3)).unknownRecommendationParameter();
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/osutrack/TestOsutrackDownloader.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.osutrack;
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException;
4 | import com.fasterxml.jackson.databind.DeserializationFeature;
5 | import com.fasterxml.jackson.databind.ObjectMapper;
6 |
7 | import java.io.BufferedReader;
8 | import java.io.InputStream;
9 | import java.io.InputStreamReader;
10 | import java.util.stream.Collectors;
11 |
12 | import jakarta.ws.rs.NotFoundException;
13 |
14 | public class TestOsutrackDownloader extends OsutrackDownloader {
15 | static final ObjectMapper JACKSON = new ObjectMapper()
16 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
17 |
18 | protected UpdateResult parseJson(String json) {
19 | UpdateResult updateResult;
20 | try {
21 | updateResult = JACKSON.readValue(json, UpdateResult.class);
22 | } catch (JsonProcessingException e) {
23 | throw new RuntimeException(e);
24 | }
25 | completeUpdateObject(updateResult);
26 | return updateResult;
27 | }
28 |
29 |
30 | @Override
31 | public UpdateResult getUpdate(int osuUserId) {
32 | InputStream inputStream = TestOsutrackDownloader.class.getResourceAsStream("/osutrack/" + osuUserId + ".json");
33 | if (inputStream == null) {
34 | throw new NotFoundException();
35 | }
36 | String json = new BufferedReader(new InputStreamReader(inputStream)).lines()
37 | .collect(Collectors.joining("\n"));
38 | return parseJson(json);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/predicates/PredicateParserTest.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import static org.junit.Assert.*;
4 |
5 | import org.junit.Test;
6 | import org.tillerino.osuApiModel.Mods;
7 |
8 |
9 | public class PredicateParserTest {
10 | PredicateParser parser = new PredicateParser();
11 |
12 | @Test
13 | public void testApproachRate() throws Exception {
14 | RecommendationPredicate predicate = parser.tryParse("AR=9", null);
15 |
16 | assertEquals(new NumericPropertyPredicate<>(
17 | "AR=9", new ApproachRate(), 9, true, 9, true), predicate);
18 | }
19 |
20 | @Test
21 | public void testOverallDifficulty() throws Exception {
22 | RecommendationPredicate predicate = parser.tryParse("OD=9", null);
23 |
24 | assertEquals(new NumericPropertyPredicate<>(
25 | "OD=9", new OverallDifficulty(), 9, true, 9, true), predicate);
26 | }
27 |
28 | @Test
29 | public void testBPM() throws Exception {
30 | RecommendationPredicate predicate = parser.tryParse("BPM>=9000", null);
31 |
32 | assertEquals(new NumericPropertyPredicate<>(
33 | "BPM>=9000", new BeatsPerMinute(), 9000, true,
34 | Double.POSITIVE_INFINITY, true),
35 | predicate);
36 | }
37 |
38 | @Test
39 | public void testExcludeMods() throws Exception {
40 | RecommendationPredicate predicate = parser.tryParse("-hr", null);
41 |
42 | assertEquals(new ExcludeMod(Mods.HardRock),
43 | predicate);
44 | }
45 |
46 | @Test
47 | public void testUnknown() throws Exception {
48 | assertNull(parser.tryParse("yourMom", null));
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/predicates/TitleLength.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.predicates;
2 |
3 | import lombok.EqualsAndHashCode;
4 |
5 | import org.tillerino.osuApiModel.OsuApiBeatmap;
6 |
7 | @EqualsAndHashCode
8 | public class TitleLength implements NumericBeatmapProperty {
9 | @Override
10 | public String getName() {
11 | return "TL";
12 | }
13 |
14 | @Override
15 | public double getValue(OsuApiBeatmap beatmap, long mods) {
16 | return beatmap.getTitle().length() * (mods != 0l ? 2 : 1);
17 | }
18 | }
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/recommendations/NamePendingApprovalRecommenderTest.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.recommendations;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import java.net.URI;
6 | import java.util.Collection;
7 | import java.util.List;
8 |
9 | import org.junit.Rule;
10 | import org.junit.Test;
11 | import org.mockserver.model.HttpRequest;
12 | import org.mockserver.model.HttpResponse;
13 | import org.mockserver.model.JsonBody;
14 | import org.mockserver.model.MediaType;
15 | import org.tillerino.MockServerRule;
16 |
17 | public class NamePendingApprovalRecommenderTest {
18 | @Rule
19 | public final MockServerRule mockServer = new MockServerRule();
20 |
21 | @Test
22 | public void getRecommendations() throws Exception {
23 | MockServerRule.mockServer().when(HttpRequest.request("/recommend")
24 | .withContentType(MediaType.APPLICATION_JSON)
25 | .withHeader("Authorization", "Bearer my-token")
26 | .withHeader("User-Agent", "https://github.com/Tillerino/Tillerinobot")
27 | .withBody(new JsonBody("""
28 | {
29 | "topPlays" : [ {
30 | "beatmapid" : 116128,
31 | "mods" : 0,
32 | "pp" : 240.588
33 | } ],
34 | "exclude" : [ 456, 116128 ],
35 | "nomod" : true,
36 | "requestMods" : 64
37 | }""")))
38 | .respond(HttpResponse.response("""
39 | {
40 | "recommendations": [ {
41 | "beatmapId": 2785705,
42 | "mods": 8,
43 | "pp": 221,
44 | "probability": 0.001763579140327404
45 | } ]
46 | }
47 | """));
48 |
49 | NamePendingApprovalRecommender recommender = new NamePendingApprovalRecommender(
50 | URI.create(MockServerRule.getExternalMockServerAddress() + "/recommend"), "my-token");
51 | List topPlays = List.of(new TopPlay(0, 0, 116128, 0, 240.588));
52 | Collection exclusions = List.of(456);
53 | Collection loadRecommendations = recommender.loadRecommendations(topPlays, exclusions, Model.NAP, true, 64);
54 | assertThat(loadRecommendations).singleElement().satisfies(recommendation -> assertThat(recommendation)
55 | .hasFieldOrPropertyWithValue("beatmapId", 2785705)
56 | .hasFieldOrPropertyWithValue("mods", 8L)
57 | .hasFieldOrPropertyWithValue("personalPP", 221)
58 | .hasFieldOrPropertyWithValue("probability", 0.001763579140327404));
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/rest/BeatmapInfoServiceIT.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import static io.restassured.RestAssured.given;
4 | import static org.hamcrest.CoreMatchers.is;
5 |
6 | import javax.inject.Inject;
7 |
8 | import org.junit.Rule;
9 | import org.junit.Test;
10 | import org.tillerino.MockServerRule;
11 | import org.tillerino.MockServerRule.MockServerModule;
12 | import org.tillerino.ppaddict.chat.GameChatClient;
13 | import org.tillerino.ppaddict.rest.AuthenticationServiceImpl.RemoteAuthenticationModule;
14 | import org.tillerino.ppaddict.util.TestClock;
15 | import org.tillerino.ppaddict.util.TestModule;
16 |
17 | import tillerino.tillerinobot.AbstractDatabaseTest;
18 | import tillerino.tillerinobot.TestBackend;
19 |
20 | @TestModule(value = { RemoteAuthenticationModule.class, MockServerModule.class, TestClock.Module.class,
21 | TestBackend.Module.class }, mocks = { GameChatClient.class, BeatmapsService.class })
22 | public class BeatmapInfoServiceIT extends AbstractDatabaseTest {
23 | @Inject
24 | @Rule
25 | public BotApiRule botApi;
26 |
27 | @Rule
28 | public MockServerRule mockServer = new MockServerRule();
29 |
30 | @Test
31 | public void testRegular() throws Exception {
32 | mockServer.mockJsonGet("/auth/authorization", "{ }", "api-key", "valid-key");
33 |
34 | given().header("api-key", "valid-key")
35 | .get("/beatmapinfo?wait=2000&beatmapid=129891&mods=0")
36 | .then()
37 | .body("beatmapid", is(129891));
38 | }
39 |
40 | @Test
41 | public void testCors() throws Exception {
42 | given()
43 | .header("Origin", "https://tillerino.github.io")
44 | .options("/botinfo")
45 | .then()
46 | .assertThat()
47 | .statusCode(200)
48 | .header("Access-Control-Allow-Origin", "https://tillerino.github.io")
49 | .header("Access-Control-Allow-Headers", "api-key");
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/rest/BotApiRule.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import javax.inject.Inject;
4 |
5 | import io.restassured.RestAssured;
6 |
7 | public class BotApiRule extends JdkServerResource {
8 | @Inject
9 | public BotApiRule(BotApiDefinition app) {
10 | super(app, "localhost", 0);
11 | }
12 |
13 | @Override
14 | protected void before() throws Throwable {
15 | super.before();
16 | RestAssured.baseURI = "http://localhost:" + getPort();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/java/tillerino/tillerinobot/rest/JdkServerResource.java:
--------------------------------------------------------------------------------
1 | package tillerino.tillerinobot.rest;
2 |
3 | import java.net.URI;
4 |
5 | import jakarta.ws.rs.core.Application;
6 |
7 | import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory;
8 | import org.glassfish.jersey.server.ResourceConfig;
9 | import org.junit.rules.ExternalResource;
10 |
11 | import com.sun.net.httpserver.HttpServer;
12 |
13 | import lombok.RequiredArgsConstructor;
14 | import lombok.extern.slf4j.Slf4j;
15 |
16 | @Slf4j
17 | @RequiredArgsConstructor
18 | public class JdkServerResource extends ExternalResource {
19 | private final Application app;
20 |
21 | private final String host;
22 |
23 | private final int port;
24 |
25 | private int actualPort = 0;
26 |
27 | private HttpServer server;
28 |
29 | @Override
30 | protected void before() throws Throwable {
31 | server = JdkHttpServerFactory.createHttpServer(new URI("http", null, host, port, "/", null, null),
32 | ResourceConfig.forApplication(app));
33 | actualPort = server.getAddress().getPort();
34 | }
35 |
36 | @Override
37 | protected void after() {
38 | try {
39 | server.stop(1);
40 | } catch (Exception e) {
41 | log.error("Stopping Jetty failed", e);
42 | }
43 | }
44 |
45 | public int getPort() {
46 | return actualPort;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/resources/MOSAIC.WAV - Magical Pants (Short Ver.) (Imaginative) [look at bg].osu:
--------------------------------------------------------------------------------
1 | osu file format v14
2 |
3 | [General]
4 | AudioFilename: audio.mp3
5 | AudioLeadIn: 0
6 | PreviewTime: 62695
7 | Countdown: 0
8 | SampleSet: Normal
9 | StackLeniency: 0.7
10 | Mode: 0
11 | LetterboxInBreaks: 0
12 | WidescreenStoryboard: 1
13 |
14 | [Editor]
15 | Bookmarks: 3524,16156,18682,28787,38892,48998,54050,61629,72998,91945,96998,99524
16 | DistanceSpacing: 1.1
17 | BeatDivisor: 8
18 | GridSize: 8
19 | TimelineZoom: 2.2
20 |
21 | [Metadata]
22 | Title:Magical Pants (Short Ver.)
23 | TitleUnicode:まじかるパンツ (Short Ver.)
24 | Artist:MOSAIC.WAV
25 | ArtistUnicode:モザイクウェブ
26 | Creator:imaginative
27 | Version:look at bg
28 | Source:
29 | Tags:
30 | BeatmapID:2467738
31 | BeatmapSetID:1183754
32 |
33 | [Difficulty]
34 | HPDrainRate:5
35 | CircleSize:5
36 | OverallDifficulty:5
37 | ApproachRate:5
38 | SliderMultiplier:1.4
39 | SliderTickRate:1
40 |
41 | [Events]
42 | //Background and Video events
43 | 0,0,"flipped pantsu!.jpg",0,0
44 | //Break Periods
45 | //Storyboard Layer 0 (Background)
46 | //Storyboard Layer 1 (Fail)
47 | //Storyboard Layer 2 (Pass)
48 | //Storyboard Layer 3 (Foreground)
49 | //Storyboard Layer 4 (Overlay)
50 | //Storyboard Sound Samples
51 |
52 | [TimingPoints]
53 | -315,315.789473684211,4,1,0,100,1,0
54 | 62842,-100,4,1,0,100,0,1
55 | 72632,-100,4,1,0,100,0,0
56 | 72948,-100,4,1,0,100,0,1
57 | 83053,-100,4,1,0,100,0,1
58 | 84316,-100,4,1,0,100,0,0
59 |
60 |
61 | [HitObjects]
62 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/resources/osutrack/2345.json:
--------------------------------------------------------------------------------
1 | {
2 | "username": "has space",
3 | "mode": 0,
4 | "playcount": 0,
5 | "pp_rank": 0,
6 | "pp_raw": 0,
7 | "accuracy": 0,
8 | "total_score": 0,
9 | "ranked_score": 0,
10 | "count300": 0,
11 | "count50": 0,
12 | "count100": 0,
13 | "level": 0,
14 | "count_rank_a": 0,
15 | "count_rank_s": 0,
16 | "count_rank_ss": 0,
17 | "levelup": false,
18 | "first": false,
19 | "exists": true,
20 | "newhs": []
21 | }
22 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/resources/osutrack/2756335.json:
--------------------------------------------------------------------------------
1 | {
2 | "username": "oliebol",
3 | "mode": 0,
4 | "playcount": 0,
5 | "pp_rank": 0,
6 | "pp_raw": 0,
7 | "accuracy": 0,
8 | "total_score": 0,
9 | "ranked_score": 0,
10 | "count300": 0,
11 | "count50": 0,
12 | "count100": 0,
13 | "level": 0,
14 | "count_rank_a": 0,
15 | "count_rank_s": 0,
16 | "count_rank_ss": 0,
17 | "levelup": false,
18 | "first": false,
19 | "exists": true,
20 | "newhs": []
21 | }
22 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/resources/osutrack/56917.json:
--------------------------------------------------------------------------------
1 | {
2 | "username": "fartownik",
3 | "mode": "0",
4 | "playcount": 1568,
5 | "pp_rank": 3,
6 | "pp_raw": 26.25,
7 | "accuracy": 0.0062103271484375,
8 | "total_score": 3383072599,
9 | "ranked_score": 384014313,
10 | "count300": 285078,
11 | "count50": 899,
12 | "count100": 10886,
13 | "level": 0.033999999999992,
14 | "count_rank_a": 10,
15 | "count_rank_s": 8,
16 | "count_rank_ss": 1,
17 | "levelup": false,
18 | "first": false,
19 | "exists": true,
20 | "newhs": [
21 | {
22 | "beatmap_id": "768986",
23 | "score": "33506036",
24 | "maxcombo": "1161",
25 | "count50": "0",
26 | "count100": "10",
27 | "count300": "852",
28 | "countmiss": "0",
29 | "countkatu": "9",
30 | "countgeki": "214",
31 | "perfect": "1",
32 | "enabled_mods": "24",
33 | "user_id": "56917",
34 | "date": "2016-12-25 07:26:27",
35 | "rank": "SH",
36 | "pp": "414.058",
37 | "ranking": 6
38 | },
39 | {
40 | "beatmap_id": "693195",
41 | "score": "24034117",
42 | "maxcombo": "931",
43 | "count50": "1",
44 | "count100": "7",
45 | "count300": "964",
46 | "countmiss": "3",
47 | "countkatu": "6",
48 | "countgeki": "147",
49 | "perfect": "0",
50 | "enabled_mods": "24",
51 | "user_id": "56917",
52 | "date": "2016-12-17 04:50:25",
53 | "rank": "A",
54 | "pp": "331.885",
55 | "ranking": 88
56 | }
57 | ]
58 | }
59 |
--------------------------------------------------------------------------------
/tillerinobot/src/test/resources/osuv1api/api/get_beatmaps%3Fb%3D53%26mods%3D0:
--------------------------------------------------------------------------------
1 | [{"beatmapset_id":"3","beatmap_id":"53","approved":"1","total_length":"83","hit_length":"77","version":"-Crusin-","file_md5":"1d23c37a2fda439be752ae2bca06c0cd","diff_size":"5","diff_overall":"4","diff_approach":"4","diff_drain":"3","mode":"0","count_normal":"67","count_slider":"15","count_spinner":"1","submit_date":"2007-10-06 19:32:02","approved_date":"2007-10-06 19:32:02","last_update":"2007-10-06 19:32:02","artist":"Ni-Ni","title":"1,2,3,4, 007 [Wipeout Series]","creator":"MCXD","creator_id":"141","bpm":"172","source":"","tags":"","genre_id":"2","language_id":"2","favourite_count":"121","rating":"7.7178","download_unavailable":"0","audio_unavailable":"0","playcount":"88254","passcount":"42585","max_combo":"124","diff_aim":"1.0225720405578613","diff_speed":"0.6706022620201111","difficultyrating":"1.8691591024398804",
2 | "_comment": "we don't add star and aim difficulty here to test our old algorithm"
3 | }]
--------------------------------------------------------------------------------