Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ nbactions.xml
nb-configuration.xml
nbproject/
.DS_Store
bin/
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,76 @@ default Color chatTextColor() {
return new Color(0x0040FF);
}

@ConfigSection(
name = "Webhook",
description = "Configure webhook notifications",
position = 5
)
String webhookSection = "webhookSection";

@ConfigItem(
keyName = "webhook",
name = "Webhook URL",
description = "The Discord Webhook URL for sending display name and profit.",
section = notificationsSection,
position = 5
description = "The Discord Webhook URL.",
section = webhookSection,
position = 1
)
String webhook();

@ConfigItem(
keyName = "webhookMentionText",
name = "Mention text",
description = "Text to prepend to alerts, e.g. @YourName or <@1234567890>.",
section = webhookSection,
position = 2
)
default String webhookMentionText() { return ""; }

@ConfigItem(
keyName = "enableWebhookBuyAlerts",
name = "Buy alerts",
description = "Notify on buy alerts.",
section = webhookSection,
position = 3
)
default boolean enableWebhookBuyAlerts() { return false; }

@ConfigItem(
keyName = "enableWebhookSellAlerts",
name = "Sell alerts",
description = "Notify on sell alerts.",
section = webhookSection,
position = 4
)
default boolean enableWebhookSellAlerts() { return false; }

@ConfigItem(
keyName = "enableWebhookDumpAlerts",
name = "Dump alerts",
description = "Notify on dump alerts.",
section = webhookSection,
position = 5
)
default boolean enableWebhookDumpAlerts() { return true; }

@ConfigItem(
keyName = "enableWebhookAbortCollectAlerts",
name = "Abort/Collect alerts",
description = "Notify when you should abort an offer or collect items.",
section = webhookSection,
position = 6
)
default boolean enableWebhookAbortCollectAlerts() { return false; }

@ConfigItem(
keyName = "webhookAlertsOnlyWhenOutOfFocus",
name = "Only alert when unfocused",
description = "Only send Buy/Sell/Dump webhooks when RuneLite is out of focus. Session stats always send.",
section = webhookSection,
position = 7
)
default boolean webhookAlertsOnlyWhenOutOfFocus() { return false; }



}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@

import javax.inject.Inject;
import javax.swing.*;
import java.awt.Canvas;
import java.awt.Window;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

@Slf4j
Expand Down Expand Up @@ -134,6 +139,10 @@ public ExecutorService provideExecutorService(@Named("copilotExecutor") Schedule
private MainPanel mainPanel;
private StatsPanelV2 statsPanel;
private NavigationButton navButton;
private volatile boolean focusListenerInstalled = false;
private ScheduledFuture<?> focusListenerInstallTask;
private Window focusWindow;
private WindowAdapter focusListener;

@Override
protected void startUp() throws Exception {
Expand Down Expand Up @@ -178,10 +187,76 @@ protected void startUp() throws Exception {
}
})
, 2000, 1000, TimeUnit.MILLISECONDS);

installFocusListenerEventually();
}

private void installFocusListenerEventually() {
// Window can be null early in startup, so retry a few times.
if (focusListenerInstallTask != null) {
focusListenerInstallTask.cancel(false);
}
focusListenerInstallTask = executorService.scheduleAtFixedRate(
() -> SwingUtilities.invokeLater(this::installFocusListenerIfPossible),
0,
1,
TimeUnit.SECONDS
);
}

private void installFocusListenerIfPossible() {
if (focusListenerInstalled) {
return;
}
try {
Canvas canvas = client.getCanvas();
if (canvas == null) {
return;
}
Window window = SwingUtilities.getWindowAncestor(canvas);
if (window == null) {
return;
}

focusWindow = window;
webHookController.setFocusWindow(window);
focusListener = new WindowAdapter() {
@Override
public void windowGainedFocus(WindowEvent e) {
webHookController.setClientHasFocus(true);
}

@Override
public void windowLostFocus(WindowEvent e) {
webHookController.setClientHasFocus(false);
}
};
window.addWindowFocusListener(focusListener);
webHookController.setClientHasFocus(window.isActive());
focusListenerInstalled = true;

if (focusListenerInstallTask != null) {
focusListenerInstallTask.cancel(false);
focusListenerInstallTask = null;
}
log.debug("installed window focus listener for webhook gating");
} catch (Exception e) {
log.debug("failed to install window focus listener", e);
}
}

@Override
protected void shutDown() throws Exception {
if (focusListenerInstallTask != null) {
focusListenerInstallTask.cancel(false);
focusListenerInstallTask = null;
}
if (focusWindow != null && focusListener != null) {
try {
focusWindow.removeWindowFocusListener(focusListener);
} catch (Exception ignored) {
}
}
offerManager.saveAll();
highlightController.removeAll();
clientThread.invokeLater(() -> slotProfitColorizer.resetAllSlots());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public class SuggestionController {
private final GrandExchange grandExchange;
private final ScheduledExecutorService executorService;
private final ApiRequestHandler apiRequestHandler;
private final WebHookController webHookController;

private final Notifier notifier;
private final OfferManager offerManager;
private final CopilotLoginManager copilotLoginManager;
Expand All @@ -56,8 +58,9 @@ public class SuggestionController {

private MainPanel mainPanel;
private LoginPanel loginPanel;
private CopilotPanel copilotPanel;
private SuggestionPanel suggestionPanel;
private CopilotPanel copilotPanel;
private SuggestionPanel suggestionPanel;
private boolean lastCollectNeeded = false;

public void togglePause() {
if (pausedManager.isPaused()) {
Expand Down Expand Up @@ -199,14 +202,53 @@ private synchronized void handleSuggestionReceived(Suggestion oldSuggestion, Sug
if (newSuggestion.isDumpAlert && config.dumpAlertSound()) {
client.playSoundEffect(SoundEffectID.GE_ADD_OFFER_DINGALING);
}
boolean shouldNotify = shouldNotify(newSuggestion, oldSuggestion);

if (newSuggestion.isDumpAlert && shouldNotify) {
// Send short-format webhook for dump alert: Item, Price
try {
webHookController.sendDumpAlert(newSuggestion);
} catch (Exception e) {
log.debug("Error sending dump webhook", e);
}
}

suggestionManager.setSuggestion(newSuggestion);
suggestionManager.setSuggestionError(null);
suggestionManager.setSuggestionRequestInProgress(false);
log.debug("Received suggestion: {}", newSuggestion.toString());
accountStatusManager.resetSkipSuggestion();
offerManager.setOfferJustPlaced(false);
suggestionPanel.refresh();

// Collect webhook alert (edge-triggered to avoid spam)
try {
boolean collectNeeded = accountStatus.isCollectNeeded(newSuggestion, grandExchange.isSetupOfferOpen());
if (collectNeeded && !lastCollectNeeded) {
webHookController.sendCollectAlert();
}
lastCollectNeeded = collectNeeded;
} catch (Exception e) {
log.debug("Error sending collect webhook", e);
}

showNotifications(oldSuggestion, newSuggestion, accountStatus);
// Send webhook alerts for buy/sell suggestions in short format
String type = newSuggestion.getType();
if (shouldNotify && "buy".equals(type)) {
try {
webHookController.sendBuyAlert(newSuggestion);
} catch (Exception e) { log.debug("Error sending buy webhook", e); }
} else if (shouldNotify && "sell".equals(type)) {
try {
webHookController.sendSellAlert(newSuggestion);
} catch (Exception e) { log.debug("Error sending sell webhook", e); }
} else if (shouldNotify && "abort".equals(type)) {
try {
webHookController.sendAbortAlert(newSuggestion);
} catch (Exception e) { log.debug("Error sending abort webhook", e); }
}

if (!newSuggestion.getType().equals("wait")) {
SwingUtilities.invokeLater(() -> flipDialogController.priceGraphPanel.newSuggestedItemId(
newSuggestion.getItemId(),
Expand Down Expand Up @@ -239,17 +281,17 @@ private PriceLine buildPriceLine(Suggestion suggestion) {
return null;
}

void showNotifications(Suggestion oldSuggestion, Suggestion newSuggestion, AccountStatus accountStatus) {
if (shouldNotify(newSuggestion, oldSuggestion)) {
String msg = newSuggestion.toMessage();
if (config.enableTrayNotifications()) {
notifier.notify(msg);
}
if (!copilotPanel.isShowing() && config.enableChatNotifications()) {
showChatNotifications(newSuggestion, accountStatus);
}
}
}
void showNotifications(Suggestion oldSuggestion, Suggestion newSuggestion, AccountStatus accountStatus) {
if (shouldNotify(newSuggestion, oldSuggestion)) {
String msg = newSuggestion.toMessage();
if (config.enableTrayNotifications()) {
notifier.notify(msg);
}
if (!copilotPanel.isShowing() && config.enableChatNotifications()) {
showChatNotifications(newSuggestion, accountStatus);
}
}
}

static boolean shouldNotify(Suggestion newSuggestion, Suggestion oldSuggestion) {
if (newSuggestion.getType().equals("wait")) {
Expand Down
Loading