diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 00226766..7e8ac9a8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -62,16 +62,16 @@ updates: timezone: "Europe/Paris" pull-request-branch-name: separator: "-" - - package-ecosystem: "npm" - directory: "/" - assignees: - - "Rakambda" - schedule: - interval: "weekly" - time: "05:00" - timezone: "Europe/Paris" - pull-request-branch-name: - separator: "-" +# - package-ecosystem: "npm" +# directory: "/" +# assignees: +# - "Rakambda" +# schedule: +# interval: "weekly" +# time: "05:00" +# timezone: "Europe/Paris" +# pull-request-branch-name: +# separator: "-" - package-ecosystem: "nuget" directory: "/" assignees: diff --git a/.github/workflows/build-test-deploy.yml b/.github/workflows/build-test-deploy.yml index 925c7fe2..b3b6e2a4 100644 --- a/.github/workflows/build-test-deploy.yml +++ b/.github/workflows/build-test-deploy.yml @@ -105,7 +105,7 @@ jobs: with: report_paths: 'miner/build/test-results/test/*.xml' - name: Publish coverage on CodeCov - uses: codecov/codecov-action@v4.3.1 + uses: codecov/codecov-action@v4.4.1 if: always() with: token: ${{ secrets.CODECOV_TOKEN }} @@ -169,7 +169,7 @@ jobs: with: report_paths: 'viewer/build/test-results/test/*.xml' - name: Publish coverage on CodeCov - uses: codecov/codecov-action@v4.3.1 + uses: codecov/codecov-action@v4.4.1 if: always() with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/gradle.properties b/gradle.properties index c8d56288..e4b89c59 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=2.2.15 +version=2.2.16 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f8525261..88c0c20f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,12 +19,12 @@ awaitility-version = "4.2.1" json-unit-version = "3.2.7" kitteh-irc-version = "9.0.0" hikari-cp-version = "5.1.0" -mariadb-version = "3.3.3" +mariadb-version = "3.4.0" sqlite-version = "3.45.3.0" mysql-version = "8.4.0" rerunner-jupiter-version = "2.1.6" flyway-version = "10.13.0" -selenide-version = "7.3.1" +selenide-version = "7.3.2" lombok-version = "1.18.32" jacocoVersion = "0.8.12" diff --git a/miner/docs/modules/ROOT/examples/global-config-schema.json b/miner/docs/modules/ROOT/examples/global-config-schema.json index bdb9365b..76f0e1f7 100644 --- a/miner/docs/modules/ROOT/examples/global-config-schema.json +++ b/miner/docs/modules/ROOT/examples/global-config-schema.json @@ -354,7 +354,7 @@ "const" : "mostTrusted" } }, - "description" : "Choose the outcome that's backed by other users with the highest average return-on-investment. Requires analytics to be enabled and recordChatsPredictions to be activated.", + "description" : "Choose the outcome that's backed by other users with the highest average return-on-investment. Requires analytics to be enabled and recordUserPredictions to be activated.", "required" : [ "type" ] diff --git a/miner/docs/modules/ROOT/examples/streamer-config-schema.json b/miner/docs/modules/ROOT/examples/streamer-config-schema.json index cbc9f228..39f09a42 100644 --- a/miner/docs/modules/ROOT/examples/streamer-config-schema.json +++ b/miner/docs/modules/ROOT/examples/streamer-config-schema.json @@ -274,7 +274,7 @@ "const" : "mostTrusted" } }, - "description" : "Choose the outcome that's backed by other users with the highest average return-on-investment. Requires analytics to be enabled and recordChatsPredictions to be activated.", + "description" : "Choose the outcome that's backed by other users with the highest average return-on-investment. Requires analytics to be enabled and recordUserPredictions to be activated.", "required" : [ "type" ] diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/GQLApi.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/GQLApi.java index 7038d3a6..1b954c47 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/GQLApi.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/GQLApi.java @@ -201,6 +201,11 @@ public List allChannelFollows(){ String cursor = null; do{ var response = channelFollows(100, ORDER_DESC, cursor); + if(response.isEmpty()){ + log.error("Failed to load follows, response is empty"); + break; + } + var followConnection = response.map(GQLResponse::getData).map(ChannelFollowsData::getUser).map(User::getFollows); followConnection.stream() diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/dropshighlightserviceavailabledrops/DropsHighlightServiceAvailableDropsOperation.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/dropshighlightserviceavailabledrops/DropsHighlightServiceAvailableDropsOperation.java index ae627754..a1942f72 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/dropshighlightserviceavailabledrops/DropsHighlightServiceAvailableDropsOperation.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/dropshighlightserviceavailabledrops/DropsHighlightServiceAvailableDropsOperation.java @@ -15,7 +15,7 @@ public class DropsHighlightServiceAvailableDropsOperation extends IGQLOperation{ public DropsHighlightServiceAvailableDropsOperation(@NotNull String channelId){ super("DropsHighlightService_AvailableDrops"); - addPersistedQueryExtension(new PersistedQueryExtension(1, "9a62a09bce5b53e26e64a671e530bc599cb6aab1e5ba3cbd5d85966d3940716f")); + addPersistedQueryExtension(new PersistedQueryExtension(1, "962510a535f25f33bbf85d7767982e3bb6d1b00f84dd3c7a06d8572323dfd010")); addVariable("channelID", channelId); } diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/DropBenefitEdge.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/DropBenefitEdge.java index efb89d36..f605b5dc 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/DropBenefitEdge.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/DropBenefitEdge.java @@ -8,6 +8,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; +import org.jetbrains.annotations.Nullable; @JsonTypeName("DropBenefitEdge") @Getter @@ -22,5 +23,6 @@ public class DropBenefitEdge extends GQLType{ @JsonProperty("entitlementLimit") private int entitlementLimit; @JsonProperty("claimCount") - private int claimCount; + @Nullable + private Integer claimCount; } diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/DropCampaign.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/DropCampaign.java index d8717a24..974f5d87 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/DropCampaign.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/DropCampaign.java @@ -74,4 +74,7 @@ public class DropCampaign extends GQLType{ @JsonProperty("allow") @Nullable private DropCampaignACL allow; + @JsonProperty("summary") + @Nullable + private DropCampaignSummary summary; } diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/DropCampaignSummary.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/DropCampaignSummary.java new file mode 100644 index 00000000..27172295 --- /dev/null +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/DropCampaignSummary.java @@ -0,0 +1,22 @@ +package fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@JsonTypeName("DropCampaignSummary") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +@ToString +public class DropCampaignSummary extends GQLType{ + @JsonProperty("includesSubRequirement") + private boolean includesSubRequirement; +} diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/GQLType.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/GQLType.java index 474eae6a..9604a893 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/GQLType.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/GQLType.java @@ -60,6 +60,8 @@ @JsonSubTypes.Type(value = CommunityMoment.class, name = "CommunityMoment"), @JsonSubTypes.Type(value = CommunityPointsEmoteModifier.class, name = "CommunityPointsEmoteModifier"), @JsonSubTypes.Type(value = StreamPlaybackAccessToken.class, name = "PlaybackAccessToken"), + @JsonSubTypes.Type(value = RewardCampaign.class, name = "RewardCampaign"), + @JsonSubTypes.Type(value = DropCampaignSummary.class, name = "DropCampaignSummary"), }) @EqualsAndHashCode public abstract class GQLType{ diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/Inventory.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/Inventory.java index 9557c793..19ac0d9a 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/Inventory.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/Inventory.java @@ -34,7 +34,7 @@ public class Inventory extends GQLType{ @JsonProperty("completedRewardCampaigns") @NotNull @Builder.Default - private List completedRewardCampaigns = new ArrayList<>(); + private List completedRewardCampaigns = new ArrayList<>(); @JsonProperty("gameEventDrops") @NotNull @Builder.Default diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/RewardCampaign.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/RewardCampaign.java new file mode 100644 index 00000000..3bd68e9c --- /dev/null +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/data/types/RewardCampaign.java @@ -0,0 +1,49 @@ +package fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import fr.rakambda.channelpointsminer.miner.util.json.ISO8601ZonedDateTimeDeserializer; +import fr.rakambda.channelpointsminer.miner.util.json.URLDeserializer; +import fr.rakambda.channelpointsminer.miner.util.json.UnknownDeserializer; +import lombok.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.net.URL; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; + +@JsonTypeName("RewardCampaign") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +@ToString +public class RewardCampaign extends GQLType{ + @JsonProperty("id") + @NotNull + private String id; + @JsonProperty("name") + @Nullable + private String name; + @JsonProperty("brand") + @Nullable + private String brand; + @JsonProperty("externalURL") + @JsonDeserialize(using = URLDeserializer.class) + @Nullable + private URL externalUrl; + @JsonProperty("startAt") + @JsonDeserialize(using = ISO8601ZonedDateTimeDeserializer.class) + @Nullable + private ZonedDateTime startAt; + @JsonProperty("endAt") + @JsonDeserialize(using = ISO8601ZonedDateTimeDeserializer.class) + @Nullable + private ZonedDateTime endAt; + @JsonProperty("status") + @Nullable + private String status; +} diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/integrity/browser/BrowserIntegrityProvider.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/integrity/browser/BrowserIntegrityProvider.java index 22835869..e499e09b 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/integrity/browser/BrowserIntegrityProvider.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/gql/integrity/browser/BrowserIntegrityProvider.java @@ -16,7 +16,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.jetbrains.annotations.NotNull; -import org.openqa.selenium.devtools.v122.page.model.FrameId; +import org.openqa.selenium.devtools.v125.page.model.FrameId; import java.io.IOException; import java.time.Duration; import java.util.Comparator; diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/twitch/TwitchApi.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/twitch/TwitchApi.java index 518fa742..6c5f046b 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/twitch/TwitchApi.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/twitch/TwitchApi.java @@ -22,8 +22,8 @@ public class TwitchApi{ private static final Pattern SETTINGS_URL_PATTERN = Pattern.compile("(https://static.twitchcdn.net/config/settings.*?js|https://assets.twitch.tv/config/settings.*?.js)"); private static final Pattern SPADE_URL_PATTERN = Pattern.compile("\"spade(Url|_url)\":\"(.*?)\""); - private static final Pattern M3U8_STREAM_PATTERN = Pattern.compile("(https://[/\\-.:\\\\,\"=\\w]*m3u8)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); - private static final Pattern M3U8_CHUNK_PATTERN = Pattern.compile("(https://(video-edge-|[.\\w\\-/]+\\.ttvnw.net)[.\\w\\-/]+\\.ts(\\?[.\\w\\-/=&]+)?)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); + private static final Pattern M3U8_STREAM_PATTERN = Pattern.compile("(https://[/\\-.:\\\\,\"=\\w]+\\.m3u8)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); + private static final Pattern M3U8_CHUNK_PATTERN = Pattern.compile("^(https://[/\\-.:\\\\,\"=\\w]+\\.ts(\\?[.\\w\\-/=&]+)?)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); private final UnirestInstance unirest; @@ -138,6 +138,11 @@ public Optional getM3u8Url(@NotNull String login, @NotNull String signature .asString(); if(!response.isSuccess()){ + if(response.getStatus() == 403){ + log.trace("Got 403 response for m3u8 playlist, is streamer region locked? (#783)"); + return Optional.empty(); + } + log.error("Failed to get streamer M3U8 content"); return Optional.empty(); } diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/ws/data/message/IPubSubMessage.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/ws/data/message/IPubSubMessage.java index d1a4129a..d9d790d6 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/ws/data/message/IPubSubMessage.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/ws/data/message/IPubSubMessage.java @@ -36,6 +36,7 @@ @JsonSubTypes.Type(value = CommunityMomentStart.class, name = "active"), @JsonSubTypes.Type(value = ActiveMultipliersUpdated.class, name = "active-multipliers-updated"), @JsonSubTypes.Type(value = ReadNotifications.class, name = "read-notifications"), + @JsonSubTypes.Type(value = ReadAllNotifications.class, name = "read-all-notifications"), @JsonSubTypes.Type(value = DropProgress.class, name = "drop-progress"), @JsonSubTypes.Type(value = DropClaim.class, name = "drop-claim"), }) diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/ws/data/message/ReadAllNotifications.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/ws/data/message/ReadAllNotifications.java new file mode 100644 index 00000000..e1daed3f --- /dev/null +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/ws/data/message/ReadAllNotifications.java @@ -0,0 +1,25 @@ +package fr.rakambda.channelpointsminer.miner.api.ws.data.message; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import fr.rakambda.channelpointsminer.miner.api.ws.data.message.readallnotifications.ReadAllNotificationsData; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; + +@JsonTypeName("read-all-notifications") +@Getter +@EqualsAndHashCode(callSuper = true) +@ToString +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ReadAllNotifications extends IPubSubMessage{ + @JsonProperty("data") + @NotNull + private ReadAllNotificationsData data; +} diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/ws/data/message/readallnotifications/ReadAllNotificationsData.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/ws/data/message/readallnotifications/ReadAllNotificationsData.java new file mode 100644 index 00000000..d928d7f7 --- /dev/null +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/api/ws/data/message/readallnotifications/ReadAllNotificationsData.java @@ -0,0 +1,32 @@ +package fr.rakambda.channelpointsminer.miner.api.ws.data.message.readallnotifications; + +import com.fasterxml.jackson.annotation.JsonProperty; +import fr.rakambda.channelpointsminer.miner.api.ws.data.message.subtype.NotificationSummary; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.LinkedList; +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@EqualsAndHashCode +@ToString +@Builder +public class ReadAllNotificationsData{ + @JsonProperty("notification_ids") + @Nullable + private List notificationIds; + @JsonProperty("display_type") + @NotNull + private String displayType; + @JsonProperty("summary") + @NotNull + private NotificationSummary summary; +} diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/browser/Browser.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/browser/Browser.java index 5322f9f1..c6f3c001 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/browser/Browser.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/browser/Browser.java @@ -16,10 +16,10 @@ import org.openqa.selenium.devtools.Command; import org.openqa.selenium.devtools.DevTools; import org.openqa.selenium.devtools.HasDevTools; -import org.openqa.selenium.devtools.v122.network.Network; -import org.openqa.selenium.devtools.v122.network.model.RequestId; -import org.openqa.selenium.devtools.v122.network.model.RequestWillBeSent; -import org.openqa.selenium.devtools.v122.network.model.ResponseReceived; +import org.openqa.selenium.devtools.v125.network.Network; +import org.openqa.selenium.devtools.v125.network.model.RequestId; +import org.openqa.selenium.devtools.v125.network.model.RequestWillBeSent; +import org.openqa.selenium.devtools.v125.network.model.ResponseReceived; import org.openqa.selenium.firefox.FirefoxDriver; import org.openqa.selenium.firefox.FirefoxOptions; import org.openqa.selenium.firefox.FirefoxProfile; diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/log/UnirestLogger.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/log/UnirestLogger.java index 5f8f774f..0c011564 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/log/UnirestLogger.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/log/UnirestLogger.java @@ -11,6 +11,7 @@ @Log4j2 public class UnirestLogger implements Interceptor{ private static final String TOKEN_URL = "https://id.twitch.tv/oauth2/token"; + private static final String M3U8_URL = "https://usher.ttvnw.net/api/channel/hls/"; @Override public void onRequest(HttpRequest request, Config config){ @@ -44,6 +45,10 @@ private static boolean shouldLogError(@NotNull HttpRequestSummary request, @NotN return false; } + if(request.getUrl().startsWith(M3U8_URL) && response.getStatus() == 403){ + return false; + } + return true; } } diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/prediction/bet/outcome/MostTrustedPicker.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/prediction/bet/outcome/MostTrustedPicker.java index 4cfebaef..56eea4f8 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/prediction/bet/outcome/MostTrustedPicker.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/prediction/bet/outcome/MostTrustedPicker.java @@ -28,7 +28,7 @@ @NoArgsConstructor @AllArgsConstructor @Log4j2 -@JsonClassDescription("Choose the outcome that's backed by other users with the highest average return-on-investment. Requires analytics to be enabled and recordChatsPredictions to be activated.") +@JsonClassDescription("Choose the outcome that's backed by other users with the highest average return-on-investment. Requires analytics to be enabled and recordUserPredictions to be activated.") public class MostTrustedPicker implements IOutcomePicker{ @JsonProperty("minTotalBetsPlacedByUser") diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/priority/DropsPriority.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/priority/DropsPriority.java index 9682eda2..2ffb60a1 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/priority/DropsPriority.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/priority/DropsPriority.java @@ -3,10 +3,14 @@ import com.fasterxml.jackson.annotation.JsonClassDescription; import com.fasterxml.jackson.annotation.JsonTypeName; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.dropshighlightserviceavailabledrops.DropsHighlightServiceAvailableDropsData; +import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.inventory.InventoryData; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.Channel; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.DropBenefitEdge; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.DropCampaign; +import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.DropCampaignSummary; +import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.Inventory; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.TimeBasedDrop; +import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.User; import fr.rakambda.channelpointsminer.miner.factory.TimeFactory; import fr.rakambda.channelpointsminer.miner.miner.IMiner; import fr.rakambda.channelpointsminer.miner.streamer.Streamer; @@ -18,6 +22,7 @@ import lombok.extern.log4j.Log4j2; import org.jetbrains.annotations.NotNull; import java.util.Collection; +import java.util.Objects; import java.util.Optional; @JsonTypeName("drops") @@ -33,23 +38,23 @@ public class DropsPriority extends IStreamerPriority{ public int getScore(@NotNull IMiner miner, @NotNull Streamer streamer){ if(streamer.isParticipateCampaigns() && streamer.isStreamingGame() - && hasCampaigns(streamer)){ + && hasCampaigns(miner, streamer)){ return getScore(); } return 0; } - private boolean hasCampaigns(@NotNull Streamer streamer){ + private boolean hasCampaigns(@NotNull IMiner miner, @NotNull Streamer streamer){ return Optional.ofNullable(streamer.getDropsHighlightServiceAvailableDrops()) .map(DropsHighlightServiceAvailableDropsData::getChannel) .map(Channel::getViewerDropCampaigns) .stream() .flatMap(Collection::stream) - .anyMatch(this::isValidCampaign); + .anyMatch(dropCampaign -> isValidCampaign(miner, streamer, dropCampaign)); } - private boolean isValidCampaign(@NotNull DropCampaign dropCampaign){ + private boolean isValidCampaign(@NotNull IMiner miner, @NotNull Streamer streamer, @NotNull DropCampaign dropCampaign){ var now = TimeFactory.nowZoned(); if(Optional.ofNullable(dropCampaign.getStartAt()).map(date -> date.isAfter(now)).orElse(false)){ @@ -60,15 +65,19 @@ private boolean isValidCampaign(@NotNull DropCampaign dropCampaign){ log.trace("Campaign {} already ended", dropCampaign.getId()); return false; } + if(streamer.isExcludeSubscriberDrops() && Optional.ofNullable(dropCampaign.getSummary()).map(DropCampaignSummary::isIncludesSubRequirement).orElse(false)){ + log.trace("Campaign {} requires subscriptions", dropCampaign.getId()); + return false; + } - var result = dropCampaign.getTimeBasedDrops().stream().anyMatch(this::isValidDrop); + var result = dropCampaign.getTimeBasedDrops().stream().anyMatch(timeBasedDrop -> isValidDrop(miner, timeBasedDrop)); if(!result){ log.trace("Campaign {} has no valid drops", dropCampaign.getId()); } return result; } - private boolean isValidDrop(@NotNull TimeBasedDrop timeBasedDrop){ + private boolean isValidDrop(@NotNull IMiner miner, @NotNull TimeBasedDrop timeBasedDrop){ var now = TimeFactory.nowZoned(); if(Optional.ofNullable(timeBasedDrop.getStartAt()).map(date -> date.isAfter(now)).orElse(false)){ @@ -83,14 +92,23 @@ private boolean isValidDrop(@NotNull TimeBasedDrop timeBasedDrop){ var result = Optional.ofNullable(timeBasedDrop.getBenefitEdges()) .stream() .flatMap(Collection::stream) - .anyMatch(this::isValidBenefit); + .anyMatch(dropBenefitEdge -> isValidBenefit(miner, dropBenefitEdge)); if(!result){ log.trace("Drop {} has no valid benefit", timeBasedDrop.getId()); } return result; } - private boolean isValidBenefit(@NotNull DropBenefitEdge dropBenefitEdge){ - return dropBenefitEdge.getClaimCount() < dropBenefitEdge.getEntitlementLimit(); + private boolean isValidBenefit(@NotNull IMiner miner, @NotNull DropBenefitEdge dropBenefitEdge){ + return Optional.ofNullable(miner.getMinerData().getInventory()) + .map(InventoryData::getCurrentUser) + .map(User::getInventory) + .map(Inventory::getGameEventDrops) + .stream() + .flatMap(Collection::stream) + .filter(userDropReward -> Objects.equals(userDropReward.getId(), dropBenefitEdge.getBenefit().getId())) + .findFirst() + .map(userDropReward -> userDropReward.getTotalCount() < dropBenefitEdge.getEntitlementLimit()) + .orElse(true); } } diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/SendM3u8MinutesWatched.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/SendM3u8MinutesWatched.java index 3e401cb3..824b7b7a 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/SendM3u8MinutesWatched.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/SendM3u8MinutesWatched.java @@ -18,12 +18,12 @@ protected String getType(){ } @Override - protected boolean checkStreamer(Streamer streamer){ + protected boolean checkStreamer(@NotNull Streamer streamer){ return Objects.nonNull(streamer.getM3u8Url()); } @Override - protected boolean send(Streamer streamer){ + protected boolean send(@NotNull Streamer streamer){ return miner.getTwitchApi().openM3u8LastChunk(streamer.getM3u8Url()); } } diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/SendMinutesWatched.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/SendMinutesWatched.java index 5416a31c..bb87f8b2 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/SendMinutesWatched.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/SendMinutesWatched.java @@ -24,9 +24,9 @@ public abstract class SendMinutesWatched implements Runnable{ protected abstract String getType(); - protected abstract boolean checkStreamer(Streamer streamer); + protected abstract boolean checkStreamer(@NotNull Streamer streamer); - protected abstract boolean send(Streamer streamer); + protected abstract boolean send(@NotNull Streamer streamer); @Override public void run(){ diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/SendSpadeMinutesWatched.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/SendSpadeMinutesWatched.java index 961afa88..bfcf9401 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/SendSpadeMinutesWatched.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/SendSpadeMinutesWatched.java @@ -21,12 +21,12 @@ protected String getType(){ } @Override - protected boolean checkStreamer(Streamer streamer){ + protected boolean checkStreamer(@NotNull Streamer streamer){ return Objects.nonNull(streamer.getSpadeUrl()); } @Override - protected boolean send(Streamer streamer){ + protected boolean send(@NotNull Streamer streamer){ var streamId = streamer.getStreamId(); if(streamId.isEmpty()){ return false; diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/UpdateStreamInfo.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/UpdateStreamInfo.java index 255446b9..dd9941ec 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/UpdateStreamInfo.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/runnable/UpdateStreamInfo.java @@ -84,7 +84,7 @@ private void updateSpadeUrl(@NotNull Streamer streamer){ private void updateM3u8Url(@NotNull Streamer streamer){ log.trace("Updating m3u8 url"); - if(streamer.isStreaming()){ + if(streamer.isParticipateCampaigns() && streamer.isStreaming()){ if(Objects.isNull(streamer.getM3u8Url())){ var accessToken = miner.getGqlApi().playbackAccessToken(streamer.getUsername()); if(accessToken.isEmpty()){ diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/streamer/Streamer.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/streamer/Streamer.java index 84873b94..2adc01a2 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/streamer/Streamer.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/streamer/Streamer.java @@ -101,6 +101,10 @@ public boolean followRaids(){ return settings.isFollowRaid(); } + public boolean isExcludeSubscriberDrops(){ + return settings.isExcludeSubscriberDrops(); + } + public boolean needUpdate(){ return TimeFactory.now().isAfter(lastUpdated.plus(5, MINUTES)); } diff --git a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/streamer/StreamerSettings.java b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/streamer/StreamerSettings.java index df13faec..f28bbe52 100644 --- a/miner/src/main/java/fr/rakambda/channelpointsminer/miner/streamer/StreamerSettings.java +++ b/miner/src/main/java/fr/rakambda/channelpointsminer/miner/streamer/StreamerSettings.java @@ -51,6 +51,10 @@ public class StreamerSettings{ @JsonPropertyDescription("Join chat. Default: false") @Builder.Default private boolean joinIrc = false; + @JsonProperty("excludeSubscriberDrops") + @JsonPropertyDescription("Exclude progressing drops that require subscriptions. Default: true") + @Builder.Default + private boolean excludeSubscriberDrops = true; @JsonProperty("index") @JsonPropertyDescription("The streamer index. This value is used when streamers have the same score from the defined priorities, the one with the lowest index will be picked first. Default: 2147483647") @Builder.Default @@ -74,6 +78,7 @@ public StreamerSettings(@NotNull StreamerSettings origin){ followRaid = origin.followRaid; participateCampaigns = origin.participateCampaigns; joinIrc = origin.joinIrc; + excludeSubscriberDrops = origin.excludeSubscriberDrops; index = origin.index; predictions = new PredictionSettings(origin.predictions); priorities.addAll(origin.priorities); diff --git a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/AbstractGQLTest.java b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/AbstractGQLTest.java index 6594c385..04f41719 100644 --- a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/AbstractGQLTest.java +++ b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/AbstractGQLTest.java @@ -69,7 +69,7 @@ protected void expectBodyRequestOk(String requestBody, String responseBody){ expectGqlRequest(requestBody, 200, responseBody); } - private void expectGqlRequest(String requestBody, int responseStatus, String responseBody){ + protected void expectGqlRequest(String requestBody, int responseStatus, String responseBody){ unirest.expect(POST, "https://gql.twitch.tv/gql") .header("Authorization", "OAuth " + ACCESS_TOKEN) .header("Client-Integrity", currentIntegrityToken) diff --git a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/GQLApiDropsHighlightServiceAvailableDropsTest.java b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/GQLApiDropsHighlightServiceAvailableDropsTest.java index 951c5034..7c9f30f5 100644 --- a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/GQLApiDropsHighlightServiceAvailableDropsTest.java +++ b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/GQLApiDropsHighlightServiceAvailableDropsTest.java @@ -6,6 +6,7 @@ import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.DropBenefit; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.DropBenefitEdge; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.DropCampaign; +import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.DropCampaignSummary; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.Game; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.TimeBasedDrop; import fr.rakambda.channelpointsminer.miner.tests.UnirestMock; @@ -64,6 +65,9 @@ void nominalWithDrops(UnirestMock unirest) throws MalformedURLException{ .build())) .requiredMinutesWatched(240) .build())) + .summary(DropCampaignSummary.builder() + .includesSubRequirement(true) + .build()) .build())) .build()) .build()) @@ -100,6 +104,6 @@ void nominalNoDrops(){ @Override protected String getValidRequest(){ - return "{\"extensions\":{\"persistedQuery\":{\"sha256Hash\":\"9a62a09bce5b53e26e64a671e530bc599cb6aab1e5ba3cbd5d85966d3940716f\",\"version\":1}},\"operationName\":\"DropsHighlightService_AvailableDrops\",\"variables\":{\"channelID\":\"%s\"}}".formatted(STREAMER_ID); + return "{\"extensions\":{\"persistedQuery\":{\"sha256Hash\":\"962510a535f25f33bbf85d7767982e3bb6d1b00f84dd3c7a06d8572323dfd010\",\"version\":1}},\"operationName\":\"DropsHighlightService_AvailableDrops\",\"variables\":{\"channelID\":\"%s\"}}".formatted(STREAMER_ID); } } \ No newline at end of file diff --git a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/GQLChannelFollowsTest.java b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/GQLChannelFollowsTest.java index 69576405..ea8a811a 100644 --- a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/GQLChannelFollowsTest.java +++ b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/gql/GQLChannelFollowsTest.java @@ -137,6 +137,16 @@ void getAllFollowsNominalOnePage(){ verifyAll(); } + @Test + void getAllFollowsStopsWhenEmptyResponse(){ + expectBodyRequestOkWithIntegrityOk(VALID_QUERY.formatted(ALL_LIMIT, ORDER), "api/gql/gql/channelFollows_severalFollows.json"); + expectGqlRequest(VALID_QUERY_WITH_CURSOR.formatted("cursor-id-2", ALL_LIMIT, ORDER), 403, null); + + assertThat(tested.allChannelFollows()).hasSize(2); + + verifyAll(); + } + @Test void getAllFollowsErrorGettingCursor(){ expectBodyRequestOkWithIntegrityOk(VALID_QUERY.formatted(ALL_LIMIT, ORDER), "api/gql/gql/channelFollows_invalidCursor.json"); diff --git a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/integrity/browser/BrowserIntegrityProviderTest.java b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/integrity/browser/BrowserIntegrityProviderTest.java index 9c883775..bf664286 100644 --- a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/integrity/browser/BrowserIntegrityProviderTest.java +++ b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/gql/integrity/browser/BrowserIntegrityProviderTest.java @@ -14,14 +14,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.openqa.selenium.devtools.v122.network.model.Headers; -import org.openqa.selenium.devtools.v122.network.model.MonotonicTime; -import org.openqa.selenium.devtools.v122.network.model.Request; -import org.openqa.selenium.devtools.v122.network.model.RequestId; -import org.openqa.selenium.devtools.v122.network.model.RequestWillBeSent; -import org.openqa.selenium.devtools.v122.network.model.Response; -import org.openqa.selenium.devtools.v122.network.model.ResponseReceived; -import org.openqa.selenium.devtools.v122.page.model.FrameId; +import org.openqa.selenium.devtools.v125.network.model.Headers; +import org.openqa.selenium.devtools.v125.network.model.MonotonicTime; +import org.openqa.selenium.devtools.v125.network.model.Request; +import org.openqa.selenium.devtools.v125.network.model.RequestId; +import org.openqa.selenium.devtools.v125.network.model.RequestWillBeSent; +import org.openqa.selenium.devtools.v125.network.model.Response; +import org.openqa.selenium.devtools.v125.network.model.ResponseReceived; +import org.openqa.selenium.devtools.v125.page.model.FrameId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/twitch/TwitchApiTest.java b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/twitch/TwitchApiTest.java index 12b7cd61..e7e37aa4 100644 --- a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/twitch/TwitchApiTest.java +++ b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/twitch/TwitchApiTest.java @@ -331,6 +331,24 @@ void getM3u8UrlError(UnirestMock unirest){ assertThat(tested.getM3u8Url(CHANNEL_NAME, M3U8_SIGNATURE, M3U8_VALUE)).isEmpty(); } + + @Test + void getM3u8UrlRegionLocked(UnirestMock unirest){ + unirest.expect(GET, M3U8_URL) + .queryString("sig", M3U8_SIGNATURE) + .queryString("token", M3U8_VALUE) + .queryString("cdm", "wv") + .queryString("player_version", "1.22.0") + .queryString("player_type", "pulsar") + .queryString("player_backend", "mediaplayer") + .queryString("playlist_include_framerate", "true") + .queryString("allow_source", "true") + .queryString("transcode_mode", "cbr_v1") + .thenReturn() + .withStatus(403); + + assertThat(tested.getM3u8Url(CHANNEL_NAME, M3U8_SIGNATURE, M3U8_VALUE)).isEmpty(); + } @Test void getM3u8ChunkUrl(UnirestMock unirest) throws MalformedURLException{ diff --git a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/ws/TwitchPubSubWebSocketClientMessageTest.java b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/ws/TwitchPubSubWebSocketClientMessageTest.java index 937edacf..a76e8160 100644 --- a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/ws/TwitchPubSubWebSocketClientMessageTest.java +++ b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/api/ws/TwitchPubSubWebSocketClientMessageTest.java @@ -18,6 +18,7 @@ import fr.rakambda.channelpointsminer.miner.api.ws.data.message.RaidCancelV2; import fr.rakambda.channelpointsminer.miner.api.ws.data.message.RaidGoV2; import fr.rakambda.channelpointsminer.miner.api.ws.data.message.RaidUpdateV2; +import fr.rakambda.channelpointsminer.miner.api.ws.data.message.ReadAllNotifications; import fr.rakambda.channelpointsminer.miner.api.ws.data.message.ReadNotifications; import fr.rakambda.channelpointsminer.miner.api.ws.data.message.UpdateSummary; import fr.rakambda.channelpointsminer.miner.api.ws.data.message.ViewCount; @@ -44,6 +45,7 @@ import fr.rakambda.channelpointsminer.miner.api.ws.data.message.predictionmade.PredictionMadeData; import fr.rakambda.channelpointsminer.miner.api.ws.data.message.predictionresult.PredictionResultData; import fr.rakambda.channelpointsminer.miner.api.ws.data.message.predictionupdated.PredictionUpdatedData; +import fr.rakambda.channelpointsminer.miner.api.ws.data.message.readallnotifications.ReadAllNotificationsData; import fr.rakambda.channelpointsminer.miner.api.ws.data.message.readnotifications.ReadNotificationsData; import fr.rakambda.channelpointsminer.miner.api.ws.data.message.subtype.ActiveMultipliers; import fr.rakambda.channelpointsminer.miner.api.ws.data.message.subtype.Claim; @@ -817,6 +819,54 @@ void onReadNotifications(WebsocketMockServer server){ verify(listener, timeout(MESSAGE_TIMEOUT)).onWebSocketMessage(expected); } + @Test + void onReadAllNotifications(WebsocketMockServer server){ + server.send(TestUtils.getAllResourceContent("api/ws/readAllNotifications.json")); + + var expected = MessageResponse.builder() + .data(MessageData.builder() + .topic(Topic.builder() + .name(ONSITE_NOTIFICATIONS) + .target("123456789") + .build()) + .message(ReadAllNotifications.builder() + .data(ReadAllNotificationsData.builder() + .displayType("VIEWER") + .summary(NotificationSummary.builder() + .unseenViewCount(0) + .lastSeenAt(ZonedDateTime.of(2024, 5, 21, 12, 1, 15, 769331187, UTC)) + .viewerUnreadCount(0) + .creatorUnreadCount(0) + .summariesByDisplayType(Map.of( + NotificationDisplayType.CREATOR, NotificationSummaryByDisplayType.builder() + .unreadSummary(Summary.builder() + .count(0) + .lastReadAll(ZonedDateTime.of(2024, 5, 21, 13, 1, 15, 769331187, UTC)) + .build()) + .unseenSummary(Summary.builder() + .count(0) + .lastSeen(ZonedDateTime.of(2024, 5, 21, 14, 1, 15, 769331187, UTC)) + .build()) + .build(), + NotificationDisplayType.VIEWER, NotificationSummaryByDisplayType.builder() + .unreadSummary(Summary.builder() + .count(0) + .lastReadAll(ZonedDateTime.of(2024, 5, 21, 15, 1, 15, 769331187, UTC)) + .build()) + .unseenSummary(Summary.builder() + .count(8) + .lastSeen(ZonedDateTime.of(2024, 5, 21, 16, 1, 15, 769331187, UTC)) + .build()) + .build() + )) + .build()) + .build()) + .build()) + .build()) + .build(); + verify(listener, timeout(MESSAGE_TIMEOUT)).onWebSocketMessage(expected); + } + @Test void onDropProgress(WebsocketMockServer server){ server.send(TestUtils.getAllResourceContent("api/ws/dropProgress.json")); diff --git a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/priority/DropsPriorityTest.java b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/priority/DropsPriorityTest.java index 13e6beba..059f3680 100644 --- a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/priority/DropsPriorityTest.java +++ b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/priority/DropsPriorityTest.java @@ -1,13 +1,20 @@ package fr.rakambda.channelpointsminer.miner.priority; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.dropshighlightserviceavailabledrops.DropsHighlightServiceAvailableDropsData; +import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.inventory.InventoryData; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.Channel; +import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.DropBenefit; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.DropBenefitEdge; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.DropCampaign; +import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.DropCampaignSummary; +import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.Inventory; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.Tag; import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.TimeBasedDrop; +import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.User; +import fr.rakambda.channelpointsminer.miner.api.gql.gql.data.types.UserDropReward; import fr.rakambda.channelpointsminer.miner.factory.TimeFactory; import fr.rakambda.channelpointsminer.miner.miner.IMiner; +import fr.rakambda.channelpointsminer.miner.miner.MinerData; import fr.rakambda.channelpointsminer.miner.streamer.Streamer; import fr.rakambda.channelpointsminer.miner.tests.ParallelizableTest; import org.mockito.Mock; @@ -28,6 +35,7 @@ class DropsPriorityTest{ private static final int SCORE = 50; private static final String DROPS_TAG_ID = "c2542d6d-cd10-4532-919b-3d19f30a768b"; + private static final String DROP_ID = "c2542d6d-cd10-4532-919b-3d19f30a768c"; private static final ZonedDateTime NOW = ZonedDateTime.of(2021, 10, 10, 12, 0, 0, 0, UTC); private static final int DROP_CLAIM_LIMIT = 2; @@ -49,11 +57,26 @@ class DropsPriorityTest{ private TimeBasedDrop timeBasedDrop; @Mock private DropBenefitEdge dropBenefitEdge; + @Mock + private DropCampaignSummary dropCampaignSummary; + @Mock + private MinerData minerData; + @Mock + private InventoryData inventoryData; + @Mock + private UserDropReward userDropReward; + @Mock + private User user; + @Mock + private Inventory inventory; + @Mock + private DropBenefit dropBenefit; @BeforeEach void setUp(){ lenient().when(streamer.isParticipateCampaigns()).thenReturn(true); lenient().when(streamer.isStreamingGame()).thenReturn(true); + lenient().when(streamer.isExcludeSubscriberDrops()).thenReturn(true); lenient().when(tag.getId()).thenReturn(DROPS_TAG_ID); @@ -64,6 +87,9 @@ void setUp(){ lenient().when(dropCampaign.getStartAt()).thenReturn(NOW.minusHours(1)); lenient().when(dropCampaign.getEndAt()).thenReturn(NOW.plusHours(1)); lenient().when(dropCampaign.getTimeBasedDrops()).thenReturn(List.of(timeBasedDrop)); + lenient().when(dropCampaign.getSummary()).thenReturn(dropCampaignSummary); + + lenient().when(dropCampaignSummary.isIncludesSubRequirement()).thenReturn(false); lenient().when(timeBasedDrop.getStartAt()).thenReturn(NOW.minusMinutes(30)); lenient().when(timeBasedDrop.getEndAt()).thenReturn(NOW.plusMinutes(30)); @@ -71,6 +97,18 @@ void setUp(){ lenient().when(dropBenefitEdge.getEntitlementLimit()).thenReturn(DROP_CLAIM_LIMIT); lenient().when(dropBenefitEdge.getClaimCount()).thenReturn(1); + lenient().when(dropBenefitEdge.getBenefit()).thenReturn(dropBenefit); + + lenient().when(dropBenefit.getId()).thenReturn(DROP_ID); + + lenient().when(miner.getMinerData()).thenReturn(minerData); + lenient().when(minerData.getInventory()).thenReturn(inventoryData); + lenient().when(inventoryData.getCurrentUser()).thenReturn(user); + lenient().when(user.getInventory()).thenReturn(inventory); + lenient().when(inventory.getGameEventDrops()).thenReturn(List.of(userDropReward)); + + lenient().when(userDropReward.getId()).thenReturn(DROP_ID); + lenient().when(userDropReward.getTotalCount()).thenReturn(0); } @Test @@ -139,6 +177,17 @@ void tooLate(){ } } + @Test + void requiresSubscription(){ + try(var timeFactory = mockStatic(TimeFactory.class)){ + timeFactory.when(TimeFactory::nowZoned).thenReturn(NOW); + + when(dropCampaignSummary.isIncludesSubRequirement()).thenReturn(true); + + assertThat(tested.getScore(miner, streamer)).isEqualTo(0); + } + } + @Test void noDrops(){ try(var timeFactory = mockStatic(TimeFactory.class)){ @@ -188,7 +237,7 @@ void claimLimitReached(){ try(var timeFactory = mockStatic(TimeFactory.class)){ timeFactory.when(TimeFactory::nowZoned).thenReturn(NOW); - when(dropBenefitEdge.getClaimCount()).thenReturn(DROP_CLAIM_LIMIT); + when(userDropReward.getTotalCount()).thenReturn(DROP_CLAIM_LIMIT); assertThat(tested.getScore(miner, streamer)).isEqualTo(0); } @@ -203,6 +252,17 @@ void valid(){ } } + @Test + void validWithSubscriptionRequired(){ + try(var timeFactory = mockStatic(TimeFactory.class)){ + timeFactory.when(TimeFactory::nowZoned).thenReturn(NOW); + + when(streamer.isExcludeSubscriberDrops()).thenReturn(false); + + assertThat(tested.getScore(miner, streamer)).isEqualTo(SCORE); + } + } + @Test void validNoCampaignStartDate(){ try(var timeFactory = mockStatic(TimeFactory.class)){ @@ -246,4 +306,15 @@ void validNoDropEndDate(){ assertThat(tested.getScore(miner, streamer)).isEqualTo(SCORE); } } + + @Test + void noInventoryIsSameAsZeroInInventory(){ + try(var timeFactory = mockStatic(TimeFactory.class)){ + timeFactory.when(TimeFactory::nowZoned).thenReturn(NOW); + + when(minerData.getInventory()).thenReturn(null); + + assertThat(tested.getScore(miner, streamer)).isEqualTo(SCORE); + } + } } \ No newline at end of file diff --git a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/runnable/UpdateStreamInfoTest.java b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/runnable/UpdateStreamInfoTest.java index 49729260..5a2eda54 100644 --- a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/runnable/UpdateStreamInfoTest.java +++ b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/runnable/UpdateStreamInfoTest.java @@ -103,6 +103,7 @@ void setUp() throws MalformedURLException{ lenient().when(streamer.getId()).thenReturn(STREAMER_ID); lenient().when(streamer.getUsername()).thenReturn(STREAMER_USERNAME); lenient().when(streamer.getChannelUrl()).thenReturn(streamerUrl); + lenient().when(streamer.isParticipateCampaigns()).thenReturn(true); lenient().when(streamer.getClaimId()).thenReturn(Optional.empty()); lenient().when(streamer.needUpdate()).thenReturn(true); @@ -309,6 +310,38 @@ void updateWithDataStreamingAndSpadeAndM3u8UrlMissing(){ } } + @Test + void updateWithDataStreamingAndSpadeAndM3u8UrlMissingButDropsNotActivated(){ + try(var timeFactory = mockStatic(TimeFactory.class)){ + timeFactory.when(TimeFactory::now).thenReturn(NOW); + + when(streamer.isStreaming()).thenReturn(true); + when(streamer.isParticipateCampaigns()).thenReturn(false); + when(streamer.getSpadeUrl()).thenReturn(null); + when(gqlApi.videoPlayerStreamInfoOverlayChannel(STREAMER_USERNAME)).thenReturn(Optional.of(gqlResponseVideoPlayer)); + when(gqlApi.channelPointsContext(STREAMER_USERNAME)).thenReturn(Optional.of(gqlResponseChannelPoints)); + when(gqlApi.chatRoomBanStatus(STREAMER_ID, ACCOUNT_ID)).thenReturn(Optional.of(gqlResponseChatRoomBanStatus)); + when(twitchApi.getSpadeUrl(streamerUrl)).thenReturn(Optional.of(spadeUrl)); + + assertDoesNotThrow(() -> tested.run()); + + verify(gqlApi).videoPlayerStreamInfoOverlayChannel(STREAMER_USERNAME); + verify(gqlApi).chatRoomBanStatus(STREAMER_ID, ACCOUNT_ID); + verify(gqlApi, never()).dropsHighlightServiceAvailableDrops(anyString()); + verify(twitchApi).getSpadeUrl(streamerUrl); + + verify(streamer).setVideoPlayerStreamInfoOverlayChannel(videoPlayerStreamInfoOverlayChannelData); + verify(streamer).setChannelPointsContext(channelPointsContextData); + verify(streamer).setSpadeUrl(spadeUrl); + verify(streamer).setM3u8Url(null); + verify(streamer).setDropsHighlightServiceAvailableDrops(null); + verify(streamer).setLastUpdated(NOW); + verify(streamer).setChatBanned(false); + verify(streamer, never()).setLastOffline(any()); + verify(streamer, never()).resetWatchedDuration(); + } + } + @Test void updateWithDataStreamingAndSpadeUrlMissingAndNotReturned(){ try(var timeFactory = mockStatic(TimeFactory.class)){ @@ -332,7 +365,7 @@ void updateWithDataStreamingAndSpadeUrlMissingAndNotReturned(){ verify(streamer).setVideoPlayerStreamInfoOverlayChannel(videoPlayerStreamInfoOverlayChannelData); verify(streamer).setChannelPointsContext(channelPointsContextData); verify(streamer, never()).setSpadeUrl(any()); - verify(streamer, never()).setM3u8Url(any()); + verify(streamer, never()).setM3u8Url(null); verify(streamer).setDropsHighlightServiceAvailableDrops(null); verify(streamer).setLastUpdated(NOW); verify(streamer).setChatBanned(false); diff --git a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/streamer/StreamerSettingsTest.java b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/streamer/StreamerSettingsTest.java index 90751ebb..4e930cc3 100644 --- a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/streamer/StreamerSettingsTest.java +++ b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/streamer/StreamerSettingsTest.java @@ -22,6 +22,7 @@ void copy(){ .makePredictions(true) .participateCampaigns(true) .followRaid(true) + .excludeSubscriberDrops(false) .joinIrc(true) .index(24) .priorities(List.of(priority)) @@ -34,6 +35,7 @@ void copy(){ assertThat(copy.isEnabled()).isEqualTo(tested.isEnabled()); assertThat(copy.isMakePredictions()).isEqualTo(tested.isMakePredictions()); assertThat(copy.isParticipateCampaigns()).isEqualTo(tested.isParticipateCampaigns()); + assertThat(copy.isExcludeSubscriberDrops()).isEqualTo(tested.isExcludeSubscriberDrops()); assertThat(copy.isFollowRaid()).isEqualTo(tested.isFollowRaid()); assertThat(copy.isJoinIrc()).isEqualTo(tested.isJoinIrc()); assertThat(copy.getIndex()).isEqualTo(tested.getIndex()); diff --git a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/streamer/StreamerTest.java b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/streamer/StreamerTest.java index e8931de9..5db5b6a4 100644 --- a/miner/src/test/java/fr/rakambda/channelpointsminer/miner/streamer/StreamerTest.java +++ b/miner/src/test/java/fr/rakambda/channelpointsminer/miner/streamer/StreamerTest.java @@ -406,6 +406,12 @@ void followRaid(){ assertThat(tested.followRaids()).isTrue(); } + @Test + void excludeSubscriberDrops(){ + when(settings.isExcludeSubscriberDrops()).thenReturn(true); + assertThat(tested.isExcludeSubscriberDrops()).isTrue(); + } + @Test void doesNotFollowRaid(){ when(settings.isFollowRaid()).thenReturn(false); diff --git a/miner/src/test/resources/api/gql/gql/dropsHighlightServiceAvailableDrops_withDrops.json b/miner/src/test/resources/api/gql/gql/dropsHighlightServiceAvailableDrops_withDrops.json index b4d8bb80..5d2bc757 100644 --- a/miner/src/test/resources/api/gql/gql/dropsHighlightServiceAvailableDrops_withDrops.json +++ b/miner/src/test/resources/api/gql/gql/dropsHighlightServiceAvailableDrops_withDrops.json @@ -42,6 +42,10 @@ "__typename": "TimeBasedDrop" } ], + "summary": { + "includesSubRequirement": true, + "__typename": "DropCampaignSummary" + }, "__typename": "DropCampaign" } ], diff --git a/miner/src/test/resources/api/ws/readAllNotifications.json b/miner/src/test/resources/api/ws/readAllNotifications.json new file mode 100644 index 00000000..3a474c63 --- /dev/null +++ b/miner/src/test/resources/api/ws/readAllNotifications.json @@ -0,0 +1,7 @@ +{ + "type" : "MESSAGE", + "data" : { + "topic" : "onsite-notifications.123456789", + "message" : "{\"type\":\"read-all-notifications\",\"data\":{\"display_type\":\"VIEWER\",\"summary\":{\"unseen_view_count\":0,\"last_seen_at\":\"2024-05-21T12:01:15.769331187Z\",\"viewer_unread_count\":0,\"creator_unread_count\":0,\"summaries_by_display_type\":{\"CREATOR\":{\"unread_summary\":{\"count\":0,\"last_read_all\":\"2024-05-21T13:01:15.769331187Z\",\"count_by_md\":{}},\"unseen_summary\":{\"count\":0,\"last_seen\":\"2024-05-21T14:01:15.769331187Z\",\"count_by_md\":{}}},\"VIEWER\":{\"unread_summary\":{\"count\":0,\"last_read_all\":\"2024-05-21T15:01:15.769331187Z\",\"count_by_md\":{}},\"unseen_summary\":{\"count\":8,\"last_seen\":\"2024-05-21T16:01:15.769331187Z\",\"count_by_md\":{}}}}}}}" + } +} \ No newline at end of file